spacetimedb 2.3.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE.txt +2 -2
  2. package/dist/index.browser.mjs +64 -4
  3. package/dist/index.browser.mjs.map +1 -1
  4. package/dist/index.cjs +64 -4
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.mjs +64 -4
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/lib/autogen/types.d.ts +675 -2
  9. package/dist/lib/autogen/types.d.ts.map +1 -1
  10. package/dist/lib/schema.d.ts.map +1 -1
  11. package/dist/min/index.browser.mjs +1 -1
  12. package/dist/min/index.browser.mjs.map +1 -1
  13. package/dist/min/sdk/index.browser.mjs +1 -1
  14. package/dist/min/sdk/index.browser.mjs.map +1 -1
  15. package/dist/sdk/decompress.d.ts.map +1 -1
  16. package/dist/sdk/index.browser.mjs +64 -4
  17. package/dist/sdk/index.browser.mjs.map +1 -1
  18. package/dist/sdk/index.cjs +64 -4
  19. package/dist/sdk/index.cjs.map +1 -1
  20. package/dist/sdk/index.mjs +64 -4
  21. package/dist/sdk/index.mjs.map +1 -1
  22. package/dist/server/http.d.ts +1 -2
  23. package/dist/server/http.d.ts.map +1 -1
  24. package/dist/server/http.test-d.d.ts +2 -0
  25. package/dist/server/http.test-d.d.ts.map +1 -0
  26. package/dist/server/http_handlers.d.ts +82 -0
  27. package/dist/server/http_handlers.d.ts.map +1 -0
  28. package/dist/server/http_internal.d.ts +1 -32
  29. package/dist/server/http_internal.d.ts.map +1 -1
  30. package/dist/server/http_shared.d.ts +44 -0
  31. package/dist/server/http_shared.d.ts.map +1 -0
  32. package/dist/server/index.d.ts +2 -0
  33. package/dist/server/index.d.ts.map +1 -1
  34. package/dist/server/index.mjs +628 -136
  35. package/dist/server/index.mjs.map +1 -1
  36. package/dist/server/runtime.d.ts +1 -0
  37. package/dist/server/runtime.d.ts.map +1 -1
  38. package/dist/server/schema.d.ts +19 -4
  39. package/dist/server/schema.d.ts.map +1 -1
  40. package/dist/server/views.d.ts +17 -1
  41. package/dist/server/views.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/lib/autogen/types.ts +38 -0
  44. package/src/lib/schema.ts +21 -0
  45. package/src/sdk/decompress.ts +19 -4
  46. package/src/sdk/logger.ts +1 -1
  47. package/src/server/http.test-d.ts +80 -0
  48. package/src/server/http.ts +14 -2
  49. package/src/server/http_handlers.ts +413 -0
  50. package/src/server/http_internal.ts +15 -142
  51. package/src/server/http_shared.ts +186 -0
  52. package/src/server/index.ts +11 -0
  53. package/src/server/procedures.ts +8 -30
  54. package/src/server/runtime.ts +137 -1
  55. package/src/server/schema.ts +86 -4
  56. package/src/server/sys.d.ts +7 -0
  57. package/src/server/view.test-d.ts +22 -0
  58. package/src/server/views.ts +127 -0
  59. package/dist/lib/http_types.d.ts +0 -2
  60. package/dist/lib/http_types.d.ts.map +0 -1
  61. package/src/lib/http_types.ts +0 -8
