Reorganize TS sdk (#3915)

# Description of Changes

This moves a bunch of stuff from `lib` back into `server` and `sdk`, and
removes all but one global variable from the server sdk in preparation
for export-based reducer definition.

# Expected complexity level and risk

2 - a pretty big refactor, but it's mostly just code movement.

# Testing

- [x] Refactor, so automated tests are sufficient.
This commit is contained in:
Noa
2026-02-03 10:50:10 -06:00
committed by GitHub
parent c853721fdb
commit aa2f70c877
29 changed files with 1365 additions and 1087 deletions
+3 -3
View File
@@ -93,7 +93,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
- uses: pnpm/action-setup@v4
with:
@@ -177,7 +177,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
- uses: pnpm/action-setup@v4
with:
@@ -459,7 +459,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
- uses: pnpm/action-setup@v4
with:
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
- uses: pnpm/action-setup@v4
with:
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
- uses: pnpm/action-setup@v4
with:
+1
View File
@@ -204,6 +204,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",
@@ -1,4 +1,4 @@
import type { UntypedTableDef } from './table';
import type { table, UntypedTableDef } from './table';
import type { ColumnMetadata } from './type_builders';
/**
@@ -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';
@@ -1,148 +0,0 @@
import {
AlgebraicType,
ProductType,
type Deserializer,
type Serializer,
} 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';
import type { Uuid } from './uuid';
export type ProcedureFn<
S extends UntypedSchemaDef,
Params extends ParamsObj,
Ret extends TypeBuilder<any, any>,
> = (ctx: ProcedureCtx<S>, args: InferTypeOfRow<Params>) => Infer<Ret>;
export interface ProcedureCtx<S extends UntypedSchemaDef> {
readonly sender: Identity;
readonly identity: Identity;
readonly timestamp: Timestamp;
readonly connectionId: ConnectionId | null;
readonly http: HttpClient;
readonly counter_uuid: { value: number };
withTx<T>(body: (ctx: TransactionCtx<S>) => T): T;
newUuidV4(): Uuid;
newUuidV7(): Uuid;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface TransactionCtx<S extends UntypedSchemaDef>
extends ReducerCtx<S> {}
export function procedure<
S extends UntypedSchemaDef,
Params extends ParamsObj,
Ret extends TypeBuilder<any, any>,
>(name: string, params: Params, ret: Ret, fn: ProcedureFn<S, Params, Ret>) {
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,
},
});
const { typespace } = MODULE_DEF;
PROCEDURES.push({
fn,
deserializeArgs: ProductType.makeDeserializer(paramsType, typespace),
serializeReturn: AlgebraicType.makeSerializer(returnType, typespace),
returnTypeBaseSize: bsatnBaseSize(typespace, returnType),
});
}
export const PROCEDURES: Array<{
fn: ProcedureFn<any, any, any>;
deserializeArgs: Deserializer<any>;
serializeReturn: Serializer<any>;
returnTypeBaseSize: number;
}> = [];
export type UntypedProcedureDef = {
name: string;
accessorName: string;
params: CoerceParams<ParamsObj>;
returnType: TypeBuilder<any, any>;
};
export type UntypedProceduresDef = {
procedures: readonly UntypedProcedureDef[];
};
export function procedures<const H extends readonly UntypedProcedureDef[]>(
...handles: H
): { procedures: H };
export function procedures<const H extends readonly UntypedProcedureDef[]>(
handles: H
): { procedures: H };
export function procedures<const H extends readonly UntypedProcedureDef[]>(
...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<any, any>,
> = {
name: Name;
accessorName: CamelCase<Name>;
params: CoerceParams<Params>;
returnType: ReturnType;
};
export function procedureSchema<
ProcedureName extends string,
Params extends ParamsObj,
ReturnType extends TypeBuilder<any, any>,
>(
name: ProcedureName,
params: Params,
returnType: ReturnType
): ProcedureDef<ProcedureName, Params, ReturnType> {
return {
name,
accessorName: toCamelCase(name),
params: coerceParams(params),
returnType,
};
}
+6 -280
View File
@@ -1,30 +1,15 @@
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 { Random } from '../server/rng';
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';
import { Uuid } from './uuid.ts';
import type { Random } from '../server/rng';
/**
* Helper to extract the parameter types from an object type
@@ -37,7 +22,8 @@ export type ParamsObj = Record<
/**
* Helper to convert a ParamsObj or RowObj into an object type
*/
type ParamsAsObject<ParamDef extends ParamsObj> = InferTypeOfRow<ParamDef>;
export type ParamsAsObject<ParamDef extends ParamsObj> =
InferTypeOfRow<ParamDef>;
/**
* Defines a SpacetimeDB reducer function.
@@ -68,7 +54,7 @@ type ParamsAsObject<ParamDef extends ParamsObj> = InferTypeOfRow<ParamDef>;
export type Reducer<S extends UntypedSchemaDef, Params extends ParamsObj> = (
ctx: ReducerCtx<S>,
payload: ParamsAsObject<Params>
) => void | { tag: 'ok' } | { tag: 'err'; value: string };
) => void;
/**
* Authentication information for the caller of a reducer.
@@ -126,263 +112,3 @@ export type ReducerCtx<SchemaDef extends UntypedSchemaDef> = Readonly<{
newUuidV7(): Uuid;
random: Random;
}>;
/**
* 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<RowObj>,
fn: Reducer<any, any>,
lifecycle?: Infer<typeof RawReducerDefV9>['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<string>();
export const REDUCERS: Reducer<any, any>[] = [];
/**
* 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<S>, payload: ParamsAsObject<Params>) => 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<S extends UntypedSchemaDef, Params extends ParamsObj>(
name: string,
params: Params,
fn: (ctx: ReducerCtx<S>, payload: ParamsAsObject<Params>) => 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<S extends UntypedSchemaDef, Params extends ParamsObj>(
name: string,
params: Params,
fn: Reducer<S, Params>
): 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<S, Params>): 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<S, Params>): void {
pushReducer(name, params, fn, Lifecycle.OnDisconnect);
}
class Reducers<ReducersDef extends UntypedReducersDef> {
reducersType: ReducersDef;
constructor(handles: readonly ReducerSchema<any, any>[]) {
this.reducersType = reducersToSchema(handles) as ReducersDef;
}
}
/**
* Helper type to convert an array of TableSchema into a schema definition
*/
type ReducersToSchema<T extends readonly ReducerSchema<any, any>[]> = {
reducers: {
/** @type {UntypedReducerDef} */
readonly [i in keyof T]: {
name: T[i]['reducerName'];
accessorName: CamelCase<T[i]['accessorName']>;
params: T[i]['params']['row'];
paramsType: T[i]['paramsSpacetimeType'];
};
};
};
export function reducersToSchema<
const T extends readonly ReducerSchema<any, any>[],
>(reducers: T): ReducersToSchema<T> {
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<T>;
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<const H extends readonly ReducerSchema<any, any>[]>(
...handles: H
): Reducers<ReducersToSchema<H>>;
/**
* 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<const H extends readonly ReducerSchema<any, any>[]>(
handles: H
): Reducers<ReducersToSchema<H>>;
export function reducers<const H extends readonly ReducerSchema<any, any>[]>(
...args: [H] | H
): Reducers<ReducersToSchema<H>> {
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<ReducerName, Params> {
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>(params),
paramsSpacetimeType: paramType,
reducerDef: {
name,
params: paramType,
lifecycle: undefined,
},
};
}
+171 -558
View File
@@ -1,12 +1,21 @@
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,
ResultBuilder,
RowBuilder,
SumBuilder,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -16,38 +25,9 @@ import {
type InferSpacetimeTypeOfTypeBuilder,
type RowObj,
type VariantsObj,
ResultBuilder,
} 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 extends UntypedSchemaDef> =
S['tables'][number]['name'];
@@ -59,23 +39,15 @@ 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<T extends readonly UntypedTableSchema[]> = {
export interface TablesToSchema<T extends readonly UntypedTableSchema[]>
extends UntypedSchemaDef {
tables: {
readonly [i in keyof T]: TableToSchema<T[i]>;
};
};
}
export interface TableToSchema<T extends UntypedTableSchema>
extends UntypedTableDef {
@@ -88,16 +60,23 @@ export interface TableToSchema<T extends UntypedTableSchema>
}
export function tablesToSchema<const T extends readonly UntypedTableSchema[]>(
ctx: ModuleContext,
tables: T
): TablesToSchema<T> {
return { tables: tables.map(tableToSchema) as TablesToSchema<T>['tables'] };
return {
tables: tables.map(schema =>
tableToSchema(ctx, schema)
) as TablesToSchema<T>['tables'],
};
}
function tableToSchema<T extends UntypedTableSchema>(
ctx: ModuleContext,
schema: T
): TableToSchema<T> {
const getColName = (i: number) =>
schema.rowType.algebraicType.value.elements[i].name;
const tableDef = schema.tableDef(ctx);
type AllowedCol = keyof T['rowType']['row'] & string;
return {
@@ -105,7 +84,7 @@ function tableToSchema<T extends UntypedTableSchema>(
accessorName: toCamelCase(schema.tableName as T['tableName']),
columns: schema.rowType.row, // typed as T[i]['rowType']['row'] under TablesToSchema<T>
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],
@@ -114,14 +93,14 @@ function tableToSchema<T extends UntypedTableSchema>(
// by casting it to an `Array<IndexOpts>` 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<AllowedCol> => {
indexes: tableDef.indexes.map((idx): UntypedIndex<AllowedCol> => {
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',
@@ -131,145 +110,159 @@ function tableToSchema<T extends UntypedTableSchema>(
};
}
/**
* The global module definition that gets populated by calls to `reducer()` and lifecycle hooks.
*/
export const MODULE_DEF: Infer<typeof RawModuleDefV9> = {
typespace: { types: [] },
tables: [],
reducers: [],
types: [],
miscExports: [],
rowLevelSecurity: [],
};
const COMPOUND_TYPES = new Map<
type CompoundTypeCache = Map<
AlgebraicTypeVariants.Product | AlgebraicTypeVariants.Sum,
RefBuilder<any, any>
>();
>;
/**
* 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<AT extends AlgebraicTypeType>(
typespace: Infer<typeof Typespace>,
typeBuilder: RefBuilder<any, AT>
): AT {
let ty: AlgebraicType = typeBuilder.algebraicType;
while (ty.tag === 'Ref') {
ty = typespace.types[ty.value];
}
return ty as AT;
}
type ModuleDef = Infer<typeof RawModuleDefV9>;
/**
* 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<any, AlgebraicType>,
>(
typeBuilder: T
): T extends SumBuilder<any> | ProductBuilder<any> | RowBuilder<any>
? RefBuilder<Infer<T>, InferSpacetimeTypeOfTypeBuilder<T>>
: 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 ResultBuilder) {
return new ResultBuilder(
registerTypesRecursively(typeBuilder.ok),
registerTypesRecursively(typeBuilder.err)
) 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<VariantsObj>
| ProductBuilder<ElementsObj>
| RowBuilder<RowObj>,
>(typeBuilder: T): RefBuilder<Infer<T>, InferSpacetimeTypeOfTypeBuilder<T>> {
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
get typespace() {
return this.#moduleDef.typespace;
}
/**
* 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<AT extends AlgebraicTypeType>(
typeBuilder: RefBuilder<any, AT>
): AT {
let ty: AlgebraicType = typeBuilder.algebraicType;
while (ty.tag === 'Ref') {
ty = this.typespace.types[ty.value];
}
return ty as AT;
}
/**
* 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<T extends TypeBuilder<any, AlgebraicType>>(
typeBuilder: T
): T extends SumBuilder<any> | ProductBuilder<any> | RowBuilder<any>
? RefBuilder<Infer<T>, InferSpacetimeTypeOfTypeBuilder<T>>
: 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 ResultBuilder) {
return new ResultBuilder(
this.registerTypesRecursively(typeBuilder.ok),
this.registerTypesRecursively(typeBuilder.err)
) as any;
} else if (typeBuilder instanceof ArrayBuilder) {
return new ArrayBuilder(
this.registerTypesRecursively(typeBuilder.element)
) as any;
} else {
return typeBuilder as any;
}
}
#registerCompoundTypeRecursively<
T extends
| SumBuilder<VariantsObj>
| ProductBuilder<ElementsObj>
| RowBuilder<RowObj>,
>(typeBuilder: T): RefBuilder<Infer<T>, InferSpacetimeTypeOfTypeBuilder<T>> {
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)}`
);
}
let r = this.#compoundTypes.get(ty);
if (r != null) {
// Already added to typespace
return r;
}
// 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,
});
}
}
this.#moduleDef.types.push({
name: splitName(name),
ty: r.ref,
customOrdering: true,
});
return r;
}
// 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);
COMPOUND_TYPES.set(ty, r);
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,
});
}
} 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,
});
}
} 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,
});
}
}
MODULE_DEF.types.push({
name: splitName(name),
ty: r.ref,
customOrdering: true,
});
return r;
}
function isUnit(typeBuilder: ProductBuilder<ElementsObj>): boolean {
@@ -283,383 +276,3 @@ export function splitName(name: string): Infer<typeof RawScopedTypeNameV9> {
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<S extends UntypedSchemaDef> {
readonly tablesDef: { tables: Infer<typeof RawTableDefV9>[] };
readonly typespace: Infer<typeof Typespace>;
readonly schemaType: S;
constructor(
tables: Infer<typeof RawTableDefV9>[],
typespace: Infer<typeof Typespace>,
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<S>, payload: ParamsAsObject<Params>) => 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<Params extends ParamsObj>(
name: string,
params: Params,
fn: Reducer<S, Params>
): Reducer<S, Params>;
reducer(name: string, fn: Reducer<S, {}>): Reducer<S, {}>;
reducer<Params extends ParamsObj>(
name: string,
paramsOrFn: Params | Reducer<S, any>,
fn?: Reducer<S, Params>
): Reducer<S, Params> {
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<S, {}>} 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<S, {}>): void;
init(name: string, fn: Reducer<S, {}>): void;
init(nameOrFn: any, maybeFn?: Reducer<S, {}>): 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<S, {}>): void;
clientConnected(name: string, fn: Reducer<S, {}>): void;
clientConnected(nameOrFn: any, maybeFn?: Reducer<S, {}>): 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<S, {}>): void;
clientDisconnected(name: string, fn: Reducer<S, {}>): void;
clientDisconnected(nameOrFn: any, maybeFn?: Reducer<S, {}>): void {
const [name, fn] =
typeof nameOrFn === 'string'
? [nameOrFn, maybeFn]
: ['on_disconnect', nameOrFn];
clientDisconnected(name, {}, fn);
}
view<Ret extends ViewReturnTypeBuilder>(
opts: ViewOpts,
ret: Ret,
fn: ViewFn<S, {}, Ret>
): void {
defineView(opts, false, {}, ret, fn);
}
// TODO: re-enable once parameterized views are supported in SQL
// view<Ret extends ViewReturnTypeBuilder>(
// opts: ViewOpts,
// ret: Ret,
// fn: ViewFn<S, {}, Ret>
// ): void;
// view<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
// opts: ViewOpts,
// params: Params,
// ret: Ret,
// fn: ViewFn<S, {}, Ret>
// ): void;
// view<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
// opts: ViewOpts,
// paramsOrRet: Ret | Params,
// retOrFn: ViewFn<S, {}, Ret> | Ret,
// maybeFn?: ViewFn<S, Params, Ret>
// ): void {
// if (typeof retOrFn === 'function') {
// defineView(name, false, {}, paramsOrRet as Ret, retOrFn);
// } else {
// defineView(name, false, paramsOrRet as Params, retOrFn, maybeFn!);
// }
// }
anonymousView<Ret extends ViewReturnTypeBuilder>(
opts: ViewOpts,
ret: Ret,
fn: AnonymousViewFn<S, {}, Ret>
): void {
defineView(opts, true, {}, ret, fn);
}
// TODO: re-enable once parameterized views are supported in SQL
// anonymousView<Ret extends ViewReturnTypeBuilder>(
// opts: ViewOpts,
// ret: Ret,
// fn: AnonymousViewFn<S, {}, Ret>
// ): void;
// anonymousView<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
// opts: ViewOpts,
// params: Params,
// ret: Ret,
// fn: AnonymousViewFn<S, {}, Ret>
// ): void;
// anonymousView<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
// opts: ViewOpts,
// paramsOrRet: Ret | Params,
// retOrFn: AnonymousViewFn<S, {}, Ret> | Ret,
// maybeFn?: AnonymousViewFn<S, Params, Ret>
// ): void {
// if (typeof retOrFn === 'function') {
// defineView(name, true, {}, paramsOrRet as Ret, retOrFn);
// } else {
// defineView(name, true, paramsOrRet as Params, retOrFn, maybeFn!);
// }
// }
procedure<Params extends ParamsObj, Ret extends TypeBuilder<any, any>>(
name: string,
params: Params,
ret: Ret,
fn: ProcedureFn<S, Params, Ret>
): ProcedureFn<S, Params, Ret>;
procedure<Ret extends TypeBuilder<any, any>>(
name: string,
ret: Ret,
fn: ProcedureFn<S, {}, Ret>
): ProcedureFn<S, {}, Ret>;
procedure<Params extends ParamsObj, Ret extends TypeBuilder<any, any>>(
name: string,
paramsOrRet: Ret | Params,
retOrFn: ProcedureFn<S, {}, Ret> | Ret,
maybeFn?: ProcedureFn<S, Params, Ret>
): ProcedureFn<S, Params, Ret> {
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<any>> =
SchemaDef extends Schema<infer S> ? 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<const H extends readonly UntypedTableSchema[]>(
...handles: H
): Schema<TablesToSchema<H>>;
/**
* 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<const H extends readonly UntypedTableSchema[]>(
handles: H
): Schema<TablesToSchema<H>>;
/**
* 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<const H extends readonly UntypedTableSchema[]>(
...args: [H] | H
): Schema<TablesToSchema<H>> {
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<TableDefs extends readonly HasAccessor[]> = {
[Tbl in TableDefs[number] as Tbl['accessorName']]: Tbl;
};
export function convertToAccessorMap<T extends readonly HasAccessor[]>(
arr: T
): ConvertToAccessorMap<T> {
return Object.fromEntries(
arr.map(v => [v.accessorName, v])
) as ConvertToAccessorMap<T>;
}
+8 -9
View File
@@ -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,
@@ -24,9 +25,9 @@ import {
type TypeBuilder,
} from './type_builders';
import type {
InvalidColumnMetadata,
Prettify,
ValidateColumnMetadata,
InvalidColumnMetadata,
} from './type_util';
import { toPascalCase } from './util';
@@ -218,8 +219,8 @@ export interface TableMethods<TableDef extends UntypedTableDef>
* 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<RowType<TableDef>>): Prettify<RowType<TableDef>>;
@@ -300,8 +301,6 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
row.typeName = toPascalCase(name);
}
const rowTypeRef = registerTypesRecursively(row);
row.algebraicType.value.elements.forEach((elem, i) => {
colIds.set(elem.name, i);
colNameList.push(elem.name);
@@ -419,9 +418,9 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
// Temporarily set the type ref to 0. We will set this later
// in the schema function.
const tableDef: Infer<typeof RawTableDefV9> = {
const tableDef = (ctx: ModuleContext): Infer<typeof RawTableDefV9> => ({
name,
productTypeRef: rowTypeRef.ref,
productTypeRef: ctx.registerTypesRecursively(row).ref,
primaryKey: pk,
indexes,
constraints,
@@ -436,7 +435,7 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
: undefined,
tableType: { tag: 'User' },
tableAccess: { tag: isPublic ? 'Public' : 'Private' },
};
});
const productType = row.algebraicType.value as RowBuilder<
CoerceRow<Row>
@@ -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<typeof RawTableDefV9>;
tableDef(ctx: ModuleContext): Infer<typeof RawTableDefV9>;
/**
* The indexes defined on the table.
@@ -3874,7 +3874,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.
*/
@@ -4002,10 +4002,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();
@@ -198,7 +198,7 @@ export class DbConnectionBuilder<DbConnection extends DbConnectionImpl<any>> {
/**
* 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.
*
+3 -3
View File
@@ -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';
@@ -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<RemoteModule> = IfAny<
}
: never
>;
export type UntypedProcedureDef = {
name: string;
accessorName: string;
params: CoerceParams<ParamsObj>;
returnType: TypeBuilder<any, any>;
};
export type UntypedProceduresDef = {
procedures: readonly UntypedProcedureDef[];
};
export function procedures<const H extends readonly UntypedProcedureDef[]>(
...handles: H
): { procedures: H };
export function procedures<const H extends readonly UntypedProcedureDef[]>(
handles: H
): { procedures: H };
export function procedures<const H extends readonly UntypedProcedureDef[]>(
...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<any, any>,
> = {
name: Name;
accessorName: CamelCase<Name>;
params: CoerceParams<Params>;
returnType: ReturnType;
};
export function procedureSchema<
ProcedureName extends string,
Params extends ParamsObj,
ReturnType extends TypeBuilder<any, any>,
>(
name: ProcedureName,
params: Params,
returnType: ReturnType
): ProcedureDef<ProcedureName, Params, ReturnType> {
return {
name,
accessorName: toCamelCase(name),
params: coerceParams(params),
returnType,
};
}
+111 -2
View File
@@ -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<R extends UntypedReducersDef> = {
flags: CallReducerFlags
) => void;
};
class Reducers<ReducersDef extends UntypedReducersDef> {
reducersType: ReducersDef;
constructor(handles: readonly ReducerSchema<any, any>[]) {
this.reducersType = reducersToSchema(handles) as ReducersDef;
}
}
/**
* Helper type to convert an array of TableSchema into a schema definition
*/
type ReducersToSchema<T extends readonly ReducerSchema<any, any>[]> = {
reducers: {
/** @type {UntypedReducerDef} */
readonly [i in keyof T]: {
name: T[i]['reducerName'];
accessorName: CamelCase<T[i]['accessorName']>;
params: T[i]['params']['row'];
paramsType: T[i]['paramsSpacetimeType'];
};
};
};
export function reducersToSchema<
const T extends readonly ReducerSchema<any, any>[],
>(reducers: T): ReducersToSchema<T> {
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<T>;
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<const H extends readonly ReducerSchema<any, any>[]>(
...handles: H
): Reducers<ReducersToSchema<H>>;
/**
* 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<const H extends readonly ReducerSchema<any, any>[]>(
handles: H
): Reducers<ReducersToSchema<H>>;
export function reducers<const H extends readonly ReducerSchema<any, any>[]>(
...args: [H] | H
): Reducers<ReducersToSchema<H>> {
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<ReducerName, Params> {
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>(params),
paramsSpacetimeType: paramType,
reducerDef: {
name,
params: paramType,
lifecycle: undefined,
},
};
}
@@ -0,0 +1,74 @@
import {
ModuleContext,
tablesToSchema,
type TablesToSchema,
type UntypedSchemaDef,
} from '../lib/schema';
import type { UntypedTableSchema } from '../lib/table_schema';
class Tables<S extends UntypedSchemaDef> {
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<const H extends readonly UntypedTableSchema[]>(
...handles: H
): Tables<TablesToSchema<H>>;
/**
* 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<const H extends readonly UntypedTableSchema[]>(
handles: H
): Tables<TablesToSchema<H>>;
/**
* 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<const H extends readonly UntypedTableSchema[]>(
...args: [H] | H
): Tables<TablesToSchema<H>> {
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<TableDefs extends readonly HasAccessor[]> = {
[Tbl in TableDefs[number] as Tbl['accessorName']]: Tbl;
};
export function convertToAccessorMap<T extends readonly HasAccessor[]>(
arr: T
): ConvertToAccessorMap<T> {
return Object.fromEntries(
arr.map(v => [v.accessorName, v])
) as ConvertToAccessorMap<T>;
}
@@ -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';
@@ -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 * from './query';
export type { ProcedureCtx, TransactionCtx } from '../lib/procedures';
export type { ProcedureCtx, TransactionCtx } from './procedures';
export { toCamelCase } from '../lib/util';
export { type Uuid } from '../lib/uuid';
export { type Random } from './rng';
@@ -1,21 +1,102 @@
import {
AlgebraicType,
ProductType,
type Deserializer,
type Serializer,
} from '../lib/algebraic_type';
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 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 { Uuid } from '../lib/uuid';
import type { HttpClient } from '../server/http_internal';
import { httpClient } from './http_internal';
import { callUserFunction, ReducerCtxImpl, sys } from './runtime';
import type { SchemaInner } from './schema';
const { freeze } = Object;
export type ProcedureFn<
S extends UntypedSchemaDef,
Params extends ParamsObj,
Ret extends TypeBuilder<any, any>,
> = (ctx: ProcedureCtx<S>, args: InferTypeOfRow<Params>) => Infer<Ret>;
export interface ProcedureCtx<S extends UntypedSchemaDef> {
readonly sender: Identity;
readonly identity: Identity;
readonly timestamp: Timestamp;
readonly connectionId: ConnectionId | null;
readonly http: HttpClient;
readonly counter_uuid: { value: number };
withTx<T>(body: (ctx: TransactionCtx<S>) => T): T;
newUuidV4(): Uuid;
newUuidV7(): Uuid;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface TransactionCtx<S extends UntypedSchemaDef>
extends ReducerCtx<S> {}
export function procedure<
S extends UntypedSchemaDef,
Params extends ParamsObj,
Ret extends TypeBuilder<any, any>,
>(
ctx: SchemaInner,
name: string,
params: Params,
ret: Ret,
fn: ProcedureFn<S, Params, Ret>
) {
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,
},
});
const { typespace } = ctx;
ctx.procedures.push({
fn,
deserializeArgs: ProductType.makeDeserializer(paramsType, typespace),
serializeReturn: AlgebraicType.makeSerializer(returnType, typespace),
returnTypeBaseSize: bsatnBaseSize(typespace, returnType),
});
}
export type Procedures = Array<{
fn: ProcedureFn<any, any, any>;
deserializeArgs: Deserializer<any>;
serializeReturn: Serializer<any>;
returnTypeBaseSize: number;
}>;
export function callProcedure(
moduleCtx: SchemaInner,
id: number,
sender: Identity,
connectionId: ConnectionId | null,
@@ -23,7 +104,7 @@ export function callProcedure(
argsBuf: Uint8Array
): Uint8Array {
const { fn, deserializeArgs, serializeReturn, returnTypeBaseSize } =
PROCEDURES[id];
moduleCtx.procedures[id];
const args = deserializeArgs(new BinaryReader(argsBuf));
const ctx: ProcedureCtx<UntypedSchemaDef> = {
@@ -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<RowObj>,
fn: Reducer<any, any>,
lifecycle?: Infer<typeof RawReducerDefV9>['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<any, any>[];
/**
* 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<S>, payload: ParamsAsObject<Params>) => 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<S extends UntypedSchemaDef, Params extends ParamsObj>(
ctx: SchemaInner,
name: string,
params: Params,
fn: Reducer<S, Params>
): 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<S extends UntypedSchemaDef, Params extends ParamsObj>(
ctx: SchemaInner,
name: string,
params: Params,
fn: Reducer<S, Params>
): 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<S, Params>
): 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<S, Params>
): void {
pushReducer(ctx, name, params, fn, Lifecycle.OnDisconnect);
}
@@ -24,35 +24,26 @@ 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,
type ReducerCtx as IReducerCtx,
} 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 type { Infer } from '../lib/type_builders';
import { bsatnBaseSize, hasOwn, 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 { makeRandom, type Random } from './rng';
import { getRegisteredSchema } from './schema';
const { freeze } = Object;
@@ -271,13 +262,18 @@ let reducerArgsDeserializers: Deserializer<any>[];
export const hooks: ModuleHooks = {
__describe_module__() {
const writer = new BinaryWriter(128);
RawModuleDef.serialize(writer, RawModuleDef.V9(MODULE_DEF));
RawModuleDef.serialize(
writer,
RawModuleDef.V9(getRegisteredSchema().moduleDef)
);
return writer.getBuffer();
},
__call_reducer__(reducerId, sender, connId, timestamp, argsBuf) {
const moduleCtx = getRegisteredSchema();
if (reducerArgsDeserializers == null) {
reducerArgsDeserializers = MODULE_DEF.reducers.map(({ params }) =>
ProductType.makeDeserializer(params, MODULE_DEF.typespace)
reducerArgsDeserializers = moduleCtx.moduleDef.reducers.map(
({ params }) =>
ProductType.makeDeserializer(params, moduleCtx.typespace)
);
}
const deserializeArgs = reducerArgsDeserializers[reducerId];
@@ -289,7 +285,11 @@ export const hooks: ModuleHooks = {
ConnectionId.nullIfZero(new ConnectionId(connId))
);
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 };
@@ -301,15 +301,16 @@ export const hooks: ModuleHooks = {
export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = {
__call_view__(id, sender, argsBuf) {
const moduleCtx = getRegisteredSchema();
const { fn, deserializeParams, serializeReturn, returnTypeBaseSize } =
VIEWS[id];
moduleCtx.views[id];
const ctx: ViewCtx<any> = 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),
});
const args = deserializeParams(new BinaryReader(argsBuf));
const ret = callUserFunction(fn, ctx, args);
@@ -324,14 +325,15 @@ export const hooks_v1_1: import('spacetime:sys@1.1').ModuleHooks = {
return { data: retBuf.getBuffer() };
},
__call_view_anon__(id, argsBuf) {
const moduleCtx = getRegisteredSchema();
const { fn, deserializeParams, serializeReturn, returnTypeBaseSize } =
ANON_VIEWS[id];
moduleCtx.anonViews[id];
const ctx: AnonymousViewCtx<any> = 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 = deserializeParams(new BinaryReader(argsBuf));
const ret = callUserFunction(fn, ctx, args);
@@ -350,6 +352,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)),
@@ -361,7 +364,7 @@ export const hooks_v1_2: import('spacetime:sys@1.2').ModuleHooks = {
let DB_VIEW: DbView<any> | null = null;
function getDbView() {
DB_VIEW ??= makeDbView(MODULE_DEF);
DB_VIEW ??= makeDbView(getRegisteredSchema().moduleDef);
return DB_VIEW;
}
@@ -1,4 +1,4 @@
import { schema } from '../lib/schema';
import { schema } from './schema';
import { table } from '../lib/table';
import t from '../lib/type_builders';
@@ -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<string>();
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<S extends UntypedSchemaDef> {
#ctx: SchemaInner<S>;
constructor(ctx: SchemaInner<S>) {
// 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<S>, payload: ParamsAsObject<Params>) => 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<Params extends ParamsObj>(
name: string,
params: Params,
fn: Reducer<S, Params>
): Reducer<S, Params>;
reducer(name: string, fn: Reducer<S, {}>): Reducer<S, {}>;
reducer<Params extends ParamsObj>(
name: string,
paramsOrFn: Params | Reducer<S, any>,
fn?: Reducer<S, Params>
): Reducer<S, Params> {
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<S, {}>} 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<S, {}>): void;
init(name: string, fn: Reducer<S, {}>): void;
init(nameOrFn: any, maybeFn?: Reducer<S, {}>): 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<S, {}>): void;
clientConnected(name: string, fn: Reducer<S, {}>): void;
clientConnected(nameOrFn: any, maybeFn?: Reducer<S, {}>): 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<S, {}>): void;
clientDisconnected(name: string, fn: Reducer<S, {}>): void;
clientDisconnected(nameOrFn: any, maybeFn?: Reducer<S, {}>): void {
const [name, fn] =
typeof nameOrFn === 'string'
? [nameOrFn, maybeFn]
: ['on_disconnect', nameOrFn];
clientDisconnected(this.#ctx, name, {}, fn);
}
view<Ret extends ViewReturnTypeBuilder>(
opts: ViewOpts,
ret: Ret,
fn: ViewFn<S, {}, Ret>
): void {
defineView(this.#ctx, opts, false, {}, ret, fn);
}
// TODO: re-enable once parameterized views are supported in SQL
// view<Ret extends ViewReturnTypeBuilder>(
// opts: ViewOpts,
// ret: Ret,
// fn: ViewFn<S, {}, Ret>
// ): void;
// view<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
// opts: ViewOpts,
// params: Params,
// ret: Ret,
// fn: ViewFn<S, {}, Ret>
// ): void;
// view<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
// opts: ViewOpts,
// paramsOrRet: Ret | Params,
// retOrFn: ViewFn<S, {}, Ret> | Ret,
// maybeFn?: ViewFn<S, Params, Ret>
// ): void {
// if (typeof retOrFn === 'function') {
// defineView(name, false, {}, paramsOrRet as Ret, retOrFn);
// } else {
// defineView(name, false, paramsOrRet as Params, retOrFn, maybeFn!);
// }
// }
anonymousView<Ret extends ViewReturnTypeBuilder>(
opts: ViewOpts,
ret: Ret,
fn: AnonymousViewFn<S, {}, Ret>
): void {
defineView(this.#ctx, opts, true, {}, ret, fn);
}
// TODO: re-enable once parameterized views are supported in SQL
// anonymousView<Ret extends ViewReturnTypeBuilder>(
// opts: ViewOpts,
// ret: Ret,
// fn: AnonymousViewFn<S, {}, Ret>
// ): void;
// anonymousView<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
// opts: ViewOpts,
// params: Params,
// ret: Ret,
// fn: AnonymousViewFn<S, {}, Ret>
// ): void;
// anonymousView<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
// opts: ViewOpts,
// paramsOrRet: Ret | Params,
// retOrFn: AnonymousViewFn<S, {}, Ret> | Ret,
// maybeFn?: AnonymousViewFn<S, Params, Ret>
// ): void {
// if (typeof retOrFn === 'function') {
// defineView(name, true, {}, paramsOrRet as Ret, retOrFn);
// } else {
// defineView(name, true, paramsOrRet as Params, retOrFn, maybeFn!);
// }
// }
procedure<Params extends ParamsObj, Ret extends TypeBuilder<any, any>>(
name: string,
params: Params,
ret: Ret,
fn: ProcedureFn<S, Params, Ret>
): ProcedureFn<S, Params, Ret>;
procedure<Ret extends TypeBuilder<any, any>>(
name: string,
ret: Ret,
fn: ProcedureFn<S, {}, Ret>
): ProcedureFn<S, {}, Ret>;
procedure<Params extends ParamsObj, Ret extends TypeBuilder<any, any>>(
name: string,
paramsOrRet: Ret | Params,
retOrFn: ProcedureFn<S, {}, Ret> | Ret,
maybeFn?: ProcedureFn<S, Params, Ret>
): ProcedureFn<S, Params, Ret> {
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<any>> =
SchemaDef extends Schema<infer S> ? 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<const H extends readonly UntypedTableSchema[]>(
...handles: H
): Schema<TablesToSchema<H>>;
/**
* 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<const H extends readonly UntypedTableSchema[]>(
handles: H
): Schema<TablesToSchema<H>>;
/**
* 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<const H extends readonly UntypedTableSchema[]>(
...args: [H] | H
): Schema<TablesToSchema<H>> {
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);
}
@@ -1,4 +1,4 @@
import { schema } from '../lib/schema';
import { schema } from './schema';
import { table } from '../lib/table';
import t from '../lib/type_builders';
@@ -7,23 +7,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 './query';
import type { SchemaInner } from './schema';
export type ViewCtx<S extends UntypedSchemaDef> = Readonly<{
sender: Identity;
@@ -90,6 +86,7 @@ export function defineView<
Params extends ParamsObj,
Ret extends ViewReturnTypeBuilder,
>(
ctx: SchemaInner,
opts: ViewOpts,
anon: Anonymous,
params: Params,
@@ -101,20 +98,19 @@ 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 { typespace } = MODULE_DEF;
const { typespace } = ctx;
const { value: paramType } = resolveType(
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,
@@ -134,7 +130,7 @@ export function defineView<
);
}
(anon ? ANON_VIEWS : VIEWS).push({
(anon ? ctx.anonViews : ctx.views).push({
fn,
deserializeParams: ProductType.makeDeserializer(paramType, typespace),
serializeReturn: AlgebraicType.makeSerializer(returnType, typespace),
@@ -149,8 +145,8 @@ type ViewInfo<F> = {
returnTypeBaseSize: number;
};
export const VIEWS: ViewInfo<ViewFn<any, any, any>>[] = [];
export const ANON_VIEWS: ViewInfo<AnonymousViewFn<any, any, any>>[] = [];
export type Views = ViewInfo<ViewFn<any, any, any>>[];
export type AnonViews = ViewInfo<AnonymousViewFn<any, any, any>>[];
// A helper to get the product type out of a type builder.
// This is only non-never if the type builder is an array.
+32 -14
View File
@@ -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' },
],
},
}
);
);
+11 -6
View File
@@ -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/test-app run format && pnpm -r --filter './templates/**' run format",
"lint": "pnpm --filter ./crates/bindings-typescript run lint && pnpm --filter ./docs run lint && pnpm --filter ./crates/bindings-typescript/test-app run lint && pnpm -r --filter './templates/**' run lint",
"build": "pnpm --filter ./crates/bindings-typescript run build && pnpm --filter ./docs run build && pnpm --filter ./crates/bindings-typescript/test-app run build && pnpm -r --filter './templates/**' 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/test-app run generate && pnpm -r --filter './templates/**' run generate",
"run-all": "pnpm -F ./crates/bindings-typescript -F ./crates/bindings-typescript/examples/quickstart-chat -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",
+142
View File
@@ -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))
@@ -108,6 +111,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
@@ -1714,6 +1720,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'}
@@ -3104,6 +3118,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'}
@@ -3537,6 +3555,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}
@@ -4254,6 +4276,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==}
@@ -4630,6 +4656,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==}
@@ -5154,6 +5184,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'}
@@ -5696,6 +5732,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==}
@@ -6099,6 +6138,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'}
@@ -6805,6 +6848,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'}
@@ -6942,6 +6988,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'}
@@ -6949,6 +6998,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==}
@@ -7880,6 +7932,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==}
@@ -8000,6 +8056,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'}
@@ -8138,6 +8199,15 @@ packages:
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
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==}
@@ -8390,6 +8460,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'}
@@ -11515,6 +11589,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
@@ -13077,6 +13161,8 @@ snapshots:
'@sinclair/typebox@0.27.8': {}
'@sindresorhus/base62@1.0.0': {}
'@sindresorhus/is@4.6.0': {}
'@sindresorhus/is@5.6.0': {}
@@ -13655,6 +13741,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)
@@ -15035,6 +15123,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: {}
@@ -15440,6 +15530,8 @@ snapshots:
commander@8.3.0: {}
comment-parser@1.4.1: {}
common-path-prefix@3.0.0: {}
compressible@2.0.18:
@@ -15963,6 +16055,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)
@@ -16696,6 +16808,8 @@ snapshots:
dependencies:
whatwg-encoding: 3.1.1
html-entities@2.6.0: {}
html-escaper@2.0.2: {}
html-minifier-terser@6.1.0:
@@ -17072,6 +17186,8 @@ snapshots:
dependencies:
argparse: 2.0.1
jsdoc-type-pratt-parser@6.10.0: {}
jsdom@26.1.0:
dependencies:
cssstyle: 4.6.0
@@ -18102,6 +18218,8 @@ snapshots:
object-assign@4.1.1: {}
object-deep-merge@2.0.0: {}
object-inspect@1.13.4: {}
object-keys@1.1.1: {}
@@ -18261,6 +18379,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
@@ -18270,6 +18392,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
@@ -19316,6 +19440,8 @@ snapshots:
requires-port@1.0.0: {}
reserved-identifiers@1.2.0: {}
resolve-alpn@1.2.1: {}
resolve-from@4.0.0: {}
@@ -19445,6 +19571,8 @@ snapshots:
semver@7.7.2: {}
semver@7.7.3: {}
send@0.19.0:
dependencies:
debug: 2.6.9
@@ -19633,6 +19761,15 @@ snapshots:
space-separated-tokens@2.0.2: {}
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
@@ -19896,6 +20033,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: {}