Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions crates/bindings-typescript/src/lib/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
UntypedIndex,
} from './indexes';
import ScheduleAt from './schedule_at';
import type { TableSchema } from './table_schema';
import type { TableSchema, TableSchedule } from './table_schema';
import {
RowBuilder,
type ColumnBuilder,
Expand Down Expand Up @@ -194,6 +194,10 @@ export type TableOpts<Row extends RowObj> = {
public?: boolean;
indexes?: IndexOpts<keyof Row & string>[]; // declarative multi‑column indexes
constraints?: ConstraintOpts<keyof Row & string>[];
/**
* @deprecated Prefer `spacetime.schedule(table, reducerOrProcedure)` so table
* definitions can live in a separate module from reducer/procedure definitions.
*/
scheduled?: () =>
| ReducerExport<any, { [k: string]: RowBuilder<RowObj> }>
| ProcedureExport<
Expand Down Expand Up @@ -422,11 +426,9 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
}

// If this column is shaped like ScheduleAtAlgebraicType, mark it as the schedule‑at column
if (scheduled) {
const algebraicType = builder.typeBuilder.algebraicType;
if (ScheduleAt.isScheduleAt(algebraicType)) {
scheduleAtCol = colIds.get(name)!;
}
const algebraicType = builder.typeBuilder.algebraicType;
if (ScheduleAt.isScheduleAt(algebraicType)) {
scheduleAtCol = colIds.get(name)!;
}
}

Expand Down Expand Up @@ -492,9 +494,9 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
CoerceRow<Row>
>['algebraicType']['value'];

const schedule =
const schedule: TableSchedule | undefined =
scheduled && scheduleAtCol !== undefined
? { scheduleAtCol, reducer: scheduled }
? { reducer: scheduled }
: undefined;

return {
Expand Down Expand Up @@ -543,6 +545,7 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
// can expose them without type-smuggling.
idxs: userIndexes as OptsIndices<Opts>,
constraints: constraints as OptsConstraints<Opts>,
scheduleAtCol,
schedule,
};
}
32 changes: 26 additions & 6 deletions crates/bindings-typescript/src/lib/table_schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import type { ProcedureExport, ReducerExport } from '../server';
import type { ProductType } from './algebraic_type';
import type { RawScheduleDefV10, RawTableDefV10 } from './autogen/types';
import type { IndexOpts } from './indexes';
import type { ModuleContext } from './schema';
import type { ColumnBuilder, RowBuilder } from './type_builders';
import type { ProcedureExport, ReducerExport } from '../server';

/**
* Internal erased form of a scheduled reducer/procedure export.
*
* The legacy `TableOpts.scheduled` option checks the scheduled function shape
* before it reaches `TableSchema`. From here, schedule resolution only needs
* the export object identity to look up its registered function name.
*/
export type UntypedScheduledFunctionExport =
| ReducerExport<any, any>
| ProcedureExport<any, any, any>;

export type TableSchedule = {
reducer: () => UntypedScheduledFunctionExport;
};

/**
* Represents a handle to a database table, including its name, row type, and row spacetime type.
Expand Down Expand Up @@ -50,12 +65,17 @@ export type TableSchema<
}[];

/**
* The schedule defined on the table, if any.
* The column id of the schedule-at column, if this table has a ScheduleAt column.
*/
readonly scheduleAtCol?: number;

/**
* The legacy schedule defined on the table, if any.
*
* @deprecated Prefer `spacetime.schedule(table, reducerOrProcedure)` so table
* definitions can live in a separate module from reducer/procedure definitions.
*/
readonly schedule?: {
scheduleAtCol: number;
reducer: () => ReducerExport<any, any> | ProcedureExport<any, any, any>;
};
readonly schedule?: TableSchedule;
};

export type UntypedTableSchema = TableSchema<
Expand Down
25 changes: 25 additions & 0 deletions crates/bindings-typescript/src/lib/type_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,31 @@ export type Values<T> = T[keyof T];
*/
export type CollapseTuple<A extends any[]> = A extends [infer T] ? T : A;

/**
* Conditional-type helper for distinguishing a single type from a union.
*/
export type IsUnion<T, U = T> = [T] extends [never]
? false
: T extends any
? [U] extends [T]
? false
: true
: false;

