@small-web/kitten
Version:
Type-safe global Kitten namespace.
548 lines (447 loc) • 16.5 kB
TypeScript
// @ts-check
import EventEmitter from 'node:events'
export class Session extends EventEmitter {
id: string
authenticated: boolean
challenge: string
createdAt: Date
redirectToAfterSignIn?: string
hasExpired (): boolean
}
export type { default as WebSocket, BufferLike } from './ws/index.d.ts'
export type { default as slugify } from './slugify/index.d.ts'
export type { Polka } from './polka/index.d.ts'
export { default as MarkdownIt } from 'markdown-it'
export namespace yaml {
export function parse(str:string, reviver?:JavaScriptFunction, options?:{}): any
export function stringify(value:any, replacer?:JavaScriptFunction, options?:{}): string
}
/**
Upload class.
*/
interface UploadConstructorParameters {
id: string,
fileName: string,
filePath: string,
mimetype: string,
field: string,
encoding: string,
truncated: boolean,
done: boolean
}
export class Upload {
constructor(parameters:UploadConstructorParameters)
resourcePath: string
downloadPath: string
delete(): Promise<void>
}
type BoundHandler<T> = (this: T) => void
type AsyncKittenHandler = (request:KittenRequest, response:KittenResponse, next:AsyncKittenHandler) => Promise<void>
/**
Abstract base class for lazily-loaded routes.
*/
export class LazilyLoadedRoute {
filePath:string
basePath:string
extension:string
pattern:string
method: 'use' | 'get'
constructor(filePath:string, basePath:string)
get handler():BoundHandler<LazilyLoadedRoute>
lazilyLoadedhandler:AsyncKittenHandler
}
import type {default as WebSocket, BufferLike } from './ws/index.d.ts'
type WebSocketWithIsAlive = WebSocket & {isAlive:boolean}
/**
MessageSender class.
Provides a namespaced send() method for use in PageSocket.
*/
export class MessageSender {
socket:WebSocketWithIsAlive
sockets: [WebSocketWithIsAlive]
includeSelf: boolean
constructor (parameterObject: {
socket: WebSocketWithIsAlive,
connections: [WebSocketWithIsAlive],
includeSelf: boolean
})
/**
Sends message either to all connections or to all connections
excluding the current one (depending on value of `this.includeSelf`).
You can specify an optional swap target that intelligently
wraps what you’re sending with the necessary envelope tag.
This is normally rather confusing with htmx’s oob swaps,
especially when inserting table rows. See:
https://htmx.org/attributes/hx-swap-oob/#using-alternate-swap-strategies
*/
send (message: BufferLike, swapTarget?: {
before?: string,
after?: string,
asFirstChildOf?: string,
asLastChildOf?: string
}):void
}
/**
Kitten component class.
*/
/**
This type definition required due to following shenanigans:
https://github.com/Microsoft/TypeScript/issues/20007#issuecomment-2255964704
*/
type JavaScriptFunction = (...args: any[]) => any
type BoundRenderHook<T> = (this: T) => string|Array<string>
export class Listener {
/**
Create EventEmitter listener.
@param {object} target
@param {string} eventName
@param {JavaScriptFunction} eventHandler
*/
constructor (target:object, eventName:string, eventHandler:JavaScriptFunction)
/**
Remove this listener from its EventEmitter.
*/
remove ():void
}
export class KittenComponent {
id: string
children: [KittenComponent]
listeners: [Listener]
data: Record<string, any>
page: KittenPage
/** Is this component attached to the live component hiearchy? */
isAttached: boolean
/** Is the page this component is on connected to its client via WebSocket? */
isConnected: boolean
/**
Factory method: Creates an instance of the Kitten component,
connects it to the passed parent, and returns a reference to
its component function, ready to be rendered in kitten.html.
@example
```
<!-- In your page (`this` refers to the page). -->
<${MyComponent.connectedTo(this)} />
```
*/
static connectedTo (parent: KittenComponent, data?: Record<string, any>):BoundRenderHook<KittenComponent>
/**
Constructor. Not to be used directly unless being called via
super() from a subclass.
*/
constructor (data?: Record<string, any>)
/**
Returns a bound version of the render function that can
refer to the component itself as this when calling
`connectedTo()` on child components.
*/
get component ():BoundRenderHook<KittenComponent>
/**
Required hook: override this method with your own render function
that returns `kitten.html`.
*/
html(parameterObject?: Record<string, any>):(string|Array<string>|Promise<string|Array<string>>)
/**
Optional hook: override this method to run custom logic when the component
has been loaded (attached to its parent and, thus, to the component hiearchy).
At this point it will have a reference to its parent component, to any data that
may have been passed to its `connectedTo()` factory method in the `kitten.html`, and
to the page it is on (at `this.parent`, `this.data`, and `this.page`, respectively.)
However, there is no guarantee that the page this component is attached to has
connected to the client via its automatic WebSocket. For that, rely on the
`onConnect()` handler instead.
*/
onLoad(): void
/**
Optional hook: override this method to provide custom logic for your app
to be run when the page this component is on connects to the client via its WebSocket.
This hook will get called not just for initially-rendered components when the page
first connects but also for any components dynamically added to an already-connected
page/component hierarchy after the fact.
(So you can be sure that this handler will be called once when a component is fully
initialised on a connected page. This is a good place to add event handlers or to
start streaming updates to the client.)
*/
onConnect(parameterObject: {
page?: KittenComponent,
request?: KittenRequest,
response?: KittenResponse
}):void
/**
Optional hook: override this method to run custom logic when the page
this component is on disconnects from the client via its WebSocket.
(This usually means the page the about to be unloaded, either because the
person is nagivating away from it or reloading it.)
*/
onDisconnect(parameterObject: {
page?: KittenComponent,
request?: KittenRequest,
response?: KittenResponse
}):void
/**
Adds child component to this one.
Child components are entered into the event bubbling hierarchy and
contain a reference to the page that they’re on.
*/
addChild (component:KittenComponent):void
/**
Add event handler for an EventEmitter.
Event listening and listener clean-up are automatically handled so the
author doesn’t have to worry about implementing this finickety
aspect manually.
*/
addEventHandler (target: object, eventName: string, eventHandler: JavaScriptFunction):void
/**
Helper for sending an updated version of this component to the page.
*/
update ():void
/**
Helper for removing this component from the live component hierachy.
Also handles removal of event listeners for itself and all its
children so we don’t have any leaks.
*/
remove ():void
/**
Helper for adding an event handler for a page event and streaming an
updated version of the component to the client (a common pattern).
@param {string} eventName
*/
updateOnEvent (eventName:string):void
/**
Helper (handler) for sending an updated version of this
component to the page.
*/
sendUpdatedComponentToPage ():void
}
/**
KittenPage class.
A KittenPage is a specialised KittenComponent that represents a live
page in memory (a live page is one that has an automatic WebSocket connection
to the page rendered by the PageRoute). It constitutes the root of the
server-side component hierarchy. Its `html()` method is dynamically bound
by the PageRoute to the authored page route function in function-based authoring
and it is used directly via its global `kitten.Page` reference when authoring
class-based page routes.
*/
export class KittenPage extends KittenComponent {
request: KittenRequest
response: KittenResponse
session: Session
socket: WebSocketWithIsAlive
sockets: [WebSocketWithIsAlive]
data: Record<string, any>
everyone: MessageSender
everyoneElse: MessageSender
/**
Construct new KittenPage instance with a reference to the
request and response objects for the page.
Every page has a unique ID (inherited from KittenComponent) and can hold
ephemeral page-level data. e.g., the states of components when
using the Streaming HTML workfow. (Page storage is ephemeral is that
it only lasts for the lifetime of a page in the browser and is destroyed
when the page is closed or reloaded.)
If you need greater persistence, use session storage (request.session) or
the built-in JSDB database (kitten.db).
A page is an authored page if the author of the Kitten app/site wrote
and exported a KittenPage subclass in the route (as opposed to exporting a
simple function and function-based event handlers that then resulted
in a generic page being created by Kitten’s PageRoute class). Keeping
track of this is an optimisation that enabled the PageSocketRoute to
not have to import the source file again if the page was authored
as a page instance and thus already contains its event handlers (as
opposed to the event handlers being exported as separate functions that
have be read in and mixed into the generic KittenPage instance by the PageSocketRoute).
*/
constructor(request: KittenRequest, response: KittenResponse, isAuthoredPage: boolean)
/**
The page has connected to its WebSocket. The list of sockets and the
specific socket for this page are passed for storage on the page and the
list of event handlers imported from the page (when using function-based
page routes), if any, to be mixed into this instance as methods.
*/
connect (socket: WebSocketWithIsAlive, sockets: [WebSocketWithIsAlive], eventHandlers: Record<string, JavaScriptFunction>):void
/**
Dynamically add specified event handler for specified event name.
@deprecated Instead of `on(eventName, handler)`, export `onEventName()` from your page (or add `onEventName()` method to your `kitten.Page` subclass if using class-based page routes.)
*/
on (eventName: string, eventHandler: () => void):void
/**
Send specified message to just this page’s socket.
You can specify an optional swap target that intelligently
wraps what you’re sending with the necessary envelope tag.
This is normally rather confusing with htmx’s oob swaps,
especially when inserting table rows. See:
https://htmx.org/attributes/hx-swap-oob/#using-alternate-swap-strategies
*/
send (message: BufferLike|Promise<BufferLike>, swapTarget?: {
before?: string,
after?: string,
asFirstChildOf?: string,
asLastChildOf?: string
}):void
}
/**
Request and response types.
These vary per route type but are all built on the base of
Polka’s request and response objects which are, themselves,
based on the incoming message and server response types of
Node’s own http module.
*/
import type { IncomingMessage, ServerResponse } from 'http'
export interface ParsedURL {
pathname: string
search: string
query: Record<string, string | string[]> | void
raw: string
}
export type PolkaResponse = ServerResponse
export interface PolkaRequest extends IncomingMessage {
url: string
method: string
originalUrl: string
params: Record<string, string>
path: string
search: string
query: Record<string,string>
body?: any
_decoded?: true
_parsedUrl: ParsedURL
}
export interface KittenRequest extends PolkaRequest {
/**
Check if the incoming request contains the "Content-Type"
header field, and it contains the given mime `type`.
Examples:
// With Content-Type: text/html; charset=utf-8
req.is('html');
req.is('text/html');
req.is('text/*');
// => true
// When Content-Type is application/json
req.is('json');
req.is('application/json');
req.is('application/*');
// => true
req.is('html');
// => false
@link https://github.com/expressjs/express/blob/master/lib/request.js#L231
*/
is (types:Array<string>|string):string|false|null
session:Session
}
type TypedArray = Int8Array|Uint8Array|Uint8ClampedArray|Int16Array|Uint16Array|Int32Array|Uint32Array|Float32Array|Float64Array|BigInt64Array|BigUint64Array
export interface KittenResponse extends PolkaResponse {
/**
JSON.stringifies passed data and ends response with
inline JSON using proper headers.
*/
json (data:any):void
/**
JSON.stringifies passed data and ends response with JSON attachment
using proper headers and requested file name (or data.json as fallback
if no file name is provided).
*/
jsonFile (data:any, fileName?:string):void
/**
Ends response with a file. Optionally, uses passed file name
(or 'dowload' as fallback) and passed mime type (or
'application/octet-stream' as fallback).
*/
file (data:string|Buffer|TypedArray|DataView, fileName?:string, mimeType?:string):void
/**
Ends response with 200 OK response code, and the response body,
if any (and '' if not).
@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200
*/
ok (body?:string):void
/**
Ends response with 201 Created response code, and the response body,
if any (and '' if not).
@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201
*/
created (body?:string):void
/**
Ends response with redirect via a GET request (303 See Other) to given location.
Alias: seeOther
*/
get (location:string):void
/**
Redirect (temporary; 307) to requested location without changing the request method.
*/
redirect (location:string):void
/**
Redirect (permanentl 308) to requested location without changing the request method.
*/
permanentRedirect (location:string):void
/**
400 Bad Request
Indicates that the server cannot or will not process the request due to something
that is perceived to be a client error (e.g., malformed request syntax, invalid request
message framing, or deceptive request routing).
@link https://httpwg.org/specs/rfc9110.html#rfc.section.15.5.1
*/
badRequest (body?:string):void
/**
“401 Unauthorized” (actually, unauthenticated) response.
Aliases: unauthorised, unauthorized.
*/
unauthenticated (body?:string):void
/**
403 Forbidden response. This should be returned
if the request is authenticated but lacks the authorisation
(i.e., sufficient rights) to access the resource.
If the request requires authentication but has not been
authenticated, you should return a “401 Unauthorized”
(actually: unauthenticated) response.
@see unauthenticated
*/
forbidden (body?:string):void
/**
404 Not Found response.
*/
notFound (body?:string):void
/**
500 Internal Server Error response.
Alias: internalServerError.
*/
error (body?:string):void
/**
General shorthand helper for setting the status code and
ending the response with an optional body.
*/
withCode (statusCode:number, body?:string):void
}
export interface KittenPostRequest extends KittenRequest {
uploads: Upload[]
}
/* Crypto */
type Hex = Uint8Array | string;
type PrivKey = Hex | bigint | number;
export class Point {
readonly x: bigint;
readonly y: bigint;
static BASE: Point;
static ZERO: Point;
_WINDOW_SIZE?: number;
constructor(x: bigint, y: bigint);
_setWindowSize(windowSize: number): void;
static fromHex(hex: Hex, strict?: boolean): Point;
static fromPrivateKey(privateKey: PrivKey): Promise<Point>;
toRawBytes(): Uint8Array;
toHex(): string;
toX25519(): Uint8Array;
isTorsionFree(): boolean;
equals(other: Point): boolean;
negate(): Point;
add(other: Point): Point;
subtract(other: Point): Point;
multiply(scalar: number | bigint): Point;
}
export class Signature {
readonly r: Point;
readonly s: bigint;
constructor(r: Point, s: bigint);
static fromHex(hex: Hex): Signature;
assertValidity(): this;
toRawBytes(): Uint8Array;
toHex(): string;
}