@@ -0,0 +1,186 @@
1
+ import { Headers, headersToList } from 'headers-polyfill';
2
+ import type {
3
+ HttpHeaders,
4
+ HttpMethod,
5
+ HttpVersion,
6
+ } from '../lib/autogen/types';
7
+
8
+ export { Headers };
9
+
10
+ export type BodyInit = ArrayBuffer | ArrayBufferView | string;
11
+ export type HeadersInit = [string, string][] | Record<string, string> | Headers;
12
+
13
+ export const textEncoder = new TextEncoder();
14
+ export const textDecoder = new TextDecoder('utf-8');
15
+
16
+ export function deserializeMethod(method: HttpMethod): string {
17
+ switch (method.tag) {
18
+ case 'Get':
19
+ return 'GET';
20
+ case 'Head':
21
+ return 'HEAD';
22
+ case 'Post':
23
+ return 'POST';
24
+ case 'Put':
25
+ return 'PUT';
26
+ case 'Delete':
27
+ return 'DELETE';
28
+ case 'Connect':
29
+ return 'CONNECT';
30
+ case 'Options':
31
+ return 'OPTIONS';
32
+ case 'Trace':
33
+ return 'TRACE';
34
+ case 'Patch':
35
+ return 'PATCH';
36
+ case 'Extension':
37
+ return method.value;
38
+ }
39
+ }
40
+
41
+ const methods = new Map<string, HttpMethod>([
42
+ ['GET', { tag: 'Get' }],
43
+ ['HEAD', { tag: 'Head' }],
44
+ ['POST', { tag: 'Post' }],
45
+ ['PUT', { tag: 'Put' }],
46
+ ['DELETE', { tag: 'Delete' }],
47
+ ['CONNECT', { tag: 'Connect' }],
48
+ ['OPTIONS', { tag: 'Options' }],
49
+ ['TRACE', { tag: 'Trace' }],
50
+ ['PATCH', { tag: 'Patch' }],
51
+ ]);
52
+
53
+ export function serializeMethod(method?: string): HttpMethod {
54
+ return (
55
+ methods.get(method?.toUpperCase() ?? 'GET') ?? {
56
+ tag: 'Extension',
57
+ value: method!,
58
+ }
59
+ );
60
+ }
61
+
62
+ export function serializeHeaders(headers: Headers): HttpHeaders {
63
+ return {
64
+ entries: headersToList(headers as any)
65
+ .flatMap(([k, v]) => (Array.isArray(v) ? v.map(v => [k, v]) : [[k, v]]))
66
+ .map(([name, value]) => ({ name, value: textEncoder.encode(value) })),
67
+ };
68
+ }
69
+
70
+ export function deserializeHeaders(headers: HttpHeaders): Headers {
71
+ return new Headers(
72
+ headers.entries.map(({ name, value }): [string, string] => [
73
+ name,
74
+ textDecoder.decode(value),
75
+ ])
76
+ );
77
+ }
78
+
79
+ export interface ResponseInit {
80
+ headers?: HeadersInit;
81
+ status?: number;
82
+ statusText?: string;
83
+ version?: HttpVersion;
84
+ }
85
+
86
+ export interface InnerResponse {
87
+ type: 'basic' | 'cors' | 'default' | 'error' | 'opaque' | 'opaqueredirect';
88
+ url: string | null;
89
+ status: number;
90
+ statusText: string;
91
+ headers: Headers;
92
+ aborted: boolean;
93
+ version: HttpVersion;
94
+ }
95
+
96
+ export const makeResponse = Symbol('makeResponse');
97
+
98
+ export class SyncResponse {
99
+ #body: string | ArrayBuffer | null;
100
+ #inner: InnerResponse;
101
+
102
+ constructor(body?: BodyInit | null, init?: ResponseInit) {
103
+ if (body == null) {
104
+ this.#body = null;
105
+ } else if (typeof body === 'string') {
106
+ this.#body = body;
107
+ } else {
108
+ // this call is fine, the typings are just weird
109
+ this.#body = new Uint8Array<ArrayBuffer>(body as any).buffer;
110
+ }
111
+
112
+ // there's a type mismatch - headers-polyfill's typing doesn't expect its
113
+ // own `Headers` type, even though the actual code handles it correctly.
114
+ this.#inner = {
115
+ headers: new Headers(init?.headers as any),
116
+ status: init?.status ?? 200,
117
+ statusText: init?.statusText ?? '',
118
+ type: 'default',
119
+ url: null,
120
+ aborted: false,
121
+ version: init?.version ?? { tag: 'Http11' },
122
+ };
123
+ }
124
+
125
+ static [makeResponse](body: BodyInit | null, inner: InnerResponse) {
126
+ const me = new SyncResponse(body);
127
+ me.#inner = inner;
128
+ return me;
129
+ }
130
+
131
+ get headers(): Headers {
132
+ return this.#inner.headers;
133
+ }
134
+
135
+ get status(): number {
136
+ return this.#inner.status;
137
+ }
138
+
139
+ get statusText() {
140
+ return this.#inner.statusText;
141
+ }
142
+
143
+ get ok(): boolean {
144
+ return 200 <= this.#inner.status && this.#inner.status <= 299;
145
+ }
146
+
147
+ get url(): string {
148
+ return this.#inner.url ?? '';
149
+ }
150
+
151
+ get type() {
152
+ return this.#inner.type;
153
+ }
154
+
155
+ get version(): HttpVersion {
156
+ return this.#inner.version;
157
+ }
158
+
159
+ arrayBuffer(): ArrayBuffer {
160
+ return this.bytes().buffer;
161
+ }
162
+
163
+ bytes(): Uint8Array<ArrayBuffer> {
164
+ if (this.#body == null) {
165
+ return new Uint8Array();
166
+ } else if (typeof this.#body === 'string') {
167
+ return textEncoder.encode(this.#body);
168
+ } else {
169
+ return new Uint8Array(this.#body);
170
+ }
171
+ }
172
+
173
+ json(): any {
174
+ return JSON.parse(this.text());
175
+ }
176
+
177
+ text(): string {
178
+ if (this.#body == null) {
179
+ return '';
180
+ } else if (typeof this.#body === 'string') {
181
+ return this.#body;
182
+ } else {
183
+ return textDecoder.decode(this.#body);
184
+ }
185
+ }
186
+ }
@@ -22,5 +22,16 @@ export type { Uuid } from '../lib/uuid';
22
22
  export type { Random } from './rng';
23
23
  export type { ViewExport, ViewCtx, AnonymousViewCtx } from './views';
24
24
  export { Range, type Bound } from './range';
25
+ export {
26
+ Headers,
27
+ Request,
28
+ SyncResponse,
29
+ Router,
30
+ type BodyInit,
31
+ type HeadersInit,
32
+ type RequestInit,
33
+ type ResponseInit,
34
+ } from './http';
35
+ export type { HandlerContext, HttpHandlerExport } from './http';
25
36
 
26
37
  import './polyfills'; // Ensure polyfills are loaded
@@ -22,7 +22,7 @@ import { Uuid } from '../lib/uuid';
22
22
  import { httpClient, type HttpClient } from './http_internal';
23
23
  import type { DbView } from './db_view';
24
24
  import { makeRandom, type Random } from './rng';
25
- import { callUserFunction, ReducerCtxImpl, sys } from './runtime';
25
+ import { callUserFunction, ReducerCtxImpl, runWithTx, sys } from './runtime';
26
26
  import {
27
27
  exportContext,
28
28
  registerExport,
@@ -214,38 +214,16 @@ const ProcedureCtxImpl = class ProcedureCtx<S extends UntypedSchemaDef>
214
214
  }
215
215
 
216
216
  withTx<T>(body: (ctx: TransactionCtx<S>) => T): T {
217
- const run = () => {
218
- const timestamp = sys.procedure_start_mut_tx();
219
-
220
- try {
221
- const ctx: TransactionCtx<S> = new TransactionCtxImpl(
217
+ return runWithTx(
218
+ timestamp =>
219
+ new TransactionCtxImpl(
222
220
  this.sender,
223
- new Timestamp(timestamp),
221
+ timestamp,
224
222
  this.connectionId,
225
223
  this.#dbView()
226
- );
227
- return body(ctx);
228
- } catch (e) {
229
- sys.procedure_abort_mut_tx();
230
- throw e;
231
- }
232
- };
233
-
234
- let res = run();
235
- try {
236
- sys.procedure_commit_mut_tx();
237
- return res;
238
- } catch {
239
- // ignore the commit error
240
- }
241
- console.warn('committing anonymous transaction failed');
242
- res = run();
243
- try {
244
- sys.procedure_commit_mut_tx();
245
- return res;
246
- } catch (e) {
247
- throw new Error('transaction retry failed again', { cause: e });
248
- }
224
+ ) as TransactionCtx<S>,
225
+ body
226
+ );
249
227
  }
250
228
 
251
229
  newUuidV4(): Uuid {
@@ -27,6 +27,18 @@ import {
27
27
  type UniqueIndex,
28
28
  } from '../lib/indexes';
29
29
  import { callProcedure } from './procedures';
30
+ import {
31
+ type HandlerContext,
32
+ Request,
33
+ SyncResponse,
34
+ makeRequest,
35
+ } from './http_handlers';
36
+ import { httpClient } from './http_internal';
37
+ import {
38
+ deserializeHeaders,
39
+ deserializeMethod,
40
+ serializeHeaders,
41
+ } from './http_shared';
30
42
  import {
31
43
  type AuthCtx,
32
44
  type JsonObject,
@@ -35,7 +47,7 @@ import {
35
47
  } from '../lib/reducers';
36
48
  import { type UntypedSchemaDef } from '../lib/schema';
37
49
  import { type RowType, type Table, type TableMethods } from '../lib/table';
38
- import { hasOwn } from '../lib/util';
50
+ import { bsatnBaseSize, hasOwn } from '../lib/util';
39
51
  import { type AnonymousViewCtx, type ViewCtx } from './views';
40
52
  import { isRowTypedQuery, makeQueryBuilder, toSql } from './query';
41
53
  import type { DbView } from './db_view';
@@ -43,11 +55,32 @@ import { getErrorConstructor, SenderError } from './errors';
43
55
  import { Range, type Bound } from './range';
44
56
  import { makeRandom, type Random } from './rng';
45
57
  import type { SchemaInner } from './schema';
58
+ import { HttpRequest, HttpResponse } from '../lib/autogen/types';
46
59
 
47
60
  const { freeze } = Object;
48
61
 
49
62
  export const sys = { ..._syscalls2_0, ..._syscalls2_1 };
50
63
 
64
+ function requestFromWire(request: HttpRequest, body: Uint8Array): Request {
65
+ return Request[makeRequest](body, {
66
+ headers: deserializeHeaders(request.headers),
67
+ method: deserializeMethod(request.method),
68
+ uri: request.uri,
69
+ version: request.version,
70
+ });
71
+ }
72
+
73
+ function responseIntoWire(response: SyncResponse): [HttpResponse, Uint8Array] {
74
+ return [
75
+ {
76
+ headers: serializeHeaders(response.headers),
77
+ version: response.version,
78
+ code: response.status,
79
+ },
80
+ response.bytes(),
81
+ ];
82
+ }
83
+
51
84
  export function parseJsonObject(json: string): JsonObject {
52
85
  let value: unknown;
53
86
 
@@ -272,6 +305,38 @@ export const callUserFunction = function __spacetimedb_end_short_backtrace<
272
305
  return fn(...args);
273
306
  };
274
307
 
308
+ export function runWithTx<T, Ctx>(
309
+ makeCtx: (timestamp: Timestamp) => Ctx,
310
+ body: (ctx: Ctx) => T
311
+ ): T {
312
+ const run = () => {
313
+ const timestamp = sys.procedure_start_mut_tx();
314
+
315
+ try {
316
+ return body(makeCtx(new Timestamp(timestamp)));
317
+ } catch (e) {
318
+ sys.procedure_abort_mut_tx();
319
+ throw e;
320
+ }
321
+ };
322
+
323
+ let res = run();
324
+ try {
325
+ sys.procedure_commit_mut_tx();
326
+ return res;
327
+ } catch {
328
+ // ignore the commit error
329
+ }
330
+ console.warn('committing anonymous transaction failed');
331
+ res = run();
332
+ try {
333
+ sys.procedure_commit_mut_tx();
334
+ return res;
335
+ } catch (e) {
336
+ throw new Error('transaction retry failed again', { cause: e });
337
+ }
338
+ }
339
+
275
340
  export const makeHooks = (schema: SchemaInner): ModuleHooks =>
276
341
  new ModuleHooksImpl(schema);
277
342
 
@@ -418,11 +483,82 @@ class ModuleHooksImpl implements ModuleHooks {
418
483
  () => this.#dbView
419
484
  );
420
485
  }
486
+
487
+ __call_http_handler__(
488
+ id: u32,
489
+ timestamp: bigint,
490
+ request: Uint8Array,
491
+ body: Uint8Array
492
+ ): [response: Uint8Array, body: Uint8Array] {
493
+ const moduleCtx = this.#schema;
494
+ const handler = moduleCtx.httpHandlers[id];
495
+ const ctx = new HandlerContextImpl(
496
+ new Timestamp(timestamp),
497
+ () => this.#dbView
498
+ );
499
+ const requestMetadata = HttpRequest.deserialize(new BinaryReader(request));
500
+ const response = callUserFunction(
501
+ handler,
502
+ ctx,
503
+ requestFromWire(requestMetadata, body)
504
+ );
505
+ const [responseMetadata, responseBody] = responseIntoWire(response);
506
+ const responseBuf = new BinaryWriter(
507
+ bsatnBaseSize(moduleCtx.typespace, HttpResponse.algebraicType)
508
+ );
509
+ HttpResponse.serialize(responseBuf, responseMetadata);
510
+ return [responseBuf.getBuffer(), responseBody];
511
+ }
421
512
  }
422
513
 
423
514
  const BINARY_WRITER = new BinaryWriter(0);
424
515
  const BINARY_READER = new BinaryReader(new Uint8Array());
425
516
 
517
+ class HandlerContextImpl<S extends UntypedSchemaDef = UntypedSchemaDef>
518
+ implements HandlerContext<S>
519
+ {
520
+ #identity: Identity | undefined;
521
+ #uuidCounter: { value: number } | undefined;
522
+ #random: Random | undefined;
523
+ #dbView: () => DbView<any>;
524
+
525
+ readonly http = httpClient;
526
+
527
+ constructor(
528
+ readonly timestamp: Timestamp,
529
+ dbView: () => DbView<any>
530
+ ) {
531
+ this.#dbView = dbView;
532
+ }
533
+
534
+ get identity() {
535
+ return (this.#identity ??= new Identity(sys.identity()));
536
+ }
537
+
538
+ get random() {
539
+ return (this.#random ??= makeRandom(this.timestamp));
540
+ }
541
+
542
+ withTx<T>(body: (ctx: any) => T): T {
543
+ return runWithTx(
544
+ timestamp =>
545
+ new ReducerCtxImpl(Identity.zero(), timestamp, null, this.#dbView()),
546
+ body
547
+ );
548
+ }
549
+
550
+ newUuidV4(): Uuid {
551
+ const bytes = this.random.fill(new Uint8Array(16));
552
+ return Uuid.fromRandomBytesV4(bytes);
553
+ }
554
+
555
+ newUuidV7(): Uuid {
556
+ const bytes = this.random.fill(new Uint8Array(4));
557
+ const counter = (this.#uuidCounter ??= { value: 0 });
558
+ return Uuid.fromCounterV7(counter, this.timestamp, bytes);
559
+ }
560
+ }
561
+
426
562
  function makeTableView(
427
563
  typespace: Typespace,
428
564
  table: RawTableDefV10
@@ -1,5 +1,9 @@
1
1
  import { moduleHooks, type ModuleDefaultExport } from 'spacetime:sys@2.0';
2
- import { CaseConversionPolicy, Lifecycle } from '../lib/autogen/types';
2
+ import {
3
+ CaseConversionPolicy,
4
+ Lifecycle,
5
+ type MethodOrAny,
6
+ } from '../lib/autogen/types';
3
7
  import {
4
8
  type ParamsAsObject,
5
9
  type ParamsObj,
@@ -14,6 +18,14 @@ import {
14
18
  } from '../lib/schema';
15
19
  import type { UntypedTableSchema } from '../lib/table_schema';
16
20
  import { ColumnBuilder, TypeBuilder } from '../lib/type_builders';
21
+ import {
22
+ Router,
23
+ type HandlerFn,
24
+ type HttpHandlerExport,
25
+ type HttpHandlerOpts,
26
+ makeHttpHandlerExport,
27
+ makeHttpRouterExport,
28
+ } from './http_handlers';
17
29
  import {
18
30
  makeProcedureExport,
19
31
  type ProcedureExport,
@@ -38,6 +50,7 @@ import {
38
50
  type ViewFn,
39
51
  type ViewOpts,
40
52
  type ViewReturnTypeBuilder,
53
+ type ValidateViewPrimaryKey,
41
54
  type Views,
42
55
  } from './views';
43
56
  import type { UntypedTableDef } from '../lib/table';
@@ -47,10 +60,12 @@ export class SchemaInner<
47
60
  > extends ModuleContext {
48
61
  schemaType: S;
49
62
  existingFunctions = new Set<string>();
63
+ existingHttpHandlers = new Set<string>();
50
64
  reducers: Reducers = [];
51
65
  procedures: Procedures = [];
52
66
  views: Views = [];
53
67
  anonViews: AnonViews = [];
68
+ httpHandlers: HandlerFn[] = [];
54
69
  /**
55
70
  * Maps ReducerExport objects to the name of the reducer.
56
71
  * Used for resolving the reducers of scheduled tables.
@@ -60,7 +75,10 @@ export class SchemaInner<
60
75
  | ProcedureExport<UntypedSchemaDef, any, any>,
61
76
  string
62
77
  > = new Map();
78
+ httpHandlerExports: Map<HttpHandlerExport<UntypedSchemaDef>, string> =
79
+ new Map();
63
80
  pendingSchedules: PendingSchedule[] = [];
81
+ pendingHttpRoutes: PendingHttpRoute[] = [];
64
82
 
65
83
  constructor(getSchemaType: (ctx: SchemaInner<S>) => S) {
66
84
  super();
@@ -70,12 +88,21 @@ export class SchemaInner<
70
88
  defineFunction(name: string) {
71
89
  if (this.existingFunctions.has(name)) {
72
90
  throw new TypeError(
73
- `There is already a reducer or procedure with the name '${name}'`
91
+ `There is already a reducer, procedure, or view with the name '${name}'`
74
92
  );
75
93
  }
76
94
  this.existingFunctions.add(name);
77
95
  }
78
96
 
97
+ defineHttpHandler(name: string) {
98
+ if (this.existingHttpHandlers.has(name)) {
99
+ throw new TypeError(
100
+ `There is already an HTTP handler with the name '${name}'`
101
+ );
102
+ }
103
+ this.existingHttpHandlers.add(name);
104
+ }
105
+
79
106
  resolveSchedules() {
80
107
  for (const { reducer, scheduleAtCol, tableName } of this.pendingSchedules) {
81
108
  const functionName = this.functionExports.get(reducer());
@@ -91,9 +118,30 @@ export class SchemaInner<
91
118
  });
92
119
  }
93
120
  }
121
+
122
+ resolveHttpRoutes() {
123
+ for (const route of this.pendingHttpRoutes) {
124
+ const handlerFunction = this.httpHandlerExports.get(route.handler);
125
+ if (handlerFunction === undefined) {
126
+ throw new TypeError(
127
+ `HTTP route for path '${route.path}' refers to a handler that was not exported.`
128
+ );
129
+ }
130
+ this.moduleDef.httpRoutes.push({
131
+ handlerFunction,
132
+ method: route.method,
133
+ path: route.path,
134
+ });
135
+ }
136
+ }
94
137
  }
95
138
 
96
139
  type PendingSchedule = UntypedTableSchema['schedule'] & { tableName: string };
140
+ type PendingHttpRoute = {
141
+ handler: HttpHandlerExport<UntypedSchemaDef>;
142
+ method: MethodOrAny;
143
+ path: string;
144
+ };
97
145
 
98
146
  /**
99
147
  * The Schema class represents the database schema for a SpacetimeDB application.
@@ -153,6 +201,7 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
153
201
  moduleExport[registerExport](registeredSchema, name);
154
202
  }
155
203
  registeredSchema.resolveSchedules();
204
+ registeredSchema.resolveHttpRoutes();
156
205
  return makeHooks(registeredSchema);
157
206
  }
158
207
 
@@ -347,7 +396,11 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
347
396
  view<Ret extends ViewReturnTypeBuilder, F extends ViewFn<S, {}, Ret>>(
348
397
  opts: ViewOpts,
349
398
  ret: Ret,
350
- fn: F
399
+ fn: F,
400
+ // Compile-time-only guard: this rest parameter is `[]` for valid return
401
+ // builders, but becomes a required error tuple when a returned row builder
402
+ // marks more than one column with `.primaryKey()`.
403
+ ..._: ValidateViewPrimaryKey<Ret>
351
404
  ): ViewExport<F> {
352
405
  return makeViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
353
406
  }
@@ -380,7 +433,15 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
380
433
  anonymousView<
381
434
  Ret extends ViewReturnTypeBuilder,
382
435
  F extends AnonymousViewFn<S, {}, Ret>,
383
- >(opts: ViewOpts, ret: Ret, fn: F): ViewExport<F> {
436
+ >(
437
+ opts: ViewOpts,
438
+ ret: Ret,
439
+ fn: F,
440
+ // Compile-time-only guard: this rest parameter is `[]` for valid return
441
+ // builders, but becomes a required error tuple when a returned row builder
442
+ // marks more than one column with `.primaryKey()`.
443
+ ..._: ValidateViewPrimaryKey<Ret>
444
+ ): ViewExport<F> {
384
445
  return makeAnonViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
385
446
  }
386
447
 
@@ -458,6 +519,27 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
458
519
  return makeProcedureExport(this.#ctx, opts, params, ret, fn);
459
520
  }
460
521
 
522
+ httpHandler(fn: HandlerFn<S>): HttpHandlerExport<S>;
523
+ httpHandler(opts: HttpHandlerOpts, fn: HandlerFn<S>): HttpHandlerExport<S>;
524
+ httpHandler(
525
+ ...args: [HandlerFn<S>] | [HttpHandlerOpts, HandlerFn<S>]
526
+ ): HttpHandlerExport<S> {
527
+ let opts: HttpHandlerOpts | undefined, fn: HandlerFn<S>;
528
+ switch (args.length) {
529
+ case 1:
530
+ [fn] = args;
531
+ break;
532
+ case 2:
533
+ [opts, fn] = args;
534
+ break;
535
+ }
536
+ return makeHttpHandlerExport(this.#ctx, opts, fn);
537
+ }
538
+
539
+ httpRouter(router: Router): ModuleExport {
540
+ return makeHttpRouterExport(this.#ctx, router);
541
+ }
542
+
461
543
  /**
462
544
  * Bundle multiple reducers, procedures, etc into one value to export.
463
545
  * The name they will be exported with is their corresponding key in the `exports` argument.
@@ -37,6 +37,13 @@ declare module 'spacetime:sys@2.0' {
37
37
  timestamp: bigint,
38
38
  args: Uint8Array
39
39
  ): Uint8Array;
40
+
41
+ __call_http_handler__(
42
+ id: u32,
43
+ timestamp: bigint,
44
+ request: Uint8Array,
45
+ body: Uint8Array
46
+ ): [response: Uint8Array, body: Uint8Array];
40
47
  }
41
48
 
42
49
  export function register_hooks(hooks: ModuleHooks);
@@ -73,11 +73,33 @@ const spacetime = schema({
73
73
 
74
74
  const arrayRetValue = t.array(person.rowType);
75
75
  const optionalPerson = t.option(person.rowType);
76
+ const multiplePrimaryKeyRows = t.array(
77
+ t.row('MultiplePrimaryKeyRows', {
78
+ id: t.u32().primaryKey(),
79
+ name: t.string().primaryKey(),
80
+ })
81
+ );
76
82
 
77
83
  spacetime.anonymousView({ name: 'v1', public: true }, arrayRetValue, ctx => {
78
84
  return ctx.from.person.build();
79
85
  });
80
86
 
87
+ // @ts-expect-error views can have at most one primaryKey column on the returned row type.
88
+ spacetime.anonymousView(
89
+ { name: 'multiplePrimaryRows', public: true },
90
+ multiplePrimaryKeyRows,
91
+ () => []
92
+ );
93
+
94
+ // @ts-expect-error the same multiple-primary-key check also applies to query-builder views.
95
+ spacetime.anonymousView(
96
+ { name: 'multiplePrimaryRowsQuery', public: true },
97
+ multiplePrimaryKeyRows,
98
+ ctx => {
99
+ return ctx.from.person;
100
+ }
101
+ );
102
+
81
103
  spacetime.anonymousView(
82
104
  { name: 'optionalPerson', public: true },
83
105
  optionalPerson,