/**
* True when an object type has exactly one known key.
*
* If keys widen to plain `string`, the exact count is no longer knowable, so
* treat it as valid and let narrower call sites or runtime checks handle it.
*/
export type HasExactlyOneKnownKey<T> = [keyof T] extends [never]
? false
: string extends keyof T
? true
: IsUnion<keyof T> extends true
? false
: true;

type CamelCaseImpl<S extends string> = S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<CamelCaseImpl<Tail>>}`
: S extends `${infer Head}-${infer Tail}`
Expand Down
17 changes: 14 additions & 3 deletions crates/bindings-typescript/src/server/procedures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ import {
type SchemaInner,
} from './schema';

declare const procedureReturnTypeBrand: unique symbol;

export type ProcedureExport<
S extends UntypedSchemaDef,
Params extends ParamsObj,
Ret extends TypeBuilder<any, any>,
> = ProcedureFn<S, Params, Ret> & ModuleExport;
> = ProcedureFn<S, Params, Ret> &
ModuleExport & {
/** Type-only brand used to preserve the procedure return builder in assignability checks. */
readonly [procedureReturnTypeBrand]: Ret;
};

export function makeProcedureExport<
S extends UntypedSchemaDef,
Expand All @@ -49,8 +55,13 @@ export function makeProcedureExport<
): ProcedureExport<S, Params, Ret> {
const name = opts?.name;

const procedureExport: ProcedureExport<S, Params, Ret> = (...args) =>
fn(...args);
// ProcedureExport carries a type-only return-builder brand, so the plain
// function literal needs a cast when we attach module export metadata below.
const procedureExport = ((...args) => fn(...args)) as ProcedureExport<
S,
Params,
Ret
>;
procedureExport[exportContext] = ctx;
procedureExport[registerExport] = (ctx, exportName) => {
registerProcedure(ctx, name ?? exportName, params, ret, fn);
Expand Down
72 changes: 72 additions & 0 deletions crates/bindings-typescript/src/server/schema.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,75 @@ spacetimedbIndexSplit.init(ctx => {
// @ts-expect-error `nickname` is not indexed, so no index accessor should exist.
const _nickname = ctx.db.account.nickname;
});

const scheduledMessages = table(
{},
{
scheduledId: t.u64().primaryKey().autoInc(),
scheduledAt: t.scheduleAt(),
text: t.string(),
}
);

const spacetimedbSchedules = schema({ scheduledMessages });

const processScheduledMessage = spacetimedbSchedules.reducer(
{
scheduledMessage: scheduledMessages.rowType,
},
(_ctx, { scheduledMessage }) => {
void scheduledMessage.text;
}
);

spacetimedbSchedules.schedule(scheduledMessages, processScheduledMessage);

const processWrongPayload = spacetimedbSchedules.reducer(
{
text: t.string(),
},
() => {}
);

// @ts-expect-error scheduled reducers must take the scheduled table row type.
spacetimedbSchedules.schedule(scheduledMessages, processWrongPayload);

const processNoPayload = spacetimedbSchedules.reducer({}, () => {});

// @ts-expect-error scheduled reducers must take exactly one payload field.
spacetimedbSchedules.schedule(scheduledMessages, processNoPayload);

const processMultiplePayloadFields = spacetimedbSchedules.reducer(
{
first: scheduledMessages.rowType,
second: scheduledMessages.rowType,
},
() => {}
);

// @ts-expect-error scheduled reducers must take exactly one payload field.
spacetimedbSchedules.schedule(scheduledMessages, processMultiplePayloadFields);

const processScheduledProcedure = spacetimedbSchedules.procedure(
{
scheduledMessage: scheduledMessages.rowType,
},
t.unit(),
(_ctx, { scheduledMessage }) => {
void scheduledMessage.text;
return {};
}
);

spacetimedbSchedules.schedule(scheduledMessages, processScheduledProcedure);

const processWrongReturnProcedure = spacetimedbSchedules.procedure(
{
scheduledMessage: scheduledMessages.rowType,
},
t.string(),
() => ''
);

// @ts-expect-error scheduled procedures must return unit.
spacetimedbSchedules.schedule(scheduledMessages, processWrongReturnProcedure);
Loading
Loading