diff --git a/crates/bindings-typescript/package.json b/crates/bindings-typescript/package.json index b43725abc36..a5b64a32160 100644 --- a/crates/bindings-typescript/package.json +++ b/crates/bindings-typescript/package.json @@ -182,6 +182,7 @@ "@vitest/coverage-v8": "^3.2.4", "brotli-size-cli": "^1.0.0", "eslint": "^9.33.0", + "eslint-plugin-jsdoc": "^61.5.0", "globals": "^15.14.0", "size-limit": "^11.2.0", "ts-node": "^10.9.2", diff --git a/crates/bindings-typescript/src/lib/constraints.ts b/crates/bindings-typescript/src/lib/constraints.ts index 862e75ae7ea..f779b49d309 100644 --- a/crates/bindings-typescript/src/lib/constraints.ts +++ b/crates/bindings-typescript/src/lib/constraints.ts @@ -1,4 +1,4 @@ -import type { UntypedTableDef } from './table'; +import type { table, UntypedTableDef } from './table'; import type { ColumnMetadata } from './type_builders'; /** diff --git a/crates/bindings-typescript/src/lib/indexes.ts b/crates/bindings-typescript/src/lib/indexes.ts index 392631875d6..a115b4bd2cc 100644 --- a/crates/bindings-typescript/src/lib/indexes.ts +++ b/crates/bindings-typescript/src/lib/indexes.ts @@ -1,4 +1,4 @@ -import type { RowType, UntypedTableDef } from './table'; +import type { RowType, table, UntypedTableDef } from './table'; import type { ColumnMetadata, IndexTypes } from './type_builders'; import type { CollapseTuple, Prettify } from './type_util'; import { Range } from '../server/range'; diff --git a/crates/bindings-typescript/src/lib/procedures.ts b/crates/bindings-typescript/src/lib/procedures.ts index 0930745704f..e69de29bb2d 100644 --- a/crates/bindings-typescript/src/lib/procedures.ts +++ b/crates/bindings-typescript/src/lib/procedures.ts @@ -1,137 +0,0 @@ -import { AlgebraicType, ProductType } from '../lib/algebraic_type'; -import type { ConnectionId } from '../lib/connection_id'; -import type { Identity } from '../lib/identity'; -import type { Timestamp } from '../lib/timestamp'; -import type { HttpClient } from '../server/http_internal'; -import type { ParamsObj, ReducerCtx } from './reducers'; -import { - MODULE_DEF, - registerTypesRecursively, - type UntypedSchemaDef, -} from './schema'; -import { - type Infer, - type InferTypeOfRow, - type TypeBuilder, -} from './type_builders'; -import type { CamelCase } from './type_util'; -import { - bsatnBaseSize, - coerceParams, - toCamelCase, - type CoerceParams, -} from './util'; - -export type ProcedureFn< - S extends UntypedSchemaDef, - Params extends ParamsObj, - Ret extends TypeBuilder, -> = (ctx: ProcedureCtx, args: InferTypeOfRow) => Infer; - -export interface ProcedureCtx { - readonly sender: Identity; - readonly identity: Identity; - readonly timestamp: Timestamp; - readonly connectionId: ConnectionId | null; - readonly http: HttpClient; - withTx(body: (ctx: TransactionCtx) => T): T; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface TransactionCtx - extends ReducerCtx {} - -export function procedure< - S extends UntypedSchemaDef, - Params extends ParamsObj, - Ret extends TypeBuilder, ->(name: string, params: Params, ret: Ret, fn: ProcedureFn) { - const paramsType: ProductType = { - elements: Object.entries(params).map(([n, c]) => ({ - name: n, - algebraicType: registerTypesRecursively( - 'typeBuilder' in c ? c.typeBuilder : c - ).algebraicType, - })), - }; - const returnType = registerTypesRecursively(ret).algebraicType; - - MODULE_DEF.miscExports.push({ - tag: 'Procedure', - value: { - name, - params: paramsType, - returnType, - }, - }); - - PROCEDURES.push({ - fn, - paramsType, - returnType, - returnTypeBaseSize: bsatnBaseSize(MODULE_DEF.typespace, returnType), - }); -} - -export const PROCEDURES: Array<{ - fn: ProcedureFn; - paramsType: ProductType; - returnType: AlgebraicType; - returnTypeBaseSize: number; -}> = []; - -export type UntypedProcedureDef = { - name: string; - accessorName: string; - params: CoerceParams; - returnType: TypeBuilder; -}; - -export type UntypedProceduresDef = { - procedures: readonly UntypedProcedureDef[]; -}; - -export function procedures( - ...handles: H -): { procedures: H }; - -export function procedures( - handles: H -): { procedures: H }; - -export function procedures( - ...args: [H] | H -): { procedures: H } { - const procedures = ( - args.length === 1 && Array.isArray(args[0]) ? args[0] : args - ) as H; - return { procedures }; -} - -type ProcedureDef< - Name extends string, - Params extends ParamsObj, - ReturnType extends TypeBuilder, -> = { - name: Name; - accessorName: CamelCase; - params: CoerceParams; - returnType: ReturnType; -}; - -export function procedureSchema< - ProcedureName extends string, - Params extends ParamsObj, - ReturnType extends TypeBuilder, ->( - name: ProcedureName, - params: Params, - returnType: ReturnType -): ProcedureDef { - return { - name, - accessorName: toCamelCase(name), - params: coerceParams(params), - returnType, - }; -} diff --git a/crates/bindings-typescript/src/lib/reducers.ts b/crates/bindings-typescript/src/lib/reducers.ts index 3baef36bcab..022e64799be 100644 --- a/crates/bindings-typescript/src/lib/reducers.ts +++ b/crates/bindings-typescript/src/lib/reducers.ts @@ -1,28 +1,13 @@ -import { ProductType } from './algebraic_type'; -import Lifecycle from './autogen/lifecycle_type'; -import type RawReducerDefV9 from './autogen/raw_reducer_def_v_9_type'; +import type { DbView } from '../server/db_view'; import type { ConnectionId } from './connection_id'; import type { Identity } from './identity'; +import { type UntypedSchemaDef } from './schema'; import type { Timestamp } from './timestamp'; -import type { UntypedReducersDef } from '../sdk/reducers'; -import type { DbView } from '../server/db_view'; -import { - MODULE_DEF, - registerTypesRecursively, - resolveType, - type UntypedSchemaDef, -} from './schema'; import { ColumnBuilder, - RowBuilder, - type Infer, type InferTypeOfRow, - type RowObj, type TypeBuilder, } from './type_builders'; -import type { ReducerSchema } from './reducer_schema'; -import { toCamelCase, toPascalCase } from './util'; -import type { CamelCase } from './type_util'; /** * Helper to extract the parameter types from an object type @@ -35,7 +20,8 @@ export type ParamsObj = Record< /** * Helper to convert a ParamsObj or RowObj into an object type */ -type ParamsAsObject = InferTypeOfRow; +export type ParamsAsObject = + InferTypeOfRow; /** * Defines a SpacetimeDB reducer function. @@ -66,7 +52,7 @@ type ParamsAsObject = InferTypeOfRow; export type Reducer = ( ctx: ReducerCtx, payload: ParamsAsObject -) => void | { tag: 'ok' } | { tag: 'err'; value: string }; +) => void; /** * Authentication information for the caller of a reducer. @@ -121,263 +107,3 @@ export type ReducerCtx = Readonly<{ db: DbView; senderAuth: AuthCtx; }>; - -/** - * internal: pushReducer() helper used by reducer() and lifecycle wrappers - * - * @param name - The name of the reducer. - * @param params - The parameters for the reducer. - * @param fn - The reducer function. - * @param lifecycle - Optional lifecycle hooks for the reducer. - */ -export function pushReducer( - name: string, - params: RowObj | RowBuilder, - fn: Reducer, - lifecycle?: Infer['lifecycle'] -): void { - if (existingReducers.has(name)) { - throw new TypeError(`There is already a reducer with the name '${name}'`); - } - existingReducers.add(name); - - if (!(params instanceof RowBuilder)) { - params = new RowBuilder(params); - } - - if (params.typeName === undefined) { - params.typeName = toPascalCase(name); - } - - const ref = registerTypesRecursively(params); - const paramsType = resolveType(MODULE_DEF.typespace, ref).value; - - MODULE_DEF.reducers.push({ - name, - params: paramsType, - lifecycle, // <- lifecycle flag lands here - }); - - // If the function isn't named (e.g. `function foobar() {}`), give it the same - // name as the reducer so that it's clear what it is in in backtraces. - if (!fn.name) { - Object.defineProperty(fn, 'name', { value: name, writable: false }); - } - - REDUCERS.push(fn); -} - -const existingReducers = new Set(); -export const REDUCERS: Reducer[] = []; - -/** - * Defines a SpacetimeDB reducer function. - * - * Reducers are the primary way to modify the state of your SpacetimeDB application. - * They are atomic, meaning that either all operations within a reducer succeed, - * or none of them do. - * - * @template S - The inferred schema type of the SpacetimeDB module. - * @template Params - The type of the parameters object expected by the reducer. - * - * @param {string} name - The name of the reducer. This name will be used to call the reducer from clients. - * @param {Params} params - An object defining the parameters that the reducer accepts. - * Each key-value pair represents a parameter name and its corresponding - * {@link TypeBuilder} or {@link ColumnBuilder}. - * @param {(ctx: ReducerCtx, payload: ParamsAsObject) => void} fn - The reducer function itself. - * - `ctx`: The reducer context, providing access to `sender`, `timestamp`, `connection_id`, and `db`. - * - `payload`: An object containing the arguments passed to the reducer, typed according to `params`. - * - * @example - * ```typescript - * // Define a reducer named 'create_user' that takes 'username' (string) and 'email' (string) - * reducer( - * 'create_user', - * { - * username: t.string(), - * email: t.string(), - * }, - * (ctx, { username, email }) => { - * // Access the 'user' table from the database view in the context - * ctx.db.user.insert({ username, email, created_at: ctx.timestamp }); - * console.log(`User ${username} created by ${ctx.sender.identityId}`); - * } - * ); - * ``` - */ -export function reducer( - name: string, - params: Params, - fn: (ctx: ReducerCtx, payload: ParamsAsObject) => void -): void { - pushReducer(name, params, fn); -} - -/** - * Registers an initialization reducer that runs when the SpacetimeDB module is published - * for the first time. - * This function is useful to set up any initial state of your database that is guaranteed - * to run only once, and before any other reducers or client connections. - * @template S - The inferred schema type of the SpacetimeDB module. - * @template Params - The type of the parameters object expected by the initialization reducer. - * - * @param params - The parameters object defining the expected input for the initialization reducer. - * @param fn - The initialization reducer function. - * - `ctx`: The reducer context, providing access to `sender`, `timestamp`, `connection_id`, and `db`. - */ -export function init( - name: string, - params: Params, - fn: Reducer -): void { - pushReducer(name, params, fn, Lifecycle.Init); -} - -/** - * Registers a reducer to be called when a client connects to the SpacetimeDB module. - * This function allows you to define custom logic that should execute - * whenever a new client establishes a connection. - * @template S - The inferred schema type of the SpacetimeDB module. - * @template Params - The type of the parameters object expected by the connection reducer. - * @param params - The parameters object defining the expected input for the connection reducer. - * @param fn - The connection reducer function itself. - */ -export function clientConnected< - S extends UntypedSchemaDef, - Params extends ParamsObj, ->(name: string, params: Params, fn: Reducer): void { - pushReducer(name, params, fn, Lifecycle.OnConnect); -} - -/** - * Registers a reducer to be called when a client disconnects from the SpacetimeDB module. - * This function allows you to define custom logic that should execute - * whenever a client disconnects. - * - * @template S - The inferred schema type of the SpacetimeDB module. - * @template Params - The type of the parameters object expected by the disconnection reducer. - * @param params - The parameters object defining the expected input for the disconnection reducer. - * @param fn - The disconnection reducer function itself. - * @example - * ```typescript - * spacetime.clientDisconnected( - * { reason: t.string() }, - * (ctx, { reason }) => { - * console.log(`Client ${ctx.connection_id} disconnected: ${reason}`); - * } - * ); - * ``` - */ -export function clientDisconnected< - S extends UntypedSchemaDef, - Params extends ParamsObj, ->(name: string, params: Params, fn: Reducer): void { - pushReducer(name, params, fn, Lifecycle.OnDisconnect); -} - -class Reducers { - reducersType: ReducersDef; - - constructor(handles: readonly ReducerSchema[]) { - this.reducersType = reducersToSchema(handles) as ReducersDef; - } -} - -/** - * Helper type to convert an array of TableSchema into a schema definition - */ -type ReducersToSchema[]> = { - reducers: { - /** @type {UntypedReducerDef} */ - readonly [i in keyof T]: { - name: T[i]['reducerName']; - accessorName: CamelCase; - params: T[i]['params']['row']; - paramsType: T[i]['paramsSpacetimeType']; - }; - }; -}; - -export function reducersToSchema< - const T extends readonly ReducerSchema[], ->(reducers: T): ReducersToSchema { - const mapped = reducers.map(r => { - const paramsRow = r.params.row; - - return { - name: r.reducerName, - // Prefer the schema's own accessorName if present at runtime; otherwise derive it. - accessorName: r.accessorName, - params: paramsRow, - paramsType: r.paramsSpacetimeType, - } as const; - }) as { - readonly [I in keyof T]: { - name: T[I]['reducerName']; - accessorName: T[I]['accessorName']; - params: T[I]['params']['row']; - paramsType: T[I]['paramsSpacetimeType']; - }; - }; - - const result = { reducers: mapped } satisfies ReducersToSchema; - return result; -} - -/** - * Creates a schema from table definitions - * @param handles - Array of table handles created by table() function - * @returns ColumnBuilder representing the complete database schema - * @example - * ```ts - * const s = schema( - * table({ name: 'user' }, userType), - * table({ name: 'post' }, postType) - * ); - * ``` - */ -export function reducers[]>( - ...handles: H -): Reducers>; - -/** - * Creates a schema from table definitions (array overload) - * @param handles - Array of table handles created by table() function - * @returns ColumnBuilder representing the complete database schema - */ -export function reducers[]>( - handles: H -): Reducers>; - -export function reducers[]>( - ...args: [H] | H -): Reducers> { - const handles = ( - args.length === 1 && Array.isArray(args[0]) ? args[0] : args - ) as H; - return new Reducers(handles); -} - -export function reducerSchema< - ReducerName extends string, - Params extends ParamsObj, ->(name: ReducerName, params: Params): ReducerSchema { - const paramType: ProductType = { - elements: Object.entries(params).map(([n, c]) => ({ - name: n, - algebraicType: - 'typeBuilder' in c ? c.typeBuilder.algebraicType : c.algebraicType, - })), - }; - return { - reducerName: name, - accessorName: toCamelCase(name), - params: new RowBuilder(params), - paramsSpacetimeType: paramType, - reducerDef: { - name, - params: paramType, - lifecycle: undefined, - }, - }; -} diff --git a/crates/bindings-typescript/src/lib/schema.ts b/crates/bindings-typescript/src/lib/schema.ts index f22727d1848..95b98e97a2a 100644 --- a/crates/bindings-typescript/src/lib/schema.ts +++ b/crates/bindings-typescript/src/lib/schema.ts @@ -1,9 +1,17 @@ -import type RawTableDefV9 from './autogen/raw_table_def_v_9_type'; -import type Typespace from './autogen/typespace_type'; +import { + AlgebraicType, + ProductType, + SumType, + type AlgebraicTypeType, + type AlgebraicTypeVariants, +} from './algebraic_type'; +import type RawModuleDefV9 from './autogen/raw_module_def_v_9_type'; +import type RawScopedTypeNameV9 from './autogen/raw_scoped_type_name_v_9_type'; +import type { UntypedIndex } from './indexes'; +import type { UntypedTableDef } from './table'; +import type { UntypedTableSchema } from './table_schema'; import { ArrayBuilder, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ColumnBuilder, OptionBuilder, ProductBuilder, RefBuilder, @@ -17,36 +25,8 @@ import { type RowObj, type VariantsObj, } from './type_builders'; -import type { UntypedTableDef } from './table'; -import { - clientConnected, - clientDisconnected, - init, - reducer, - type ParamsObj, - type Reducer, -} from './reducers'; -import type RawModuleDefV9 from './autogen/raw_module_def_v_9_type'; -import { - AlgebraicType, - ProductType, - SumType, - type AlgebraicTypeType, - type AlgebraicTypeVariants, -} from './algebraic_type'; -import type RawScopedTypeNameV9 from './autogen/raw_scoped_type_name_v_9_type'; import type { CamelCase } from './type_util'; -import type { UntypedTableSchema } from './table_schema'; import { toCamelCase } from './util'; -import { - defineView, - type AnonymousViewFn, - type ViewFn, - type ViewOpts, - type ViewReturnTypeBuilder, -} from './views'; -import type { UntypedIndex } from './indexes'; -import { procedure, type ProcedureFn } from './procedures'; export type TableNamesOf = S['tables'][number]['name']; @@ -58,25 +38,18 @@ export type UntypedSchemaDef = { tables: readonly UntypedTableDef[]; }; -let REGISTERED_SCHEMA: UntypedSchemaDef | null = null; - -export function getRegisteredSchema(): UntypedSchemaDef { - if (REGISTERED_SCHEMA == null) { - throw new Error('Schema has not been registered yet. Call schema() first.'); - } - return REGISTERED_SCHEMA; -} - /** * Helper type to convert an array of TableSchema into a schema definition */ -type TablesToSchema = { +export interface TablesToSchema + extends UntypedSchemaDef { tables: { readonly [i in keyof T]: TableToSchema; }; -}; +} -interface TableToSchema extends UntypedTableDef { +export interface TableToSchema + extends UntypedTableDef { name: T['tableName']; accessorName: CamelCase; columns: T['rowType']['row']; @@ -86,16 +59,23 @@ interface TableToSchema extends UntypedTableDef { } export function tablesToSchema( + ctx: ModuleContext, tables: T ): TablesToSchema { - return { tables: tables.map(tableToSchema) as TablesToSchema['tables'] }; + return { + tables: tables.map(schema => + tableToSchema(ctx, schema) + ) as TablesToSchema['tables'], + }; } function tableToSchema( + ctx: ModuleContext, schema: T ): TableToSchema { const getColName = (i: number) => schema.rowType.algebraicType.value.elements[i].name; + const tableDef = schema.tableDef(ctx); type AllowedCol = keyof T['rowType']['row'] & string; return { @@ -103,7 +83,7 @@ function tableToSchema( accessorName: toCamelCase(schema.tableName as T['tableName']), columns: schema.rowType.row, // typed as T[i]['rowType']['row'] under TablesToSchema rowType: schema.rowSpacetimeType, - constraints: schema.tableDef.constraints.map(c => ({ + constraints: tableDef.constraints.map(c => ({ name: c.name, constraint: 'unique', columns: c.data.value.columns.map(getColName) as [string], @@ -112,14 +92,14 @@ function tableToSchema( // by casting it to an `Array` as `TableToSchema` expects. // This is then used in `TableCacheImpl.constructor` and who knows where else. // We should stop lying about our types. - indexes: schema.tableDef.indexes.map((idx): UntypedIndex => { + indexes: tableDef.indexes.map((idx): UntypedIndex => { const columnIds = idx.algorithm.tag === 'Direct' ? [idx.algorithm.value] : idx.algorithm.value; return { name: idx.accessorName!, - unique: schema.tableDef.constraints.some(c => + unique: tableDef.constraints.some(c => c.data.value.columns.every(col => columnIds.includes(col)) ), algorithm: idx.algorithm.tag.toLowerCase() as 'btree', @@ -129,140 +109,154 @@ function tableToSchema( }; } -/** - * The global module definition that gets populated by calls to `reducer()` and lifecycle hooks. - */ -export const MODULE_DEF: Infer = { - typespace: { types: [] }, - tables: [], - reducers: [], - types: [], - miscExports: [], - rowLevelSecurity: [], -}; - -const COMPOUND_TYPES = new Map< +type CompoundTypeCache = Map< AlgebraicTypeVariants.Product | AlgebraicTypeVariants.Sum, RefBuilder ->(); +>; -/** - * Resolves the actual type of a TypeBuilder by following its references until it reaches a non-ref type. - * @param typespace The typespace to resolve types against. - * @param typeBuilder The TypeBuilder to resolve. - * @returns The resolved algebraic type. - */ -export function resolveType( - typespace: Infer, - typeBuilder: RefBuilder -): AT { - let ty: AlgebraicType = typeBuilder.algebraicType; - while (ty.tag === 'Ref') { - ty = typespace.types[ty.value]; - } - return ty as AT; -} +type ModuleDef = Infer; -/** - * Adds a type to the module definition's typespace as a `Ref` if it is a named compound type (Product or Sum). - * Otherwise, returns the type as is. - * @param name - * @param ty - * @returns - */ -export function registerTypesRecursively< - T extends TypeBuilder, ->( - typeBuilder: T -): T extends SumBuilder | ProductBuilder | RowBuilder - ? RefBuilder, InferSpacetimeTypeOfTypeBuilder> - : T { - if ( - (typeBuilder instanceof ProductBuilder && !isUnit(typeBuilder)) || - typeBuilder instanceof SumBuilder || - typeBuilder instanceof RowBuilder - ) { - return registerCompoundTypeRecursively(typeBuilder) as any; - } else if (typeBuilder instanceof OptionBuilder) { - return new OptionBuilder( - registerTypesRecursively(typeBuilder.value) - ) as any; - } else if (typeBuilder instanceof ArrayBuilder) { - return new ArrayBuilder( - registerTypesRecursively(typeBuilder.element) - ) as any; - } else { - return typeBuilder as any; - } -} +export class ModuleContext { + #compoundTypes: CompoundTypeCache = new Map(); + /** + * The global module definition that gets populated by calls to `reducer()` and lifecycle hooks. + */ + #moduleDef: ModuleDef = { + typespace: { types: [] }, + tables: [], + reducers: [], + types: [], + miscExports: [], + rowLevelSecurity: [], + }; -function registerCompoundTypeRecursively< - T extends - | SumBuilder - | ProductBuilder - | RowBuilder, ->(typeBuilder: T): RefBuilder, InferSpacetimeTypeOfTypeBuilder> { - const ty = typeBuilder.algebraicType; - // NB! You must ensure that all TypeBuilder passed into this function - // have a name. This function ensures that nested types always have a - // name by assigning them one if they are missing it. - const name = typeBuilder.typeName; - if (name === undefined) { - throw new Error( - `Missing type name for ${typeBuilder.constructor.name ?? 'TypeBuilder'} ${JSON.stringify(typeBuilder)}` - ); + get moduleDef() { + return this.#moduleDef; } - let r = COMPOUND_TYPES.get(ty); - if (r != null) { - // Already added to typespace - return r; + get typespace() { + return this.#moduleDef.typespace; } - // Recursively register nested compound types - const newTy = - typeBuilder instanceof RowBuilder || typeBuilder instanceof ProductBuilder - ? ({ - tag: 'Product', - value: { elements: [] }, - } as AlgebraicTypeVariants.Product) - : ({ tag: 'Sum', value: { variants: [] } } as AlgebraicTypeVariants.Sum); - - r = new RefBuilder(MODULE_DEF.typespace.types.length); - MODULE_DEF.typespace.types.push(newTy); + /** + * Resolves the actual type of a TypeBuilder by following its references until it reaches a non-ref type. + * @param typespace The typespace to resolve types against. + * @param typeBuilder The TypeBuilder to resolve. + * @returns The resolved algebraic type. + */ + public resolveType( + typeBuilder: RefBuilder + ): AT { + let ty: AlgebraicType = typeBuilder.algebraicType; + while (ty.tag === 'Ref') { + ty = this.typespace.types[ty.value]; + } + return ty as AT; + } - COMPOUND_TYPES.set(ty, r); + /** + * Adds a type to the module definition's typespace as a `Ref` if it is a named compound type (Product or Sum). + * Otherwise, returns the type as is. + * @param name + * @param ty + * @returns + */ + public registerTypesRecursively>( + typeBuilder: T + ): T extends SumBuilder | ProductBuilder | RowBuilder + ? RefBuilder, InferSpacetimeTypeOfTypeBuilder> + : T { + if ( + (typeBuilder instanceof ProductBuilder && !isUnit(typeBuilder)) || + typeBuilder instanceof SumBuilder || + typeBuilder instanceof RowBuilder + ) { + return this.#registerCompoundTypeRecursively(typeBuilder) as any; + } else if (typeBuilder instanceof OptionBuilder) { + return new OptionBuilder( + this.registerTypesRecursively(typeBuilder.value) + ) as any; + } else if (typeBuilder instanceof ArrayBuilder) { + return new ArrayBuilder( + this.registerTypesRecursively(typeBuilder.element) + ) as any; + } else { + return typeBuilder as any; + } + } - if (typeBuilder instanceof RowBuilder) { - for (const [name, elem] of Object.entries(typeBuilder.row)) { - (newTy.value as ProductType).elements.push({ - name, - algebraicType: registerTypesRecursively(elem.typeBuilder).algebraicType, - }); + #registerCompoundTypeRecursively< + T extends + | SumBuilder + | ProductBuilder + | RowBuilder, + >(typeBuilder: T): RefBuilder, InferSpacetimeTypeOfTypeBuilder> { + const ty = typeBuilder.algebraicType; + // NB! You must ensure that all TypeBuilder passed into this function + // have a name. This function ensures that nested types always have a + // name by assigning them one if they are missing it. + const name = typeBuilder.typeName; + if (name === undefined) { + throw new Error( + `Missing type name for ${typeBuilder.constructor.name ?? 'TypeBuilder'} ${JSON.stringify(typeBuilder)}` + ); } - } else if (typeBuilder instanceof ProductBuilder) { - for (const [name, elem] of Object.entries(typeBuilder.elements)) { - (newTy.value as ProductType).elements.push({ - name, - algebraicType: registerTypesRecursively(elem).algebraicType, - }); + + let r = this.#compoundTypes.get(ty); + if (r != null) { + // Already added to typespace + return r; } - } else if (typeBuilder instanceof SumBuilder) { - for (const [name, variant] of Object.entries(typeBuilder.variants)) { - (newTy.value as SumType).variants.push({ - name, - algebraicType: registerTypesRecursively(variant).algebraicType, - }); + + // Recursively register nested compound types + const newTy = + typeBuilder instanceof RowBuilder || typeBuilder instanceof ProductBuilder + ? ({ + tag: 'Product', + value: { elements: [] }, + } as AlgebraicTypeVariants.Product) + : ({ + tag: 'Sum', + value: { variants: [] }, + } as AlgebraicTypeVariants.Sum); + + r = new RefBuilder(this.#moduleDef.typespace.types.length); + this.#moduleDef.typespace.types.push(newTy); + + this.#compoundTypes.set(ty, r); + + if (typeBuilder instanceof RowBuilder) { + for (const [name, elem] of Object.entries(typeBuilder.row)) { + (newTy.value as ProductType).elements.push({ + name, + algebraicType: this.registerTypesRecursively(elem.typeBuilder) + .algebraicType, + }); + } + } else if (typeBuilder instanceof ProductBuilder) { + for (const [name, elem] of Object.entries(typeBuilder.elements)) { + (newTy.value as ProductType).elements.push({ + name, + algebraicType: this.registerTypesRecursively(elem).algebraicType, + }); + } + } else if (typeBuilder instanceof SumBuilder) { + for (const [name, variant] of Object.entries(typeBuilder.variants)) { + (newTy.value as SumType).variants.push({ + name, + algebraicType: this.registerTypesRecursively(variant).algebraicType, + }); + } } - } - MODULE_DEF.types.push({ - name: splitName(name), - ty: r.ref, - customOrdering: true, - }); + this.#moduleDef.types.push({ + name: splitName(name), + ty: r.ref, + customOrdering: true, + }); - return r; + return r; + } } function isUnit(typeBuilder: ProductBuilder): boolean { @@ -276,383 +270,3 @@ export function splitName(name: string): Infer { const scope = name.split('.'); return { name: scope.pop()!, scope }; } - -/** - * The Schema class represents the database schema for a SpacetimeDB application. - * It encapsulates the table definitions and typespace, and provides methods to define - * reducers and lifecycle hooks. - * - * Schema has a generic parameter S which represents the inferred schema type. This type - * is automatically inferred when creating a schema using the `schema()` function and is - * used to type the database view in reducer contexts. - * - * The methods on this class are used to register reducers and lifecycle hooks - * with the SpacetimeDB runtime. Theey forward to free functions that handle the actual - * registration logic, but having them as methods on the Schema class helps with type inference. - * - * @template S - The inferred schema type of the SpacetimeDB module. - * - * @example - * ```typescript - * const spacetime = schema( - * table({ name: 'user' }, userType), - * table({ name: 'post' }, postType) - * ); - * spacetime.reducer( - * 'create_user', - * { username: t.string(), email: t.string() }, - * (ctx, { username, email }) => { - * ctx.db.user.insert({ username, email, created_at: ctx.timestamp }); - * console.log(`User ${username} created by ${ctx.sender.identityId}`); - * } - * ); - * ``` - */ -// TODO(cloutiertyler): It might be nice to have a way to access the types -// for the tables from the schema object, e.g. `spacetimedb.user.type` would -// be the type of the user table. -class Schema { - readonly tablesDef: { tables: Infer[] }; - readonly typespace: Infer; - readonly schemaType: S; - - constructor( - tables: Infer[], - typespace: Infer, - handles: readonly UntypedTableSchema[] - ) { - this.tablesDef = { tables }; - this.typespace = typespace; - // TODO: TableSchema and TableDef should really be unified - this.schemaType = tablesToSchema(handles) as S; - } - - /** - * Defines a SpacetimeDB reducer function. - * - * Reducers are the primary way to modify the state of your SpacetimeDB application. - * They are atomic, meaning that either all operations within a reducer succeed, - * or none of them do. - * - * @template S - The inferred schema type of the SpacetimeDB module. - * @template Params - The type of the parameters object expected by the reducer. - * - * @param {string} name - The name of the reducer. This name will be used to call the reducer from clients. - * @param {Params} params - An object defining the parameters that the reducer accepts. - * Each key-value pair represents a parameter name and its corresponding - * {@link TypeBuilder} or {@link ColumnBuilder}. - * @param {(ctx: ReducerCtx, payload: ParamsAsObject) => void} fn - The reducer function itself. - * - `ctx`: The reducer context, providing access to `sender`, `timestamp`, `connection_id`, and `db`. - * - `payload`: An object containing the arguments passed to the reducer, typed according to `params`. - * - * @example - * ```typescript - * // Define a reducer named 'create_user' that takes 'username' (string) and 'email' (string) - * spacetime.reducer( - * 'create_user', - * { - * username: t.string(), - * email: t.string(), - * }, - * (ctx, { username, email }) => { - * // Access the 'user' table from the database view in the context - * ctx.db.user.insert({ username, email, created_at: ctx.timestamp }); - * console.log(`User ${username} created by ${ctx.sender.identityId}`); - * } - * ); - * ``` - */ - reducer( - name: string, - params: Params, - fn: Reducer - ): Reducer; - reducer(name: string, fn: Reducer): Reducer; - reducer( - name: string, - paramsOrFn: Params | Reducer, - fn?: Reducer - ): Reducer { - if (typeof paramsOrFn === 'function') { - // This is the case where params are omitted. - // The second argument is the reducer function. - // We pass an empty object for the params. - reducer(name, {}, paramsOrFn); - return paramsOrFn; - } else { - // This is the case where params are provided. - // The second argument is the params object, and the third is the function. - // The `fn` parameter is guaranteed to be defined here. - reducer(name, paramsOrFn, fn!); - return fn!; - } - } - - /** - * Registers an initialization reducer that runs when the SpacetimeDB module is published - * for the first time. - * - * This function is useful to set up any initial state of your database that is guaranteed - * to run only once, and before any other reducers or client connections. - * - * @template S - The inferred schema type of the SpacetimeDB module. - * @param {Reducer} fn - The initialization reducer function. - * - `ctx`: The reducer context, providing access to `sender`, `timestamp`, `connection_id`, and `db`. - * @example - * ```typescript - * spacetime.init((ctx) => { - * ctx.db.user.insert({ username: 'admin', email: 'admin@example.com' }); - * }); - * ``` - */ - init(fn: Reducer): void; - init(name: string, fn: Reducer): void; - init(nameOrFn: any, maybeFn?: Reducer): void { - const [name, fn] = - typeof nameOrFn === 'string' ? [nameOrFn, maybeFn] : ['init', nameOrFn]; - init(name, {}, fn); - } - - /** - * Registers a reducer to be called when a client connects to the SpacetimeDB module. - * This function allows you to define custom logic that should execute - * whenever a new client establishes a connection. - * @template S - The inferred schema type of the SpacetimeDB module. - * - * @param fn - The reducer function to execute on client connection. - * - * @example - * ```typescript - * spacetime.clientConnected( - * (ctx) => { - * console.log(`Client ${ctx.connectionId} connected`); - * } - * ); - */ - clientConnected(fn: Reducer): void; - clientConnected(name: string, fn: Reducer): void; - clientConnected(nameOrFn: any, maybeFn?: Reducer): void { - const [name, fn] = - typeof nameOrFn === 'string' - ? [nameOrFn, maybeFn] - : ['on_connect', nameOrFn]; - clientConnected(name, {}, fn); - } - - /** - * Registers a reducer to be called when a client disconnects from the SpacetimeDB module. - * This function allows you to define custom logic that should execute - * whenever a client disconnects. - * @template S - The inferred schema type of the SpacetimeDB module. - * - * @param fn - The reducer function to execute on client disconnection. - * - * @example - * ```typescript - * spacetime.clientDisconnected( - * (ctx) => { - * console.log(`Client ${ctx.connectionId} disconnected`); - * } - * ); - * ``` - */ - clientDisconnected(fn: Reducer): void; - clientDisconnected(name: string, fn: Reducer): void; - clientDisconnected(nameOrFn: any, maybeFn?: Reducer): void { - const [name, fn] = - typeof nameOrFn === 'string' - ? [nameOrFn, maybeFn] - : ['on_disconnect', nameOrFn]; - clientDisconnected(name, {}, fn); - } - - view( - opts: ViewOpts, - ret: Ret, - fn: ViewFn - ): void { - defineView(opts, false, {}, ret, fn); - } - - // TODO: re-enable once parameterized views are supported in SQL - // view( - // opts: ViewOpts, - // ret: Ret, - // fn: ViewFn - // ): void; - // view( - // opts: ViewOpts, - // params: Params, - // ret: Ret, - // fn: ViewFn - // ): void; - // view( - // opts: ViewOpts, - // paramsOrRet: Ret | Params, - // retOrFn: ViewFn | Ret, - // maybeFn?: ViewFn - // ): void { - // if (typeof retOrFn === 'function') { - // defineView(name, false, {}, paramsOrRet as Ret, retOrFn); - // } else { - // defineView(name, false, paramsOrRet as Params, retOrFn, maybeFn!); - // } - // } - - anonymousView( - opts: ViewOpts, - ret: Ret, - fn: AnonymousViewFn - ): void { - defineView(opts, true, {}, ret, fn); - } - - // TODO: re-enable once parameterized views are supported in SQL - // anonymousView( - // opts: ViewOpts, - // ret: Ret, - // fn: AnonymousViewFn - // ): void; - // anonymousView( - // opts: ViewOpts, - // params: Params, - // ret: Ret, - // fn: AnonymousViewFn - // ): void; - // anonymousView( - // opts: ViewOpts, - // paramsOrRet: Ret | Params, - // retOrFn: AnonymousViewFn | Ret, - // maybeFn?: AnonymousViewFn - // ): void { - // if (typeof retOrFn === 'function') { - // defineView(name, true, {}, paramsOrRet as Ret, retOrFn); - // } else { - // defineView(name, true, paramsOrRet as Params, retOrFn, maybeFn!); - // } - // } - - procedure>( - name: string, - params: Params, - ret: Ret, - fn: ProcedureFn - ): ProcedureFn; - procedure>( - name: string, - ret: Ret, - fn: ProcedureFn - ): ProcedureFn; - procedure>( - name: string, - paramsOrRet: Ret | Params, - retOrFn: ProcedureFn | Ret, - maybeFn?: ProcedureFn - ): ProcedureFn { - if (typeof retOrFn === 'function') { - procedure(name, {}, paramsOrRet as Ret, retOrFn); - return retOrFn; - } else { - procedure(name, paramsOrRet as Params, retOrFn, maybeFn!); - return maybeFn!; - } - } - - clientVisibilityFilter = { - sql(filter: string): void { - MODULE_DEF.rowLevelSecurity.push({ sql: filter }); - }, - }; -} - -/** - * Extracts the inferred schema type from a Schema instance - */ -export type InferSchema> = - SchemaDef extends Schema ? S : never; - -/** - * Creates a schema from table definitions - * @param handles - Array of table handles created by table() function - * @returns ColumnBuilder representing the complete database schema - * @example - * ```ts - * const s = schema( - * table({ name: 'user' }, userType), - * table({ name: 'post' }, postType) - * ); - * ``` - */ -export function schema( - ...handles: H -): Schema>; - -/** - * Creates a schema from table definitions (array overload) - * @param handles - Array of table handles created by table() function - * @returns ColumnBuilder representing the complete database schema - */ -export function schema( - handles: H -): Schema>; - -/** - * Creates a schema from table definitions - * @param args - Either an array of table handles or a variadic list of table handles - * @returns ColumnBuilder representing the complete database schema - * @example - * ```ts - * const s = schema( - * table({ name: 'user' }, userType), - * table({ name: 'post' }, postType) - * ); - * ``` - */ -export function schema( - ...args: [H] | H -): Schema> { - const handles = ( - args.length === 1 && Array.isArray(args[0]) ? args[0] : args - ) as H; - const tableDefs = handles.map(h => h.tableDef); - - // Side-effect: - // Modify the `MODULE_DEF` which will be read by - // __describe_module__ - MODULE_DEF.tables.push(...tableDefs); - REGISTERED_SCHEMA = { - tables: handles.map(handle => ({ - name: handle.tableName, - accessorName: handle.tableName, - columns: handle.rowType.row, - rowType: handle.rowSpacetimeType, - indexes: handle.idxs, - constraints: handle.constraints, - })), - }; - // MODULE_DEF.typespace = typespace; - // throw new Error( - // MODULE_DEF.tables - // .map(t => { - // const p = MODULE_DEF.typespace.types[t.productTypeRef]; - // return `${t.name}: ${t.productTypeRef} ${p && (p as AlgebraicTypeVariants.Product).value.elements.map(x => x.name)}`; - // }) - // .join('\n') - // ); - - return new Schema(tableDefs, MODULE_DEF.typespace, handles); -} - -type HasAccessor = { accessorName: PropertyKey }; - -export type ConvertToAccessorMap = { - [Tbl in TableDefs[number] as Tbl['accessorName']]: Tbl; -}; - -export function convertToAccessorMap( - arr: T -): ConvertToAccessorMap { - return Object.fromEntries( - arr.map(v => [v.accessorName, v]) - ) as ConvertToAccessorMap; -} diff --git a/crates/bindings-typescript/src/lib/table.ts b/crates/bindings-typescript/src/lib/table.ts index 167c8fedd75..88c12fcd9b1 100644 --- a/crates/bindings-typescript/src/lib/table.ts +++ b/crates/bindings-typescript/src/lib/table.ts @@ -1,3 +1,4 @@ +import type { errors } from '../server/errors'; import type RawConstraintDefV9 from './autogen/raw_constraint_def_v_9_type'; import RawIndexAlgorithm from './autogen/raw_index_algorithm_type'; import type RawIndexDefV9 from './autogen/raw_index_def_v_9_type'; @@ -12,7 +13,7 @@ import type { ReadonlyIndexes, } from './indexes'; import ScheduleAt from './schedule_at'; -import { registerTypesRecursively } from './schema'; +import type { ModuleContext } from './schema'; import type { TableSchema } from './table_schema'; import { RowBuilder, @@ -179,8 +180,8 @@ export interface TableMethods * Insert and return the inserted row (auto-increment fields filled). * * May throw on error: - * * If there are any unique or primary key columns in this table, may throw {@link UniqueAlreadyExists}. - * * If there are any auto-incrementing columns in this table, may throw {@link AutoIncOverflow}. + * * If there are any unique or primary key columns in this table, may throw {@link errors.UniqueAlreadyExists}. + * * If there are any auto-incrementing columns in this table, may throw {@link errors.AutoIncOverflow}. * */ insert(row: Prettify>): Prettify>; @@ -227,8 +228,6 @@ export function table>( row.typeName = toPascalCase(name); } - const rowTypeRef = registerTypesRecursively(row); - row.algebraicType.value.elements.forEach((elem, i) => { colIds.set(elem.name, i); colNameList.push(elem.name); @@ -346,9 +345,9 @@ export function table>( // Temporarily set the type ref to 0. We will set this later // in the schema function. - const tableDef: Infer = { + const tableDef = (ctx: ModuleContext): Infer => ({ name, - productTypeRef: rowTypeRef.ref, + productTypeRef: ctx.registerTypesRecursively(row).ref, primaryKey: pk, indexes, constraints, @@ -363,7 +362,7 @@ export function table>( : undefined, tableType: { tag: 'User' }, tableAccess: { tag: isPublic ? 'Public' : 'Private' }, - }; + }); const productType = row.algebraicType.value as RowBuilder< CoerceRow diff --git a/crates/bindings-typescript/src/lib/table_schema.ts b/crates/bindings-typescript/src/lib/table_schema.ts index f15d9ccc0d5..b48b33ca444 100644 --- a/crates/bindings-typescript/src/lib/table_schema.ts +++ b/crates/bindings-typescript/src/lib/table_schema.ts @@ -1,5 +1,7 @@ +import type { ProductType } from './algebraic_type'; import type RawTableDefV9 from './autogen/raw_table_def_v_9_type'; import type { IndexOpts } from './indexes'; +import type { ModuleContext } from './schema'; import type { ColumnBuilder, Infer, RowBuilder } from './type_builders'; /** @@ -28,7 +30,7 @@ export type TableSchema< /** * The {@link RawTableDefV9} of the configured table */ - readonly tableDef: Infer; + tableDef(ctx: ModuleContext): Infer; /** * The indexes defined on the table. diff --git a/crates/bindings-typescript/src/lib/type_builders.ts b/crates/bindings-typescript/src/lib/type_builders.ts index 629519b73f2..0bced1517aa 100644 --- a/crates/bindings-typescript/src/lib/type_builders.ts +++ b/crates/bindings-typescript/src/lib/type_builders.ts @@ -3632,7 +3632,7 @@ export const t = { enum: enumImpl, /** - * This is a special helper function for conveniently creating {@link Product} type columns with no fields. + * This is a special helper function for conveniently creating `Product` type columns with no fields. * * @returns A new {@link ProductBuilder} instance with no fields. */ @@ -3737,10 +3737,10 @@ export const t = { }, /** - * This is a convenience method for creating a column with the {@link ByteArray} type. + * This is a convenience method for creating a column with the `ByteArray` type. * You can create a column of the same type by constructing an `array` of `u8`. * The TypeScript representation is {@link Uint8Array}. - * @returns A new {@link ByteArrayBuilder} instance with the {@link ByteArray} type. + * @returns A new {@link ByteArrayBuilder} instance with the `ByteArray` type. */ byteArray: (): ByteArrayBuilder => { return new ByteArrayBuilder(); diff --git a/crates/bindings-typescript/src/sdk/db_connection_builder.ts b/crates/bindings-typescript/src/sdk/db_connection_builder.ts index efc32059ae9..b3906575a6d 100644 --- a/crates/bindings-typescript/src/sdk/db_connection_builder.ts +++ b/crates/bindings-typescript/src/sdk/db_connection_builder.ts @@ -198,7 +198,7 @@ export class DbConnectionBuilder> { /** * Registers a callback to run when a {@link DbConnection} whose connection initially succeeded - * is disconnected, either after a {@link DbConnection.disconnect} call or due to an error. + * is disconnected, either after a {@link DbConnection.disconnect()} call or due to an error. * * If the connection ended because of an error, the error is passed to the callback. * diff --git a/crates/bindings-typescript/src/sdk/index.ts b/crates/bindings-typescript/src/sdk/index.ts index af3cb33668c..c87ee195a91 100644 --- a/crates/bindings-typescript/src/sdk/index.ts +++ b/crates/bindings-typescript/src/sdk/index.ts @@ -6,7 +6,7 @@ export { type ClientTable } from './client_table.ts'; export { type RemoteModule } from './spacetime_module.ts'; export { type SetReducerFlags } from './reducers.ts'; export * from '../lib/type_builders.ts'; -export { schema, convertToAccessorMap } from '../lib/schema.ts'; +export { schema, convertToAccessorMap } from './schema.ts'; export { table } from '../lib/table.ts'; -export { reducerSchema, reducers } from '../lib/reducers.ts'; -export { procedureSchema, procedures } from '../lib/procedures.ts'; +export { reducerSchema, reducers } from './reducers.ts'; +export { procedureSchema, procedures } from './procedures.ts'; diff --git a/crates/bindings-typescript/src/sdk/procedures.ts b/crates/bindings-typescript/src/sdk/procedures.ts index 9407cf7cc63..cc5759cdfc9 100644 --- a/crates/bindings-typescript/src/sdk/procedures.ts +++ b/crates/bindings-typescript/src/sdk/procedures.ts @@ -1,5 +1,7 @@ -import type { Infer, InferTypeOfRow } from '../lib/type_builders'; +import type { ParamsObj } from '../lib/reducers'; +import type { Infer, InferTypeOfRow, TypeBuilder } from '../lib/type_builders'; import type { CamelCase } from '../lib/type_util'; +import { coerceParams, toCamelCase, type CoerceParams } from '../lib/util'; import type { UntypedRemoteModule } from './spacetime_module'; // Utility: detect 'any' @@ -25,3 +27,59 @@ export type ProceduresView = IfAny< } : never >; + +export type UntypedProcedureDef = { + name: string; + accessorName: string; + params: CoerceParams; + returnType: TypeBuilder; +}; + +export type UntypedProceduresDef = { + procedures: readonly UntypedProcedureDef[]; +}; + +export function procedures( + ...handles: H +): { procedures: H }; + +export function procedures( + handles: H +): { procedures: H }; + +export function procedures( + ...args: [H] | H +): { procedures: H } { + const procedures = ( + args.length === 1 && Array.isArray(args[0]) ? args[0] : args + ) as H; + return { procedures }; +} + +type ProcedureDef< + Name extends string, + Params extends ParamsObj, + ReturnType extends TypeBuilder, +> = { + name: Name; + accessorName: CamelCase; + params: CoerceParams; + returnType: ReturnType; +}; + +export function procedureSchema< + ProcedureName extends string, + Params extends ParamsObj, + ReturnType extends TypeBuilder, +>( + name: ProcedureName, + params: Params, + returnType: ReturnType +): ProcedureDef { + return { + name, + accessorName: toCamelCase(name), + params: coerceParams(params), + returnType, + }; +} diff --git a/crates/bindings-typescript/src/sdk/reducers.ts b/crates/bindings-typescript/src/sdk/reducers.ts index 03cce971d0e..794837b03c9 100644 --- a/crates/bindings-typescript/src/sdk/reducers.ts +++ b/crates/bindings-typescript/src/sdk/reducers.ts @@ -1,14 +1,16 @@ import type { ProductType } from '../lib/algebraic_type'; +import type { ReducerSchema } from '../lib/reducer_schema'; import type { ParamsObj } from '../lib/reducers'; import type { CoerceRow } from '../lib/table'; -import type { InferTypeOfRow } from '../lib/type_builders'; +import { RowBuilder, type InferTypeOfRow } from '../lib/type_builders'; import type { CamelCase, PascalCase } from '../lib/type_util'; +import { toCamelCase } from '../lib/util'; import type { CallReducerFlags } from './db_connection_impl'; -import type { UntypedRemoteModule } from './spacetime_module'; import type { ReducerEventContextInterface, SubscriptionEventContextInterface, } from './event_context'; +import type { UntypedRemoteModule } from './spacetime_module'; export type ReducerEventCallback< RemoteModule extends UntypedRemoteModule, @@ -90,3 +92,110 @@ export type SetReducerFlags = { flags: CallReducerFlags ) => void; }; + +class Reducers { + reducersType: ReducersDef; + + constructor(handles: readonly ReducerSchema[]) { + this.reducersType = reducersToSchema(handles) as ReducersDef; + } +} + +/** + * Helper type to convert an array of TableSchema into a schema definition + */ +type ReducersToSchema[]> = { + reducers: { + /** @type {UntypedReducerDef} */ + readonly [i in keyof T]: { + name: T[i]['reducerName']; + accessorName: CamelCase; + params: T[i]['params']['row']; + paramsType: T[i]['paramsSpacetimeType']; + }; + }; +}; + +export function reducersToSchema< + const T extends readonly ReducerSchema[], +>(reducers: T): ReducersToSchema { + const mapped = reducers.map(r => { + const paramsRow = r.params.row; + + return { + name: r.reducerName, + // Prefer the schema's own accessorName if present at runtime; otherwise derive it. + accessorName: r.accessorName, + params: paramsRow, + paramsType: r.paramsSpacetimeType, + } as const; + }) as { + readonly [I in keyof T]: { + name: T[I]['reducerName']; + accessorName: T[I]['accessorName']; + params: T[I]['params']['row']; + paramsType: T[I]['paramsSpacetimeType']; + }; + }; + + const result = { reducers: mapped } satisfies ReducersToSchema; + return result; +} + +/** + * Creates a schema from table definitions + * @param handles - Array of table handles created by table() function + * @returns ColumnBuilder representing the complete database schema + * @example + * ```ts + * const s = schema( + * table({ name: 'user' }, userType), + * table({ name: 'post' }, postType) + * ); + * ``` + */ +export function reducers[]>( + ...handles: H +): Reducers>; + +/** + * Creates a schema from table definitions (array overload) + * @param handles - Array of table handles created by table() function + * @returns ColumnBuilder representing the complete database schema + */ +export function reducers[]>( + handles: H +): Reducers>; + +export function reducers[]>( + ...args: [H] | H +): Reducers> { + const handles = ( + args.length === 1 && Array.isArray(args[0]) ? args[0] : args + ) as H; + return new Reducers(handles); +} + +export function reducerSchema< + ReducerName extends string, + Params extends ParamsObj, +>(name: ReducerName, params: Params): ReducerSchema { + const paramType: ProductType = { + elements: Object.entries(params).map(([n, c]) => ({ + name: n, + algebraicType: + 'typeBuilder' in c ? c.typeBuilder.algebraicType : c.algebraicType, + })), + }; + return { + reducerName: name, + accessorName: toCamelCase(name), + params: new RowBuilder(params), + paramsSpacetimeType: paramType, + reducerDef: { + name, + params: paramType, + lifecycle: undefined, + }, + }; +} diff --git a/crates/bindings-typescript/src/sdk/schema.ts b/crates/bindings-typescript/src/sdk/schema.ts new file mode 100644 index 00000000000..539292d0ef5 --- /dev/null +++ b/crates/bindings-typescript/src/sdk/schema.ts @@ -0,0 +1,74 @@ +import { + ModuleContext, + tablesToSchema, + type TablesToSchema, + type UntypedSchemaDef, +} from '../lib/schema'; +import type { UntypedTableSchema } from '../lib/table_schema'; + +class Tables { + constructor(readonly schemaType: S) {} +} + +/** + * Creates a schema from table definitions + * @param handles - Array of table handles created by table() function + * @returns ColumnBuilder representing the complete database schema + * @example + * ```ts + * const s = schema( + * table({ name: 'user' }, userType), + * table({ name: 'post' }, postType) + * ); + * ``` + */ +export function schema( + ...handles: H +): Tables>; + +/** + * Creates a schema from table definitions (array overload) + * @param handles - Array of table handles created by table() function + * @returns ColumnBuilder representing the complete database schema + */ +export function schema( + handles: H +): Tables>; + +/** + * Creates a schema from table definitions + * @param args - Either an array of table handles or a variadic list of table handles + * @returns ColumnBuilder representing the complete database schema + * @example + * ```ts + * const s = schema( + * table({ name: 'user' }, userType), + * table({ name: 'post' }, postType) + * ); + * ``` + */ +export function schema( + ...args: [H] | H +): Tables> { + const handles = ( + args.length === 1 && Array.isArray(args[0]) ? args[0] : args + ) as H; + + const ctx = new ModuleContext(); + + return new Tables(tablesToSchema(ctx, handles)); +} + +type HasAccessor = { accessorName: PropertyKey }; + +export type ConvertToAccessorMap = { + [Tbl in TableDefs[number] as Tbl['accessorName']]: Tbl; +}; + +export function convertToAccessorMap( + arr: T +): ConvertToAccessorMap { + return Object.fromEntries( + arr.map(v => [v.accessorName, v]) + ) as ConvertToAccessorMap; +} diff --git a/crates/bindings-typescript/src/sdk/spacetime_module.ts b/crates/bindings-typescript/src/sdk/spacetime_module.ts index 049486957e5..aa19b38cd32 100644 --- a/crates/bindings-typescript/src/sdk/spacetime_module.ts +++ b/crates/bindings-typescript/src/sdk/spacetime_module.ts @@ -1,4 +1,4 @@ -import type { UntypedProceduresDef } from '../lib/procedures'; +import type { UntypedProceduresDef } from './procedures'; import type { UntypedSchemaDef } from '../lib/schema'; import type { UntypedReducersDef } from './reducers'; diff --git a/crates/bindings-typescript/src/server/index.ts b/crates/bindings-typescript/src/server/index.ts index bf31099a3b1..243dd841301 100644 --- a/crates/bindings-typescript/src/server/index.ts +++ b/crates/bindings-typescript/src/server/index.ts @@ -1,12 +1,11 @@ export * from '../lib/type_builders'; -export { schema, type InferSchema } from '../lib/schema'; +export { schema, type InferSchema } from './schema'; export { table } from '../lib/table'; -export { reducers } from '../lib/reducers'; export { SenderError, SpacetimeHostError, errors } from './errors'; export { type Reducer, type ReducerCtx } from '../lib/reducers'; export { type DbView } from './db_view'; export { and, or, not } from './query'; -export type { ProcedureCtx, TransactionCtx } from '../lib/procedures'; +export type { ProcedureCtx, TransactionCtx } from './procedures'; import './polyfills'; // Ensure polyfills are loaded import './register_hooks'; // Ensure module hooks are registered diff --git a/crates/bindings-typescript/src/server/procedures.ts b/crates/bindings-typescript/src/server/procedures.ts index 4add662a957..3fc24bc27e7 100644 --- a/crates/bindings-typescript/src/server/procedures.ts +++ b/crates/bindings-typescript/src/server/procedures.ts @@ -3,30 +3,101 @@ import BinaryReader from '../lib/binary_reader'; import BinaryWriter from '../lib/binary_writer'; import type { ConnectionId } from '../lib/connection_id'; import { Identity } from '../lib/identity'; -import { - PROCEDURES, - type ProcedureCtx, - type TransactionCtx, -} from '../lib/procedures'; -import { MODULE_DEF, type UntypedSchemaDef } from '../lib/schema'; +import type { ParamsObj, ReducerCtx } from '../lib/reducers'; +import { type UntypedSchemaDef } from '../lib/schema'; import { Timestamp } from '../lib/timestamp'; +import { + type Infer, + type InferTypeOfRow, + type TypeBuilder, +} from '../lib/type_builders'; +import { bsatnBaseSize } from '../lib/util'; +import type { HttpClient } from '../server/http_internal'; import { httpClient } from './http_internal'; import { callUserFunction, makeReducerCtx, sys } from './runtime'; +import type { SchemaInner } from './schema'; const { freeze } = Object; +export type ProcedureFn< + S extends UntypedSchemaDef, + Params extends ParamsObj, + Ret extends TypeBuilder, +> = (ctx: ProcedureCtx, args: InferTypeOfRow) => Infer; + +export interface ProcedureCtx { + readonly sender: Identity; + readonly identity: Identity; + readonly timestamp: Timestamp; + readonly connectionId: ConnectionId | null; + readonly http: HttpClient; + withTx(body: (ctx: TransactionCtx) => T): T; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface TransactionCtx + extends ReducerCtx {} + +export function procedure< + S extends UntypedSchemaDef, + Params extends ParamsObj, + Ret extends TypeBuilder, +>( + ctx: SchemaInner, + name: string, + params: Params, + ret: Ret, + fn: ProcedureFn +) { + ctx.defineFunction(name); + const paramsType: ProductType = { + elements: Object.entries(params).map(([n, c]) => ({ + name: n, + algebraicType: ctx.registerTypesRecursively( + 'typeBuilder' in c ? c.typeBuilder : c + ).algebraicType, + })), + }; + const returnType = ctx.registerTypesRecursively(ret).algebraicType; + + ctx.moduleDef.miscExports.push({ + tag: 'Procedure', + value: { + name, + params: paramsType, + returnType, + }, + }); + + ctx.procedures.push({ + fn, + paramsType, + returnType, + returnTypeBaseSize: bsatnBaseSize(ctx.typespace, returnType), + }); +} + +export type Procedures = Array<{ + fn: ProcedureFn; + paramsType: ProductType; + returnType: AlgebraicType; + returnTypeBaseSize: number; +}>; + export function callProcedure( + moduleCtx: SchemaInner, id: number, sender: Identity, connectionId: ConnectionId | null, timestamp: Timestamp, argsBuf: Uint8Array ): Uint8Array { - const { fn, paramsType, returnType, returnTypeBaseSize } = PROCEDURES[id]; + const { fn, paramsType, returnType, returnTypeBaseSize } = + moduleCtx.procedures[id]; const args = ProductType.deserializeValue( new BinaryReader(argsBuf), paramsType, - MODULE_DEF.typespace + moduleCtx.typespace ); const ctx: ProcedureCtx = { @@ -73,6 +144,6 @@ export function callProcedure( const ret = callUserFunction(fn, ctx, args); const retBuf = new BinaryWriter(returnTypeBaseSize); - AlgebraicType.serializeValue(retBuf, returnType, ret, MODULE_DEF.typespace); + AlgebraicType.serializeValue(retBuf, returnType, ret, moduleCtx.typespace); return retBuf.getBuffer(); } diff --git a/crates/bindings-typescript/src/server/reducers.ts b/crates/bindings-typescript/src/server/reducers.ts new file mode 100644 index 00000000000..f149e3e002d --- /dev/null +++ b/crates/bindings-typescript/src/server/reducers.ts @@ -0,0 +1,180 @@ +import Lifecycle from '../lib/autogen/lifecycle_type'; +import type RawReducerDefV9 from '../lib/autogen/raw_reducer_def_v_9_type'; +import type { + ParamsAsObject, + ParamsObj, + Reducer, + ReducerCtx, +} from '../lib/reducers'; +import { type UntypedSchemaDef } from '../lib/schema'; +import { + ColumnBuilder, + RowBuilder, + type Infer, + type RowObj, + type TypeBuilder, +} from '../lib/type_builders'; +import { toPascalCase } from '../lib/util'; +import type { SchemaInner } from './schema'; + +/** + * internal: pushReducer() helper used by reducer() and lifecycle wrappers + * + * @param name - The name of the reducer. + * @param params - The parameters for the reducer. + * @param fn - The reducer function. + * @param lifecycle - Optional lifecycle hooks for the reducer. + */ +export function pushReducer( + ctx: SchemaInner, + name: string, + params: RowObj | RowBuilder, + fn: Reducer, + lifecycle?: Infer['lifecycle'] +): void { + ctx.defineFunction(name); + + if (!(params instanceof RowBuilder)) { + params = new RowBuilder(params); + } + + if (params.typeName === undefined) { + params.typeName = toPascalCase(name); + } + + const ref = ctx.registerTypesRecursively(params); + const paramsType = ctx.resolveType(ref).value; + + ctx.moduleDef.reducers.push({ + name, + params: paramsType, + lifecycle, // <- lifecycle flag lands here + }); + + // If the function isn't named (e.g. `function foobar() {}`), give it the same + // name as the reducer so that it's clear what it is in in backtraces. + if (!fn.name) { + Object.defineProperty(fn, 'name', { value: name, writable: false }); + } + + ctx.reducers.push(fn); +} + +export type Reducers = Reducer[]; + +/** + * Defines a SpacetimeDB reducer function. + * + * Reducers are the primary way to modify the state of your SpacetimeDB application. + * They are atomic, meaning that either all operations within a reducer succeed, + * or none of them do. + * + * @template S - The inferred schema type of the SpacetimeDB module. + * @template Params - The type of the parameters object expected by the reducer. + * + * @param {string} name - The name of the reducer. This name will be used to call the reducer from clients. + * @param {Params} params - An object defining the parameters that the reducer accepts. + * Each key-value pair represents a parameter name and its corresponding + * {@link TypeBuilder} or {@link ColumnBuilder}. + * @param {(ctx: ReducerCtx, payload: ParamsAsObject) => void} fn - The reducer function itself. + * - `ctx`: The reducer context, providing access to `sender`, `timestamp`, `connection_id`, and `db`. + * - `payload`: An object containing the arguments passed to the reducer, typed according to `params`. + * + * @example + * ```typescript + * // Define a reducer named 'create_user' that takes 'username' (string) and 'email' (string) + * reducer( + * 'create_user', + * { + * username: t.string(), + * email: t.string(), + * }, + * (ctx, { username, email }) => { + * // Access the 'user' table from the database view in the context + * ctx.db.user.insert({ username, email, created_at: ctx.timestamp }); + * console.log(`User ${username} created by ${ctx.sender.identityId}`); + * } + * ); + * ``` + */ +export function reducer( + ctx: SchemaInner, + name: string, + params: Params, + fn: Reducer +): void { + pushReducer(ctx, name, params, fn); +} + +/** + * Registers an initialization reducer that runs when the SpacetimeDB module is published + * for the first time. + * This function is useful to set up any initial state of your database that is guaranteed + * to run only once, and before any other reducers or client connections. + * @template S - The inferred schema type of the SpacetimeDB module. + * @template Params - The type of the parameters object expected by the initialization reducer. + * + * @param params - The parameters object defining the expected input for the initialization reducer. + * @param fn - The initialization reducer function. + * - `ctx`: The reducer context, providing access to `sender`, `timestamp`, `connection_id`, and `db`. + */ +export function init( + ctx: SchemaInner, + name: string, + params: Params, + fn: Reducer +): void { + pushReducer(ctx, name, params, fn, Lifecycle.Init); +} + +/** + * Registers a reducer to be called when a client connects to the SpacetimeDB module. + * This function allows you to define custom logic that should execute + * whenever a new client establishes a connection. + * @template S - The inferred schema type of the SpacetimeDB module. + * @template Params - The type of the parameters object expected by the connection reducer. + * @param params - The parameters object defining the expected input for the connection reducer. + * @param fn - The connection reducer function itself. + */ +export function clientConnected< + S extends UntypedSchemaDef, + Params extends ParamsObj, +>( + ctx: SchemaInner, + name: string, + params: Params, + fn: Reducer +): void { + pushReducer(ctx, name, params, fn, Lifecycle.OnConnect); +} + +/** + * Registers a reducer to be called when a client disconnects from the SpacetimeDB module. + * This function allows you to define custom logic that should execute + * whenever a client disconnects. + * + * @template S - The inferred schema type of the SpacetimeDB module. + * @template Params - The type of the parameters object expected by the disconnection reducer. + * @param params - The parameters object defining the expected input for the disconnection reducer. + * @param fn - The disconnection reducer function itself. + * @example + * ```typescript + * spacetime.clientDisconnected( + * { reason: t.string() }, + * (ctx, { reason }) => { + * console.log(`Client ${ctx.connection_id} disconnected: ${reason}`); + * } + * ); + * ``` + */ +export function clientDisconnected< + S extends UntypedSchemaDef, + Params extends ParamsObj, +>( + ctx: SchemaInner, + name: string, + params: Params, + fn: Reducer +): void { + pushReducer(ctx, name, params, fn, Lifecycle.OnDisconnect); +} diff --git a/crates/bindings-typescript/src/server/runtime.ts b/crates/bindings-typescript/src/server/runtime.ts index 70e501b5e8e..19bc86af6c1 100644 --- a/crates/bindings-typescript/src/server/runtime.ts +++ b/crates/bindings-typescript/src/server/runtime.ts @@ -17,34 +17,25 @@ import { type RangedIndex, type UniqueIndex, } from '../lib/indexes'; -import { callProcedure as callProcedure } from './procedures'; +import { callProcedure } from './procedures'; import { - REDUCERS, type AuthCtx, type JsonObject, type JwtClaims, type ReducerCtx, } from '../lib/reducers'; -import { - MODULE_DEF, - getRegisteredSchema, - type UntypedSchemaDef, -} from '../lib/schema'; +import { type UntypedSchemaDef } from '../lib/schema'; import { type RowType, type Table, type TableMethods } from '../lib/table'; import { Timestamp } from '../lib/timestamp'; import type { Infer } from '../lib/type_builders'; import { bsatnBaseSize, toCamelCase } from '../lib/util'; -import { - ANON_VIEWS, - VIEWS, - type AnonymousViewCtx, - type ViewCtx, -} from '../lib/views'; +import { type AnonymousViewCtx, type ViewCtx } from './views'; import { isRowTypedQuery, makeQueryBuilder, toSql } from './query'; import type { DbView } from './db_view'; import { SenderError, SpacetimeHostError } from './errors'; import { Range, type Bound } from './range'; import ViewResultHeader from '../lib/autogen/view_result_header_type'; +import { getRegisteredSchema } from './schema'; const { freeze } = Object; @@ -215,18 +206,19 @@ export const hooks: ModuleHooks = { AlgebraicType.serializeValue( writer, RawModuleDef.algebraicType, - RawModuleDef.V9(MODULE_DEF) + RawModuleDef.V9(getRegisteredSchema().moduleDef) ); return writer.getBuffer(); }, __call_reducer__(reducerId, sender, connId, timestamp, argsBuf) { + const moduleCtx = getRegisteredSchema(); const argsType = AlgebraicType.Product( - MODULE_DEF.reducers[reducerId].params + moduleCtx.moduleDef.reducers[reducerId].params ); const args = AlgebraicType.deserializeValue( new BinaryReader(argsBuf), argsType, - MODULE_DEF.typespace + moduleCtx.typespace ); const senderIdentity = new Identity(sender); const ctx: ReducerCtx = freeze( @@ -237,7 +229,11 @@ export const hooks: ModuleHooks = { ) ); try { - return callUserFunction(REDUCERS[reducerId], ctx, args) ?? { tag: 'ok' }; + return ( + callUserFunction(moduleCtx.reducers[reducerId], ctx, args) ?? { + tag: 'ok', + } + ); } catch (e) { if (e instanceof SenderError) { return { tag: 'err', value: e.message }; @@ -249,20 +245,21 @@ export const hooks: ModuleHooks = { export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = { __call_view__(id, sender, argsBuf) { - const { fn, params, returnType, returnTypeBaseSize } = VIEWS[id]; + const moduleCtx = getRegisteredSchema(); + const { fn, params, returnType, returnTypeBaseSize } = moduleCtx.views[id]; const ctx: ViewCtx = freeze({ sender: new Identity(sender), // this is the non-readonly DbView, but the typing for the user will be // the readonly one, and if they do call mutating functions it will fail // at runtime db: getDbView(), - from: makeQueryBuilder(getRegisteredSchema()), + from: makeQueryBuilder(moduleCtx.schemaType), }); // ViewResultHeader.RawSql const args = ProductType.deserializeValue( new BinaryReader(argsBuf), params, - MODULE_DEF.typespace + moduleCtx.typespace ); const ret = callUserFunction(fn, ctx, args); const retBuf = new BinaryWriter(returnTypeBaseSize); @@ -273,7 +270,7 @@ export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = { retBuf, ViewResultHeader.algebraicType, v, - MODULE_DEF.typespace + moduleCtx.typespace ); return { data: retBuf.getBuffer(), @@ -283,13 +280,13 @@ export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = { retBuf, ViewResultHeader.algebraicType, ViewResultHeader.RowData, - MODULE_DEF.typespace + moduleCtx.typespace ); AlgebraicType.serializeValue( retBuf, returnType, ret, - MODULE_DEF.typespace + moduleCtx.typespace ); return { data: retBuf.getBuffer(), @@ -297,18 +294,20 @@ export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = { } }, __call_view_anon__(id, argsBuf) { - const { fn, params, returnType, returnTypeBaseSize } = ANON_VIEWS[id]; + const moduleCtx = getRegisteredSchema(); + const { fn, params, returnType, returnTypeBaseSize } = + moduleCtx.anonViews[id]; const ctx: AnonymousViewCtx = freeze({ // this is the non-readonly DbView, but the typing for the user will be // the readonly one, and if they do call mutating functions it will fail // at runtime db: getDbView(), - from: makeQueryBuilder(getRegisteredSchema()), + from: makeQueryBuilder(moduleCtx.schemaType), }); const args = ProductType.deserializeValue( new BinaryReader(argsBuf), params, - MODULE_DEF.typespace + moduleCtx.typespace ); const ret = callUserFunction(fn, ctx, args); const retBuf = new BinaryWriter(returnTypeBaseSize); @@ -319,7 +318,7 @@ export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = { retBuf, ViewResultHeader.algebraicType, v, - MODULE_DEF.typespace + moduleCtx.typespace ); return { data: retBuf.getBuffer(), @@ -329,13 +328,13 @@ export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = { retBuf, ViewResultHeader.algebraicType, ViewResultHeader.RowData, - MODULE_DEF.typespace + moduleCtx.typespace ); AlgebraicType.serializeValue( retBuf, returnType, ret, - MODULE_DEF.typespace + moduleCtx.typespace ); return { data: retBuf.getBuffer(), @@ -347,6 +346,7 @@ export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = { export const hooks_v1_2: import('spacetime:sys@1.2').ModuleHooks = { __call_procedure__(id, sender, connection_id, timestamp, args) { return callProcedure( + getRegisteredSchema(), id, new Identity(sender), ConnectionId.nullIfZero(new ConnectionId(connection_id)), @@ -358,7 +358,7 @@ export const hooks_v1_2: import('spacetime:sys@1.2').ModuleHooks = { let DB_VIEW: DbView | null = null; function getDbView() { - DB_VIEW ??= makeDbView(MODULE_DEF); + DB_VIEW ??= makeDbView(getRegisteredSchema().moduleDef); return DB_VIEW; } @@ -651,7 +651,7 @@ function hasOwn( function* tableIterator(id: u32, ty: AlgebraicType): Generator { using iter = new IteratorHandle(id); - const { typespace } = MODULE_DEF; + const { typespace } = getRegisteredSchema(); let buf; while ((buf = advanceIter(iter)) != null) { diff --git a/crates/bindings-typescript/src/server/schema.test-d.ts b/crates/bindings-typescript/src/server/schema.test-d.ts index c1846ca1716..cc8e5a8d967 100644 --- a/crates/bindings-typescript/src/server/schema.test-d.ts +++ b/crates/bindings-typescript/src/server/schema.test-d.ts @@ -1,4 +1,4 @@ -import { schema } from '../lib/schema'; +import { schema } from './schema'; import { table } from '../lib/table'; import t from '../lib/type_builders'; diff --git a/crates/bindings-typescript/src/server/schema.ts b/crates/bindings-typescript/src/server/schema.ts new file mode 100644 index 00000000000..8a69b055dcb --- /dev/null +++ b/crates/bindings-typescript/src/server/schema.ts @@ -0,0 +1,420 @@ +import { + type ParamsAsObject, + type ParamsObj, + type Reducer, + type ReducerCtx, +} from '../lib/reducers'; +import { + ModuleContext, + tablesToSchema, + type TablesToSchema, + type UntypedSchemaDef, +} from '../lib/schema'; +import type { UntypedTableSchema } from '../lib/table_schema'; +import { ColumnBuilder, TypeBuilder } from '../lib/type_builders'; +import { procedure, type ProcedureFn, type Procedures } from './procedures'; +import { + clientConnected, + clientDisconnected, + init, + reducer, + type Reducers, +} from './reducers'; + +import { + defineView, + type AnonViews, + type AnonymousViewFn, + type ViewFn, + type ViewOpts, + type ViewReturnTypeBuilder, + type Views, +} from './views'; + +let REGISTERED_SCHEMA: SchemaInner | null = null; + +export function getRegisteredSchema(): SchemaInner { + if (REGISTERED_SCHEMA == null) { + throw new Error('Schema has not been registered yet. Call schema() first.'); + } + return REGISTERED_SCHEMA; +} + +export class SchemaInner< + S extends UntypedSchemaDef = UntypedSchemaDef, +> extends ModuleContext { + schemaType: S; + existingFunctions = new Set(); + reducers: Reducers = []; + procedures: Procedures = []; + views: Views = []; + anonViews: AnonViews = []; + + constructor(getSchemaType: (ctx: ModuleContext) => S) { + super(); + this.schemaType = getSchemaType(this); + } + + defineFunction(name: string) { + if (this.existingFunctions.has(name)) { + throw new TypeError( + `There is already a reducer or procedure with the name '${name}'` + ); + } + this.existingFunctions.add(name); + } +} + +/** + * The Schema class represents the database schema for a SpacetimeDB application. + * It encapsulates the table definitions and typespace, and provides methods to define + * reducers and lifecycle hooks. + * + * Schema has a generic parameter S which represents the inferred schema type. This type + * is automatically inferred when creating a schema using the `schema()` function and is + * used to type the database view in reducer contexts. + * + * The methods on this class are used to register reducers and lifecycle hooks + * with the SpacetimeDB runtime. Theey forward to free functions that handle the actual + * registration logic, but having them as methods on the Schema class helps with type inference. + * + * @template S - The inferred schema type of the SpacetimeDB module. + * + * @example + * ```typescript + * const spacetime = schema( + * table({ name: 'user' }, userType), + * table({ name: 'post' }, postType) + * ); + * spacetime.reducer( + * 'create_user', + * { username: t.string(), email: t.string() }, + * (ctx, { username, email }) => { + * ctx.db.user.insert({ username, email, created_at: ctx.timestamp }); + * console.log(`User ${username} created by ${ctx.sender.identityId}`); + * } + * ); + * ``` + */ +// TODO(cloutiertyler): It might be nice to have a way to access the types +// for the tables from the schema object, e.g. `spacetimedb.user.type` would +// be the type of the user table. +class Schema { + #ctx: SchemaInner; + + constructor(ctx: SchemaInner) { + // TODO: TableSchema and TableDef should really be unified + this.#ctx = ctx; + } + + get schemaType(): S { + return this.#ctx.schemaType; + } + + get moduleDef() { + return this.#ctx.moduleDef; + } + + get typespace() { + return this.#ctx.typespace; + } + + /** + * Defines a SpacetimeDB reducer function. + * + * Reducers are the primary way to modify the state of your SpacetimeDB application. + * They are atomic, meaning that either all operations within a reducer succeed, + * or none of them do. + * + * @template S - The inferred schema type of the SpacetimeDB module. + * @template Params - The type of the parameters object expected by the reducer. + * + * @param {string} name - The name of the reducer. This name will be used to call the reducer from clients. + * @param {Params} params - An object defining the parameters that the reducer accepts. + * Each key-value pair represents a parameter name and its corresponding + * {@link TypeBuilder} or {@link ColumnBuilder}. + * @param {(ctx: ReducerCtx, payload: ParamsAsObject) => void} fn - The reducer function itself. + * - `ctx`: The reducer context, providing access to `sender`, `timestamp`, `connection_id`, and `db`. + * - `payload`: An object containing the arguments passed to the reducer, typed according to `params`. + * + * @example + * ```typescript + * // Define a reducer named 'create_user' that takes 'username' (string) and 'email' (string) + * spacetime.reducer( + * 'create_user', + * { + * username: t.string(), + * email: t.string(), + * }, + * (ctx, { username, email }) => { + * // Access the 'user' table from the database view in the context + * ctx.db.user.insert({ username, email, created_at: ctx.timestamp }); + * console.log(`User ${username} created by ${ctx.sender.identityId}`); + * } + * ); + * ``` + */ + reducer( + name: string, + params: Params, + fn: Reducer + ): Reducer; + reducer(name: string, fn: Reducer): Reducer; + reducer( + name: string, + paramsOrFn: Params | Reducer, + fn?: Reducer + ): Reducer { + if (typeof paramsOrFn === 'function') { + // This is the case where params are omitted. + // The second argument is the reducer function. + // We pass an empty object for the params. + reducer(this.#ctx, name, {}, paramsOrFn); + return paramsOrFn; + } else { + // This is the case where params are provided. + // The second argument is the params object, and the third is the function. + // The `fn` parameter is guaranteed to be defined here. + reducer(this.#ctx, name, paramsOrFn, fn!); + return fn!; + } + } + + /** + * Registers an initialization reducer that runs when the SpacetimeDB module is published + * for the first time. + * + * This function is useful to set up any initial state of your database that is guaranteed + * to run only once, and before any other reducers or client connections. + * + * @template S - The inferred schema type of the SpacetimeDB module. + * @param {Reducer} fn - The initialization reducer function. + * - `ctx`: The reducer context, providing access to `sender`, `timestamp`, `connection_id`, and `db`. + * @example + * ```typescript + * spacetime.init((ctx) => { + * ctx.db.user.insert({ username: 'admin', email: 'admin@example.com' }); + * }); + * ``` + */ + init(fn: Reducer): void; + init(name: string, fn: Reducer): void; + init(nameOrFn: any, maybeFn?: Reducer): void { + const [name, fn] = + typeof nameOrFn === 'string' ? [nameOrFn, maybeFn] : ['init', nameOrFn]; + init(this.#ctx, name, {}, fn); + } + + /** + * Registers a reducer to be called when a client connects to the SpacetimeDB module. + * This function allows you to define custom logic that should execute + * whenever a new client establishes a connection. + * @template S - The inferred schema type of the SpacetimeDB module. + * + * @param fn - The reducer function to execute on client connection. + * + * @example + * ```typescript + * spacetime.clientConnected( + * (ctx) => { + * console.log(`Client ${ctx.connectionId} connected`); + * } + * ); + */ + clientConnected(fn: Reducer): void; + clientConnected(name: string, fn: Reducer): void; + clientConnected(nameOrFn: any, maybeFn?: Reducer): void { + const [name, fn] = + typeof nameOrFn === 'string' + ? [nameOrFn, maybeFn] + : ['on_connect', nameOrFn]; + clientConnected(this.#ctx, name, {}, fn); + } + + /** + * Registers a reducer to be called when a client disconnects from the SpacetimeDB module. + * This function allows you to define custom logic that should execute + * whenever a client disconnects. + * @template S - The inferred schema type of the SpacetimeDB module. + * + * @param fn - The reducer function to execute on client disconnection. + * + * @example + * ```typescript + * spacetime.clientDisconnected( + * (ctx) => { + * console.log(`Client ${ctx.connectionId} disconnected`); + * } + * ); + * ``` + */ + clientDisconnected(fn: Reducer): void; + clientDisconnected(name: string, fn: Reducer): void; + clientDisconnected(nameOrFn: any, maybeFn?: Reducer): void { + const [name, fn] = + typeof nameOrFn === 'string' + ? [nameOrFn, maybeFn] + : ['on_disconnect', nameOrFn]; + clientDisconnected(this.#ctx, name, {}, fn); + } + + view( + opts: ViewOpts, + ret: Ret, + fn: ViewFn + ): void { + defineView(this.#ctx, opts, false, {}, ret, fn); + } + + // TODO: re-enable once parameterized views are supported in SQL + // view( + // opts: ViewOpts, + // ret: Ret, + // fn: ViewFn + // ): void; + // view( + // opts: ViewOpts, + // params: Params, + // ret: Ret, + // fn: ViewFn + // ): void; + // view( + // opts: ViewOpts, + // paramsOrRet: Ret | Params, + // retOrFn: ViewFn | Ret, + // maybeFn?: ViewFn + // ): void { + // if (typeof retOrFn === 'function') { + // defineView(name, false, {}, paramsOrRet as Ret, retOrFn); + // } else { + // defineView(name, false, paramsOrRet as Params, retOrFn, maybeFn!); + // } + // } + + anonymousView( + opts: ViewOpts, + ret: Ret, + fn: AnonymousViewFn + ): void { + defineView(this.#ctx, opts, true, {}, ret, fn); + } + + // TODO: re-enable once parameterized views are supported in SQL + // anonymousView( + // opts: ViewOpts, + // ret: Ret, + // fn: AnonymousViewFn + // ): void; + // anonymousView( + // opts: ViewOpts, + // params: Params, + // ret: Ret, + // fn: AnonymousViewFn + // ): void; + // anonymousView( + // opts: ViewOpts, + // paramsOrRet: Ret | Params, + // retOrFn: AnonymousViewFn | Ret, + // maybeFn?: AnonymousViewFn + // ): void { + // if (typeof retOrFn === 'function') { + // defineView(name, true, {}, paramsOrRet as Ret, retOrFn); + // } else { + // defineView(name, true, paramsOrRet as Params, retOrFn, maybeFn!); + // } + // } + + procedure>( + name: string, + params: Params, + ret: Ret, + fn: ProcedureFn + ): ProcedureFn; + procedure>( + name: string, + ret: Ret, + fn: ProcedureFn + ): ProcedureFn; + procedure>( + name: string, + paramsOrRet: Ret | Params, + retOrFn: ProcedureFn | Ret, + maybeFn?: ProcedureFn + ): ProcedureFn { + if (typeof retOrFn === 'function') { + procedure(this.#ctx, name, {}, paramsOrRet as Ret, retOrFn); + return retOrFn; + } else { + procedure(this.#ctx, name, paramsOrRet as Params, retOrFn, maybeFn!); + return maybeFn!; + } + } + + clientVisibilityFilter = { + sql: (filter: string) => { + this.#ctx.moduleDef.rowLevelSecurity.push({ sql: filter }); + }, + }; +} + +/** + * Extracts the inferred schema type from a Schema instance + */ +export type InferSchema> = + SchemaDef extends Schema ? S : never; + +/** + * Creates a schema from table definitions + * @param handles - Array of table handles created by table() function + * @returns ColumnBuilder representing the complete database schema + * @example + * ```ts + * const s = schema( + * table({ name: 'user' }, userType), + * table({ name: 'post' }, postType) + * ); + * ``` + */ +export function schema( + ...handles: H +): Schema>; + +/** + * Creates a schema from table definitions (array overload) + * @param handles - Array of table handles created by table() function + * @returns ColumnBuilder representing the complete database schema + */ +export function schema( + handles: H +): Schema>; + +/** + * Creates a schema from table definitions + * @param args - Either an array of table handles or a variadic list of table handles + * @returns ColumnBuilder representing the complete database schema + * @example + * ```ts + * const s = schema( + * table({ name: 'user' }, userType), + * table({ name: 'post' }, postType) + * ); + * ``` + */ +export function schema( + ...args: [H] | H +): Schema> { + const handles = ( + args.length === 1 && Array.isArray(args[0]) ? args[0] : args + ) as H; + + const ctx = new SchemaInner(ctx => { + const tableDefs = handles.map(h => h.tableDef(ctx)); + ctx.moduleDef.tables.push(...tableDefs); + + return tablesToSchema(ctx, handles); + }); + + REGISTERED_SCHEMA = ctx; + + return new Schema(ctx); +} diff --git a/crates/bindings-typescript/src/server/view.test-d.ts b/crates/bindings-typescript/src/server/view.test-d.ts index a12c71a2528..8e5b7fbff7b 100644 --- a/crates/bindings-typescript/src/server/view.test-d.ts +++ b/crates/bindings-typescript/src/server/view.test-d.ts @@ -1,4 +1,4 @@ -import { schema } from '../lib/schema'; +import { schema } from './schema'; import { table } from '../lib/table'; import t from '../lib/type_builders'; diff --git a/crates/bindings-typescript/src/lib/views.ts b/crates/bindings-typescript/src/server/views.ts similarity index 81% rename from crates/bindings-typescript/src/lib/views.ts rename to crates/bindings-typescript/src/server/views.ts index 82f9f069393..f4e86be5653 100644 --- a/crates/bindings-typescript/src/lib/views.ts +++ b/crates/bindings-typescript/src/server/views.ts @@ -5,23 +5,19 @@ import { } from '../lib/algebraic_type'; import type { Identity } from '../lib/identity'; import type { OptionAlgebraicType } from '../lib/option'; -import type { ParamsObj } from './reducers'; -import { - MODULE_DEF, - registerTypesRecursively, - resolveType, - type UntypedSchemaDef, -} from './schema'; -import type { ReadonlyTable } from './table'; +import type { ParamsObj } from '../lib/reducers'; +import { type UntypedSchemaDef } from '../lib/schema'; +import type { ReadonlyTable } from '../lib/table'; import { RowBuilder, type Infer, type InferSpacetimeTypeOfTypeBuilder, type InferTypeOfRow, type TypeBuilder, -} from './type_builders'; -import { bsatnBaseSize, toPascalCase } from './util'; +} from '../lib/type_builders'; +import { bsatnBaseSize, toPascalCase } from '../lib/util'; import { type QueryBuilder, type RowTypedQuery } from '../server/query'; +import type { SchemaInner } from './schema'; export type ViewCtx = Readonly<{ sender: Identity; @@ -88,6 +84,7 @@ export function defineView< Params extends ParamsObj, Ret extends ViewReturnTypeBuilder, >( + ctx: SchemaInner, opts: ViewOpts, anon: Anonymous, params: Params, @@ -99,18 +96,17 @@ export function defineView< const paramsBuilder = new RowBuilder(params, toPascalCase(opts.name)); // Register return types if they are product types - let returnType = registerTypesRecursively(ret).algebraicType; + let returnType = ctx.registerTypesRecursively(ret).algebraicType; - const { value: paramType } = resolveType( - MODULE_DEF.typespace, - registerTypesRecursively(paramsBuilder) + const { value: paramType } = ctx.resolveType( + ctx.registerTypesRecursively(paramsBuilder) ); - MODULE_DEF.miscExports.push({ + ctx.moduleDef.miscExports.push({ tag: 'View', value: { name: opts.name, - index: (anon ? ANON_VIEWS : VIEWS).length, + index: (anon ? ctx.anonViews : ctx.views).length, isPublic: opts.public, isAnonymous: anon, params: paramType, @@ -130,11 +126,11 @@ export function defineView< ); } - (anon ? ANON_VIEWS : VIEWS).push({ + (anon ? ctx.anonViews : ctx.views).push({ fn, params: paramType, returnType, - returnTypeBaseSize: bsatnBaseSize(MODULE_DEF.typespace, returnType), + returnTypeBaseSize: bsatnBaseSize(ctx.typespace, returnType), }); } @@ -145,8 +141,8 @@ type ViewInfo = { returnTypeBaseSize: number; }; -export const VIEWS: ViewInfo>[] = []; -export const ANON_VIEWS: ViewInfo>[] = []; +export type Views = ViewInfo>[]; +export type AnonViews = ViewInfo>[]; // A helper to get the product type out of a type builder. // This is only non-never if the type builder is an array. diff --git a/eslint.config.js b/eslint.config.js index 4f96b16ab8e..df702b1b603 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,12 +3,18 @@ import globals from 'globals'; import tseslint from 'typescript-eslint'; import reactHooks from 'eslint-plugin-react-hooks'; import reactRefresh from 'eslint-plugin-react-refresh'; +import { jsdoc } from 'eslint-plugin-jsdoc'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; const __dirname = dirname(fileURLToPath(import.meta.url)); export default tseslint.config( + jsdoc({ + rules: { + 'jsdoc/no-undefined-types': 'error', + }, + }), { ignores: ['**/dist/**', '**/build/**', '**/coverage/**'], }, @@ -48,7 +54,7 @@ export default tseslint.config( }, }, linterOptions: { - reportUnusedDisableDirectives: "off", + reportUnusedDisableDirectives: 'off', }, plugins: { '@typescript-eslint': tseslint.plugin, @@ -58,25 +64,37 @@ export default tseslint.config( rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-namespace': 'error', - "@typescript-eslint/no-unused-vars": [ - "error", + '@typescript-eslint/no-unused-vars': [ + 'error', { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "destructuredArrayIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" - } + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, ], 'no-restricted-syntax': [ 'error', - { selector: 'TSEnumDeclaration', message: 'Do not use enums; stick to JS-compatible types.' }, - { selector: 'TSEnumDeclaration[const=true]', message: 'Do not use const enum; use unions or objects.' }, + { + selector: 'TSEnumDeclaration', + message: 'Do not use enums; stick to JS-compatible types.', + }, + { + selector: 'TSEnumDeclaration[const=true]', + message: 'Do not use const enum; use unions or objects.', + }, { selector: 'Decorator', message: 'Do not use decorators.' }, ], ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], - "eslint-comments/no-unused-disable": "off", - "@typescript-eslint/no-empty-object-type": ['error', { allowObjectTypes: 'always' }], + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + 'eslint-comments/no-unused-disable': 'off', + '@typescript-eslint/no-empty-object-type': [ + 'error', + { allowObjectTypes: 'always' }, + ], }, } -); \ No newline at end of file +); diff --git a/package.json b/package.json index 64fa9419780..eeb4bc9bba5 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,18 @@ { "private": true, "packageManager": "pnpm@9.7.0", - "engines": { "node": ">=18.0.0", "pnpm": ">=9.0.0" }, + "engines": { + "node": ">=18.0.0", + "pnpm": ">=9.0.0" + }, "type": "module", "scripts": { - "format": "pnpm --filter ./crates/bindings-typescript run format && pnpm --filter ./docs run format && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run format && pnpm --filter ./crates/bindings-typescript/test-app run format", - "lint": "pnpm --filter ./crates/bindings-typescript run lint && pnpm --filter ./docs run lint && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run lint && pnpm --filter ./crates/bindings-typescript/test-app run lint", - "build": "pnpm --filter ./crates/bindings-typescript run build && pnpm --filter ./docs run build && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run build && pnpm --filter ./crates/bindings-typescript/test-app run build", - "test": "pnpm --filter ./crates/bindings-typescript run test && pnpm --filter ./docs run test && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run test && pnpm --filter ./crates/bindings-typescript/test-app run test", - "generate": "pnpm --filter ./crates/bindings-typescript run generate && pnpm --filter ./docs run generate && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run generate && pnpm --filter ./crates/bindings-typescript/test-app run generate", + "run-all": "pnpm -F ./crates/bindings-typescript -F ./crates/bindings-typescript/examples/quickstart-chat -F ./crates/bindings-typescript/test-app -F ./docs run", + "format": "pnpm run-all format && prettier eslint.config.js --write", + "lint": "pnpm run-all lint && prettier eslint.config.js --check", + "build": "pnpm run-all build", + "test": "pnpm run-all test", + "generate": "pnpm run-all generate", "clean": "pnpm -r exec rimraf dist .tsbuildinfo coverage" }, "devDependencies": { @@ -17,6 +21,7 @@ "@typescript-eslint/eslint-plugin": "^8.18.2", "@typescript-eslint/parser": "^8.18.2", "eslint": "^9.17.0", + "eslint-plugin-jsdoc": "^61.5.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.16", "globals": "^15.14.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 773d5cebd69..277dfbd6f80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: eslint: specifier: ^9.17.0 version: 9.33.0(jiti@2.5.1) + eslint-plugin-jsdoc: + specifier: ^61.5.0 + version: 61.5.0(eslint@9.33.0(jiti@2.5.1)) eslint-plugin-react-hooks: specifier: ^5.0.0 version: 5.2.0(eslint@9.33.0(jiti@2.5.1)) @@ -102,6 +105,9 @@ importers: eslint: specifier: ^9.33.0 version: 9.33.0(jiti@2.5.1) + eslint-plugin-jsdoc: + specifier: ^61.5.0 + version: 61.5.0(eslint@9.33.0(jiti@2.5.1)) globals: specifier: ^15.14.0 version: 15.15.0 @@ -267,7 +273,7 @@ importers: version: 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/plugin-content-docs': specifier: 3.9.2 - version: 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + version: 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/preset-classic': specifier: 3.9.2 version: 3.9.2(@algolia/client-search@5.39.0)(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.6.3) @@ -1659,6 +1665,14 @@ packages: '@emotion/memoize@0.7.4': resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + '@es-joy/jsdoccomment@0.76.0': + resolution: {integrity: sha512-g+RihtzFgGTx2WYCuTHbdOXJeAlGnROws0TeALx9ow/ZmOROOZkVg5wp/B44n0WJgI4SQFP1eWM2iRPlU2Y14w==} + engines: {node: '>=20.11.0'} + + '@es-joy/resolve.exports@1.2.0': + resolution: {integrity: sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==} + engines: {node: '>=10'} + '@esbuild/aix-ppc64@0.25.9': resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} @@ -3046,6 +3060,10 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sindresorhus/base62@1.0.0': + resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} + engines: {node: '>=18'} + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -3459,6 +3477,10 @@ packages: resolution: {integrity: sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.50.0': + resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.40.0': resolution: {integrity: sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4117,6 +4139,10 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -4489,6 +4515,10 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} @@ -5000,6 +5030,12 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} + eslint-plugin-jsdoc@61.5.0: + resolution: {integrity: sha512-PR81eOGq4S7diVnV9xzFSBE4CDENRQGP0Lckkek8AdHtbj+6Bm0cItwlFnxsLFriJHspiE3mpu8U20eODyToIg==} + engines: {node: '>=20.11.0'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + eslint-plugin-react-hooks@5.2.0: resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} engines: {node: '>=10'} @@ -5536,6 +5572,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -5936,6 +5975,10 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdoc-type-pratt-parser@6.10.0: + resolution: {integrity: sha512-+LexoTRyYui5iOhJGn13N9ZazL23nAHGkXsa1p/C8yeq79WRfLBag6ZZ0FQG2aRoc9yfo59JT9EYCQonOkHKkQ==} + engines: {node: '>=20.0.0'} + jsdom@26.1.0: resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} engines: {node: '>=18'} @@ -6633,6 +6676,9 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-deep-merge@2.0.0: + resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -6770,6 +6816,9 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-imports-exports@0.2.4: + resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -6777,6 +6826,9 @@ packages: parse-numeric-range@1.3.0: resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} + parse-statements@1.0.11: + resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} + parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -7705,6 +7757,10 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reserved-identifiers@1.2.0: + resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} + engines: {node: '>=18'} + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -7825,6 +7881,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -7974,6 +8035,15 @@ packages: undici: optional: true + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + + spdx-license-ids@3.0.22: + resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} + spdy-transport@3.0.0: resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} @@ -8214,6 +8284,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + to-valid-identifier@1.0.0: + resolution: {integrity: sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==} + engines: {node: '>=20'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -10496,7 +10570,7 @@ snapshots: '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -10533,6 +10607,46 @@ snapshots: - webpack-cli '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@docusaurus/logger': 3.9.2 + '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/react-router-config': 5.0.11 + combine-promises: 1.2.0 + fs-extra: 11.3.2 + js-yaml: 4.1.0 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + schema-dts: 1.1.5 + tslib: 2.8.1 + utility-types: 3.11.0 + webpack: 5.102.0 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': dependencies: '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/logger': 3.9.2 @@ -10801,7 +10915,7 @@ snapshots: dependencies: '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) @@ -10849,7 +10963,7 @@ snapshots: '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.9.2 @@ -10889,7 +11003,7 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -10913,12 +11027,36 @@ snapshots: - uglify-js - webpack-cli + '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/history': 4.7.11 + '@types/react': 18.3.23 + '@types/react-router-config': 5.0.11 + clsx: 2.1.1 + parse-numeric-range: 1.3.0 + prism-react-renderer: 2.4.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + utility-types: 3.11.0 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.39.0)(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.6.3)': dependencies: '@docsearch/react': 4.2.0(@algolia/client-search@5.39.0)(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/logger': 3.9.2 - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.9.2 '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11204,6 +11342,16 @@ snapshots: '@emotion/memoize@0.7.4': optional: true + '@es-joy/jsdoccomment@0.76.0': + dependencies: + '@types/estree': 1.0.8 + '@typescript-eslint/types': 8.50.0 + comment-parser: 1.4.1 + esquery: 1.6.0 + jsdoc-type-pratt-parser: 6.10.0 + + '@es-joy/resolve.exports@1.2.0': {} + '@esbuild/aix-ppc64@0.25.9': optional: true @@ -12761,6 +12909,8 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@sindresorhus/base62@1.0.0': {} + '@sindresorhus/is@4.6.0': {} '@sindresorhus/is@5.6.0': {} @@ -13313,6 +13463,8 @@ snapshots: '@typescript-eslint/types@8.40.0': {} + '@typescript-eslint/types@8.50.0': {} + '@typescript-eslint/typescript-estree@8.40.0(typescript@5.6.3)': dependencies: '@typescript-eslint/project-service': 8.40.0(typescript@5.6.3) @@ -14596,6 +14748,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + are-docs-informative@0.0.2: {} + arg@4.1.3: {} arg@5.0.2: {} @@ -14999,6 +15153,8 @@ snapshots: commander@8.3.0: {} + comment-parser@1.4.1: {} + common-path-prefix@3.0.0: {} compressible@2.0.18: @@ -15514,6 +15670,26 @@ snapshots: escape-string-regexp@5.0.0: {} + eslint-plugin-jsdoc@61.5.0(eslint@9.33.0(jiti@2.5.1)): + dependencies: + '@es-joy/jsdoccomment': 0.76.0 + '@es-joy/resolve.exports': 1.2.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint: 9.33.0(jiti@2.5.1) + espree: 10.4.0 + esquery: 1.6.0 + html-entities: 2.6.0 + object-deep-merge: 2.0.0 + parse-imports-exports: 0.2.4 + semver: 7.7.3 + spdx-expression-parse: 4.0.0 + to-valid-identifier: 1.0.0 + transitivePeerDependencies: + - supports-color + eslint-plugin-react-hooks@5.2.0(eslint@9.33.0(jiti@2.5.1)): dependencies: eslint: 9.33.0(jiti@2.5.1) @@ -16241,6 +16417,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-entities@2.6.0: {} + html-escaper@2.0.2: {} html-minifier-terser@6.1.0: @@ -16613,6 +16791,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsdoc-type-pratt-parser@6.10.0: {} + jsdom@26.1.0: dependencies: cssstyle: 4.6.0 @@ -17635,6 +17815,8 @@ snapshots: object-assign@4.1.1: {} + object-deep-merge@2.0.0: {} + object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -17794,6 +17976,10 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse-imports-exports@0.2.4: + dependencies: + parse-statements: 1.0.11 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.27.1 @@ -17803,6 +17989,8 @@ snapshots: parse-numeric-range@1.3.0: {} + parse-statements@1.0.11: {} + parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 @@ -18847,6 +19035,8 @@ snapshots: requires-port@1.0.0: {} + reserved-identifiers@1.2.0: {} + resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} @@ -18976,6 +19166,8 @@ snapshots: semver@7.7.2: {} + semver@7.7.3: {} + send@0.19.0: dependencies: debug: 2.6.9 @@ -19173,6 +19365,15 @@ snapshots: react: 19.2.0 undici: 6.21.3 + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.22 + + spdx-license-ids@3.0.22: {} + spdy-transport@3.0.0: dependencies: debug: 4.4.3 @@ -19406,6 +19607,11 @@ snapshots: dependencies: is-number: 7.0.0 + to-valid-identifier@1.0.0: + dependencies: + '@sindresorhus/base62': 1.0.0 + reserved-identifiers: 1.2.0 + toidentifier@1.0.1: {} totalist@3.0.1: {}