spacetimedb 2.2.0 → 2.4.0
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.
- package/LICENSE.txt +2 -2
- package/dist/browser/vue/index.mjs +36 -1
- package/dist/browser/vue/index.mjs.map +1 -1
- package/dist/index.browser.mjs +50 -4
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +50 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +50 -4
- package/dist/index.mjs.map +1 -1
- package/dist/lib/autogen/types.d.ts +674 -18
- package/dist/lib/autogen/types.d.ts.map +1 -1
- package/dist/lib/reducers.d.ts +2 -0
- package/dist/lib/reducers.d.ts.map +1 -1
- package/dist/lib/schema.d.ts.map +1 -1
- package/dist/min/index.browser.mjs +1 -1
- package/dist/min/index.browser.mjs.map +1 -1
- package/dist/min/sdk/index.browser.mjs +1 -1
- package/dist/min/sdk/index.browser.mjs.map +1 -1
- package/dist/sdk/decompress.d.ts.map +1 -1
- package/dist/sdk/index.browser.mjs +50 -4
- package/dist/sdk/index.browser.mjs.map +1 -1
- package/dist/sdk/index.cjs +50 -4
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.mjs +50 -4
- package/dist/sdk/index.mjs.map +1 -1
- package/dist/server/http.d.ts +1 -2
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.test-d.d.ts +2 -0
- package/dist/server/http.test-d.d.ts.map +1 -0
- package/dist/server/http_handlers.d.ts +82 -0
- package/dist/server/http_handlers.d.ts.map +1 -0
- package/dist/server/http_internal.d.ts +1 -32
- package/dist/server/http_internal.d.ts.map +1 -1
- package/dist/server/http_shared.d.ts +44 -0
- package/dist/server/http_shared.d.ts.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.mjs +590 -136
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/procedures.d.ts +2 -0
- package/dist/server/procedures.d.ts.map +1 -1
- package/dist/server/runtime.d.ts +2 -0
- package/dist/server/runtime.d.ts.map +1 -1
- package/dist/server/schema.d.ts +16 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/views.d.ts.map +1 -1
- package/dist/tanstack/index.cjs +34 -0
- package/dist/tanstack/index.cjs.map +1 -1
- package/dist/tanstack/index.d.ts +1 -0
- package/dist/tanstack/index.d.ts.map +1 -1
- package/dist/tanstack/index.mjs +34 -1
- package/dist/tanstack/index.mjs.map +1 -1
- package/dist/vue/index.cjs +36 -0
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.mjs +36 -1
- package/dist/vue/index.mjs.map +1 -1
- package/dist/vue/useProcedure.d.ts +4 -0
- package/dist/vue/useProcedure.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/lib/autogen/types.ts +29 -0
- package/src/lib/reducers.ts +2 -0
- package/src/lib/schema.ts +14 -0
- package/src/sdk/decompress.ts +19 -4
- package/src/sdk/logger.ts +1 -1
- package/src/server/http.test-d.ts +80 -0
- package/src/server/http.ts +14 -2
- package/src/server/http_handlers.ts +413 -0
- package/src/server/http_internal.ts +15 -142
- package/src/server/http_shared.ts +186 -0
- package/src/server/index.ts +11 -0
- package/src/server/procedures.ts +15 -31
- package/src/server/runtime.ts +142 -2
- package/src/server/schema.ts +71 -2
- package/src/server/sys.d.ts +7 -0
- package/src/server/views.ts +1 -0
- package/src/tanstack/index.ts +1 -0
- package/src/vue/index.ts +1 -0
- package/src/vue/useProcedure.ts +62 -0
- package/dist/lib/http_types.d.ts +0 -2
- package/dist/lib/http_types.d.ts.map +0 -1
- package/src/lib/http_types.ts +0 -8
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { UntypedProcedureDef } from '../sdk/procedures';
|
|
2
|
+
import type { ProcedureParamsType, ProcedureReturnType } from '../sdk/type_utils';
|
|
3
|
+
export declare function useProcedure<ProcedureDef extends UntypedProcedureDef>(procedureDef: ProcedureDef): (...params: ProcedureParamsType<ProcedureDef>) => Promise<ProcedureReturnType<ProcedureDef>>;
|
|
4
|
+
//# sourceMappingURL=useProcedure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useProcedure.d.ts","sourceRoot":"","sources":["../../src/vue/useProcedure.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAE3B,wBAAgB,YAAY,CAAC,YAAY,SAAS,mBAAmB,EACnE,YAAY,EAAE,YAAY,GACzB,CACD,GAAG,MAAM,EAAE,mBAAmB,CAAC,YAAY,CAAC,KACzC,OAAO,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC,CAiD9C"}
|
package/package.json
CHANGED
package/src/lib/autogen/types.ts
CHANGED
|
@@ -158,6 +158,15 @@ export const Lifecycle = __t.enum('Lifecycle', {
|
|
|
158
158
|
});
|
|
159
159
|
export type Lifecycle = __Infer<typeof Lifecycle>;
|
|
160
160
|
|
|
161
|
+
// The tagged union or sum type for the algebraic type `MethodOrAny`.
|
|
162
|
+
export const MethodOrAny = __t.enum('MethodOrAny', {
|
|
163
|
+
Any: __t.unit(),
|
|
164
|
+
get Method() {
|
|
165
|
+
return HttpMethod;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
export type MethodOrAny = __Infer<typeof MethodOrAny>;
|
|
169
|
+
|
|
161
170
|
// The tagged union or sum type for the algebraic type `MiscModuleExport`.
|
|
162
171
|
export const MiscModuleExport = __t.enum('MiscModuleExport', {
|
|
163
172
|
get TypeAlias() {
|
|
@@ -239,6 +248,20 @@ export const RawConstraintDefV9 = __t.object('RawConstraintDefV9', {
|
|
|
239
248
|
});
|
|
240
249
|
export type RawConstraintDefV9 = __Infer<typeof RawConstraintDefV9>;
|
|
241
250
|
|
|
251
|
+
export const RawHttpHandlerDefV10 = __t.object('RawHttpHandlerDefV10', {
|
|
252
|
+
sourceName: __t.string(),
|
|
253
|
+
});
|
|
254
|
+
export type RawHttpHandlerDefV10 = __Infer<typeof RawHttpHandlerDefV10>;
|
|
255
|
+
|
|
256
|
+
export const RawHttpRouteDefV10 = __t.object('RawHttpRouteDefV10', {
|
|
257
|
+
handlerFunction: __t.string(),
|
|
258
|
+
get method() {
|
|
259
|
+
return MethodOrAny;
|
|
260
|
+
},
|
|
261
|
+
path: __t.string(),
|
|
262
|
+
});
|
|
263
|
+
export type RawHttpRouteDefV10 = __Infer<typeof RawHttpRouteDefV10>;
|
|
264
|
+
|
|
242
265
|
// The tagged union or sum type for the algebraic type `RawIndexAlgorithm`.
|
|
243
266
|
export const RawIndexAlgorithm = __t.enum('RawIndexAlgorithm', {
|
|
244
267
|
BTree: __t.array(__t.u16()),
|
|
@@ -358,6 +381,12 @@ export const RawModuleDefV10Section = __t.enum('RawModuleDefV10Section', {
|
|
|
358
381
|
get ExplicitNames() {
|
|
359
382
|
return ExplicitNames;
|
|
360
383
|
},
|
|
384
|
+
get HttpHandlers() {
|
|
385
|
+
return __t.array(RawHttpHandlerDefV10);
|
|
386
|
+
},
|
|
387
|
+
get HttpRoutes() {
|
|
388
|
+
return __t.array(RawHttpRouteDefV10);
|
|
389
|
+
},
|
|
361
390
|
});
|
|
362
391
|
export type RawModuleDefV10Section = __Infer<typeof RawModuleDefV10Section>;
|
|
363
392
|
|
package/src/lib/reducers.ts
CHANGED
|
@@ -103,6 +103,8 @@ export interface JwtClaims {
|
|
|
103
103
|
*/
|
|
104
104
|
export type ReducerCtx<SchemaDef extends UntypedSchemaDef> = Readonly<{
|
|
105
105
|
sender: Identity;
|
|
106
|
+
databaseIdentity: Identity;
|
|
107
|
+
/** @deprecated Use `databaseIdentity` instead. */
|
|
106
108
|
identity: Identity;
|
|
107
109
|
timestamp: Timestamp;
|
|
108
110
|
connectionId: ConnectionId | null;
|
package/src/lib/schema.ts
CHANGED
|
@@ -195,6 +195,8 @@ export class ModuleContext {
|
|
|
195
195
|
procedures: [],
|
|
196
196
|
views: [],
|
|
197
197
|
lifeCycleReducers: [],
|
|
198
|
+
httpHandlers: [],
|
|
199
|
+
httpRoutes: [],
|
|
198
200
|
caseConversionPolicy: { tag: 'SnakeCase' },
|
|
199
201
|
explicitNames: {
|
|
200
202
|
entries: [],
|
|
@@ -227,6 +229,18 @@ export class ModuleContext {
|
|
|
227
229
|
value: module.lifeCycleReducers,
|
|
228
230
|
}
|
|
229
231
|
);
|
|
232
|
+
push(
|
|
233
|
+
module.httpHandlers && {
|
|
234
|
+
tag: 'HttpHandlers',
|
|
235
|
+
value: module.httpHandlers,
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
push(
|
|
239
|
+
module.httpRoutes && {
|
|
240
|
+
tag: 'HttpRoutes',
|
|
241
|
+
value: module.httpRoutes,
|
|
242
|
+
}
|
|
243
|
+
);
|
|
230
244
|
push(
|
|
231
245
|
module.rowLevelSecurity && {
|
|
232
246
|
tag: 'RowLevelSecurity',
|
package/src/sdk/decompress.ts
CHANGED
|
@@ -28,9 +28,24 @@ export async function decompress(
|
|
|
28
28
|
const decompressedStream = readableStream.pipeThrough(decompressionStream);
|
|
29
29
|
|
|
30
30
|
// Collect the decompressed chunks efficiently
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const reader = decompressedStream.getReader();
|
|
32
|
+
const chunks: Uint8Array[] = [];
|
|
33
|
+
let totalLength = 0;
|
|
34
|
+
let result: any;
|
|
35
|
+
|
|
36
|
+
while (!(result = await reader.read()).done) {
|
|
37
|
+
chunks.push(result.value);
|
|
38
|
+
totalLength += result.value.length;
|
|
34
39
|
}
|
|
35
|
-
|
|
40
|
+
|
|
41
|
+
// Allocate a single Uint8Array for the decompressed data
|
|
42
|
+
const decompressedArray = new Uint8Array(totalLength);
|
|
43
|
+
let chunkOffset = 0;
|
|
44
|
+
|
|
45
|
+
for (const chunk of chunks) {
|
|
46
|
+
decompressedArray.set(chunk, chunkOffset);
|
|
47
|
+
chunkOffset += chunk.length;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return decompressedArray;
|
|
36
51
|
}
|
package/src/sdk/logger.ts
CHANGED
|
@@ -73,7 +73,7 @@ const SENSITIVE_KEYS = new Set([
|
|
|
73
73
|
]);
|
|
74
74
|
|
|
75
75
|
export const stringify = (value: unknown): string | undefined =>
|
|
76
|
-
ssStringify(value, (key, current) => {
|
|
76
|
+
ssStringify(value, (key: string, current: unknown) => {
|
|
77
77
|
if (SENSITIVE_KEYS.has(key)) {
|
|
78
78
|
return '[REDACTED]';
|
|
79
79
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { table } from '../lib/table';
|
|
2
|
+
import t from '../lib/type_builders';
|
|
3
|
+
import {
|
|
4
|
+
type HandlerContext,
|
|
5
|
+
Request,
|
|
6
|
+
SyncResponse,
|
|
7
|
+
Router,
|
|
8
|
+
schema,
|
|
9
|
+
} from './index';
|
|
10
|
+
|
|
11
|
+
const person = table(
|
|
12
|
+
{},
|
|
13
|
+
{
|
|
14
|
+
id: t.u32().primaryKey(),
|
|
15
|
+
name: t.string(),
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const stdb = schema({ person });
|
|
20
|
+
|
|
21
|
+
const hello = stdb.httpHandler((ctx, req) => {
|
|
22
|
+
void ctx.identity;
|
|
23
|
+
void ctx.random;
|
|
24
|
+
req.text();
|
|
25
|
+
req.json();
|
|
26
|
+
|
|
27
|
+
ctx.withTx(tx => {
|
|
28
|
+
tx.db.person.insert({ id: 1, name: 'alice' });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return new SyncResponse('hello', {
|
|
32
|
+
headers: { 'content-type': 'text/plain' },
|
|
33
|
+
status: 200,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const _typedHello: (ctx: HandlerContext<any>, req: Request) => SyncResponse = (
|
|
38
|
+
ctx,
|
|
39
|
+
req
|
|
40
|
+
) => {
|
|
41
|
+
void ctx.timestamp;
|
|
42
|
+
return new SyncResponse(req.text());
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const named = stdb.httpHandler({ name: 'hello' }, (_ctx, _req) => {
|
|
46
|
+
return new SyncResponse('named');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const routes = stdb.httpRouter(
|
|
50
|
+
new Router()
|
|
51
|
+
.get('/hello', hello)
|
|
52
|
+
.get('/named', named)
|
|
53
|
+
.post('/hello-post', hello)
|
|
54
|
+
.nest('/api', new Router().any('/v1', hello))
|
|
55
|
+
.merge(new Router().get('', hello))
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
void routes;
|
|
59
|
+
|
|
60
|
+
// @ts-expect-error handlers must return SyncResponse
|
|
61
|
+
stdb.httpHandler((_ctx, _req) => 123);
|
|
62
|
+
|
|
63
|
+
// @ts-expect-error handlers must take HandlerContext as the first argument
|
|
64
|
+
stdb.httpHandler((_ctx: number, _req: Request) => new SyncResponse('bad'));
|
|
65
|
+
|
|
66
|
+
// @ts-expect-error handlers must take a Request as the second argument
|
|
67
|
+
stdb.httpHandler((_ctx, _req: number) => new SyncResponse('bad'));
|
|
68
|
+
|
|
69
|
+
stdb.httpHandler((ctx, req) => {
|
|
70
|
+
// @ts-expect-error HTTP handlers do not expose sender directly
|
|
71
|
+
void ctx.sender;
|
|
72
|
+
// @ts-expect-error HTTP handlers do not expose connectionId directly
|
|
73
|
+
void ctx.connectionId;
|
|
74
|
+
// @ts-expect-error HTTP handlers do not expose db directly
|
|
75
|
+
void ctx.db;
|
|
76
|
+
return new SyncResponse(req.text());
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// @ts-expect-error routers must reference exported http handlers, not raw functions
|
|
80
|
+
new Router().get('/raw', (_ctx, _req) => new SyncResponse('bad'));
|
package/src/server/http.ts
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
1
|
+
export {
|
|
2
|
+
type BodyInit,
|
|
3
|
+
type HeadersInit,
|
|
4
|
+
type RequestInit,
|
|
5
|
+
type ResponseInit,
|
|
6
|
+
Headers,
|
|
7
|
+
Request,
|
|
8
|
+
SyncResponse,
|
|
9
|
+
Router,
|
|
10
|
+
type HandlerContext,
|
|
11
|
+
type HandlerFn,
|
|
12
|
+
type HttpHandlerExport,
|
|
13
|
+
type HttpHandlerOpts,
|
|
14
|
+
} from './http_handlers';
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import type { Identity } from '../lib/identity';
|
|
2
|
+
import type {
|
|
3
|
+
HttpMethod,
|
|
4
|
+
HttpVersion,
|
|
5
|
+
MethodOrAny,
|
|
6
|
+
} from '../lib/autogen/types';
|
|
7
|
+
import type { UntypedSchemaDef } from '../lib/schema';
|
|
8
|
+
import type { Timestamp } from '../lib/timestamp';
|
|
9
|
+
import type { Uuid } from '../lib/uuid';
|
|
10
|
+
import type { TransactionCtx } from './procedures';
|
|
11
|
+
import type { HttpClient } from './http_internal';
|
|
12
|
+
import type { Random } from './rng';
|
|
13
|
+
import {
|
|
14
|
+
exportContext,
|
|
15
|
+
registerExport,
|
|
16
|
+
type ModuleExport,
|
|
17
|
+
type SchemaInner,
|
|
18
|
+
} from './schema';
|
|
19
|
+
import {
|
|
20
|
+
Headers,
|
|
21
|
+
makeResponse,
|
|
22
|
+
SyncResponse,
|
|
23
|
+
textDecoder,
|
|
24
|
+
textEncoder,
|
|
25
|
+
type BodyInit,
|
|
26
|
+
type HeadersInit,
|
|
27
|
+
type ResponseInit,
|
|
28
|
+
} from './http_shared';
|
|
29
|
+
|
|
30
|
+
export { Headers };
|
|
31
|
+
export { SyncResponse };
|
|
32
|
+
export type { BodyInit, HeadersInit, ResponseInit };
|
|
33
|
+
export { makeResponse };
|
|
34
|
+
export const httpHandlerFn = Symbol('SpacetimeDB.httpHandlerFn');
|
|
35
|
+
|
|
36
|
+
export interface RequestInit {
|
|
37
|
+
body?: BodyInit | null;
|
|
38
|
+
headers?: HeadersInit;
|
|
39
|
+
method?: string;
|
|
40
|
+
version?: HttpVersion;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type RequestInner = {
|
|
44
|
+
headers: Headers;
|
|
45
|
+
method: string;
|
|
46
|
+
uri: string;
|
|
47
|
+
version: HttpVersion;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type RouteSpec = {
|
|
51
|
+
handler: HttpHandlerExport<any>;
|
|
52
|
+
method: MethodOrAny;
|
|
53
|
+
path: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const ACCEPTABLE_ROUTE_PATH_CHARS_HUMAN_DESCRIPTION =
|
|
57
|
+
'ASCII lowercase letters, digits and `-_~/`';
|
|
58
|
+
|
|
59
|
+
export const makeRequest = Symbol('makeRequest');
|
|
60
|
+
|
|
61
|
+
function coerceRequestBody(body?: BodyInit | null): string | Uint8Array | null {
|
|
62
|
+
if (body == null) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (typeof body === 'string') {
|
|
66
|
+
return body;
|
|
67
|
+
}
|
|
68
|
+
return new Uint8Array(body as any);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function requestBodyToBytes(body: string | Uint8Array | null): Uint8Array {
|
|
72
|
+
if (body == null) {
|
|
73
|
+
return new Uint8Array();
|
|
74
|
+
}
|
|
75
|
+
if (typeof body === 'string') {
|
|
76
|
+
return textEncoder.encode(body);
|
|
77
|
+
}
|
|
78
|
+
return body;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function requestBodyToText(body: string | Uint8Array | null): string {
|
|
82
|
+
if (body == null) {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
if (typeof body === 'string') {
|
|
86
|
+
return body;
|
|
87
|
+
}
|
|
88
|
+
return textDecoder.decode(body);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function characterIsAcceptableForRoutePath(c: string) {
|
|
92
|
+
return (
|
|
93
|
+
(c >= 'a' && c <= 'z') ||
|
|
94
|
+
(c >= '0' && c <= '9') ||
|
|
95
|
+
c === '-' ||
|
|
96
|
+
c === '_' ||
|
|
97
|
+
c === '~' ||
|
|
98
|
+
c === '/'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function assertValidPath(path: string) {
|
|
103
|
+
if (path !== '' && !path.startsWith('/')) {
|
|
104
|
+
throw new TypeError(`Route paths must start with \`/\`: ${path}`);
|
|
105
|
+
}
|
|
106
|
+
if (![...path].every(characterIsAcceptableForRoutePath)) {
|
|
107
|
+
throw new TypeError(
|
|
108
|
+
`Route paths may contain only ${ACCEPTABLE_ROUTE_PATH_CHARS_HUMAN_DESCRIPTION}: ${path}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function routesOverlap(a: RouteSpec, b: RouteSpec) {
|
|
114
|
+
const methodsMatch = (left: HttpMethod, right: HttpMethod) => {
|
|
115
|
+
if (left.tag !== right.tag) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (left.tag === 'Extension' && right.tag === 'Extension') {
|
|
119
|
+
return left.value === right.value;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
a.path === b.path &&
|
|
126
|
+
(a.method.tag === 'Any' ||
|
|
127
|
+
b.method.tag === 'Any' ||
|
|
128
|
+
(a.method.tag === 'Method' &&
|
|
129
|
+
b.method.tag === 'Method' &&
|
|
130
|
+
methodsMatch(a.method.value, b.method.value)))
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function joinPaths(prefix: string, suffix: string) {
|
|
135
|
+
if (prefix === '/') {
|
|
136
|
+
return suffix;
|
|
137
|
+
}
|
|
138
|
+
if (suffix === '/') {
|
|
139
|
+
return prefix;
|
|
140
|
+
}
|
|
141
|
+
let prefixEnd = prefix.length;
|
|
142
|
+
while (prefixEnd > 0 && prefix[prefixEnd - 1] === '/') {
|
|
143
|
+
prefixEnd--;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let suffixStart = 0;
|
|
147
|
+
while (suffixStart < suffix.length && suffix[suffixStart] === '/') {
|
|
148
|
+
suffixStart++;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const joinedPrefix = prefix.slice(0, prefixEnd);
|
|
152
|
+
const joinedSuffix = suffix.slice(suffixStart);
|
|
153
|
+
return `${joinedPrefix}/${joinedSuffix}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export class Request {
|
|
157
|
+
#body: string | Uint8Array | null;
|
|
158
|
+
#inner: RequestInner;
|
|
159
|
+
|
|
160
|
+
constructor(url: URL | string, init: RequestInit = {}) {
|
|
161
|
+
this.#body = coerceRequestBody(init.body);
|
|
162
|
+
this.#inner = {
|
|
163
|
+
headers: new Headers(init.headers as any),
|
|
164
|
+
method: init.method ?? 'GET',
|
|
165
|
+
uri: '' + url,
|
|
166
|
+
version: init.version ?? { tag: 'Http11' },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static [makeRequest](body: BodyInit | null, inner: RequestInner) {
|
|
171
|
+
const me = new Request(inner.uri);
|
|
172
|
+
me.#body = coerceRequestBody(body);
|
|
173
|
+
me.#inner = inner;
|
|
174
|
+
return me;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get headers(): Headers {
|
|
178
|
+
return this.#inner.headers;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
get method(): string {
|
|
182
|
+
return this.#inner.method;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
get uri(): string {
|
|
186
|
+
return this.#inner.uri;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
get url(): string {
|
|
190
|
+
return this.#inner.uri;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
get version(): HttpVersion {
|
|
194
|
+
return this.#inner.version;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
arrayBuffer(): ArrayBuffer {
|
|
198
|
+
return this.bytes().buffer as ArrayBuffer;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
bytes(): Uint8Array {
|
|
202
|
+
return requestBodyToBytes(this.#body);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
json(): any {
|
|
206
|
+
return JSON.parse(this.text());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
text(): string {
|
|
210
|
+
return requestBodyToText(this.#body);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface HandlerContext<S extends UntypedSchemaDef = UntypedSchemaDef> {
|
|
215
|
+
readonly timestamp: Timestamp;
|
|
216
|
+
readonly http: HttpClient;
|
|
217
|
+
readonly identity: Identity;
|
|
218
|
+
readonly random: Random;
|
|
219
|
+
withTx<T>(body: (ctx: TransactionCtx<S>) => T): T;
|
|
220
|
+
newUuidV4(): Uuid;
|
|
221
|
+
newUuidV7(): Uuid;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export type HandlerFn<S extends UntypedSchemaDef = UntypedSchemaDef> = (
|
|
225
|
+
ctx: HandlerContext<S>,
|
|
226
|
+
req: Request
|
|
227
|
+
) => SyncResponse;
|
|
228
|
+
|
|
229
|
+
export interface HttpHandlerExport<
|
|
230
|
+
S extends UntypedSchemaDef = UntypedSchemaDef,
|
|
231
|
+
> extends ModuleExport {
|
|
232
|
+
[httpHandlerFn]: HandlerFn<S>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const exportedHttpHandlerObjects = new WeakSet<object>();
|
|
236
|
+
|
|
237
|
+
export interface HttpHandlerOpts {
|
|
238
|
+
name: string;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export class Router {
|
|
242
|
+
#routes: RouteSpec[];
|
|
243
|
+
|
|
244
|
+
constructor(routes: RouteSpec[] = []) {
|
|
245
|
+
this.#routes = routes;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
get(path: string, handler: HttpHandlerExport<any>) {
|
|
249
|
+
return this.addRoute(
|
|
250
|
+
{ tag: 'Method', value: { tag: 'Get' } },
|
|
251
|
+
path,
|
|
252
|
+
handler
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
head(path: string, handler: HttpHandlerExport<any>) {
|
|
257
|
+
return this.addRoute(
|
|
258
|
+
{ tag: 'Method', value: { tag: 'Head' } },
|
|
259
|
+
path,
|
|
260
|
+
handler
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
options(path: string, handler: HttpHandlerExport<any>) {
|
|
265
|
+
return this.addRoute(
|
|
266
|
+
{ tag: 'Method', value: { tag: 'Options' } },
|
|
267
|
+
path,
|
|
268
|
+
handler
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
put(path: string, handler: HttpHandlerExport<any>) {
|
|
273
|
+
return this.addRoute(
|
|
274
|
+
{ tag: 'Method', value: { tag: 'Put' } },
|
|
275
|
+
path,
|
|
276
|
+
handler
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
delete(path: string, handler: HttpHandlerExport<any>) {
|
|
281
|
+
return this.addRoute(
|
|
282
|
+
{ tag: 'Method', value: { tag: 'Delete' } },
|
|
283
|
+
path,
|
|
284
|
+
handler
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
post(path: string, handler: HttpHandlerExport<any>) {
|
|
289
|
+
return this.addRoute(
|
|
290
|
+
{ tag: 'Method', value: { tag: 'Post' } },
|
|
291
|
+
path,
|
|
292
|
+
handler
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
patch(path: string, handler: HttpHandlerExport<any>) {
|
|
297
|
+
return this.addRoute(
|
|
298
|
+
{ tag: 'Method', value: { tag: 'Patch' } },
|
|
299
|
+
path,
|
|
300
|
+
handler
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
any(path: string, handler: HttpHandlerExport<any>) {
|
|
305
|
+
return this.addRoute({ tag: 'Any' }, path, handler);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
nest(path: string, subRouter: Router) {
|
|
309
|
+
assertValidPath(path);
|
|
310
|
+
if (this.#routes.some(route => route.path.startsWith(path))) {
|
|
311
|
+
throw new TypeError(
|
|
312
|
+
`Cannot nest router at \`${path}\`; existing routes overlap with nested path`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
let merged = new Router(this.#routes);
|
|
316
|
+
for (const route of subRouter.#routes) {
|
|
317
|
+
merged = merged.addRoute(
|
|
318
|
+
route.method,
|
|
319
|
+
joinPaths(path, route.path),
|
|
320
|
+
route.handler
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return merged;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
merge(otherRouter: Router) {
|
|
327
|
+
let merged = new Router(this.#routes);
|
|
328
|
+
for (const route of otherRouter.#routes) {
|
|
329
|
+
merged = merged.addRoute(route.method, route.path, route.handler);
|
|
330
|
+
}
|
|
331
|
+
return merged;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
intoRoutes() {
|
|
335
|
+
return this.#routes.slice();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private addRoute(
|
|
339
|
+
method: MethodOrAny,
|
|
340
|
+
path: string,
|
|
341
|
+
handler: HttpHandlerExport<any>
|
|
342
|
+
) {
|
|
343
|
+
assertValidPath(path);
|
|
344
|
+
const candidate = { method, path, handler };
|
|
345
|
+
if (this.#routes.some(route => routesOverlap(route, candidate))) {
|
|
346
|
+
throw new TypeError(`Route conflict for \`${path}\``);
|
|
347
|
+
}
|
|
348
|
+
return new Router([...this.#routes, candidate]);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function makeHttpHandlerExport<S extends UntypedSchemaDef>(
|
|
353
|
+
ctx: SchemaInner,
|
|
354
|
+
opts: HttpHandlerOpts | undefined,
|
|
355
|
+
fn: HandlerFn<S>
|
|
356
|
+
): HttpHandlerExport<S> {
|
|
357
|
+
const handlerExport = {
|
|
358
|
+
[httpHandlerFn]: fn,
|
|
359
|
+
[exportContext]: ctx,
|
|
360
|
+
[registerExport](ctx: SchemaInner, exportName: string) {
|
|
361
|
+
if (exportedHttpHandlerObjects.has(handlerExport)) {
|
|
362
|
+
throw new TypeError(
|
|
363
|
+
`HTTP handler '${exportName}' was exported more than once`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
exportedHttpHandlerObjects.add(handlerExport);
|
|
367
|
+
registerHttpHandler(ctx, exportName, fn, opts);
|
|
368
|
+
ctx.httpHandlerExports.set(
|
|
369
|
+
handlerExport as HttpHandlerExport<UntypedSchemaDef>,
|
|
370
|
+
exportName
|
|
371
|
+
);
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
return handlerExport as HttpHandlerExport<S>;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function makeHttpRouterExport(
|
|
378
|
+
ctx: SchemaInner,
|
|
379
|
+
router: Router
|
|
380
|
+
): ModuleExport {
|
|
381
|
+
return {
|
|
382
|
+
[exportContext]: ctx,
|
|
383
|
+
[registerExport](ctx: SchemaInner) {
|
|
384
|
+
ctx.pendingHttpRoutes.push(...router.intoRoutes());
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function registerHttpHandler<S extends UntypedSchemaDef>(
|
|
390
|
+
ctx: SchemaInner,
|
|
391
|
+
exportName: string,
|
|
392
|
+
fn: HandlerFn<S>,
|
|
393
|
+
opts?: HttpHandlerOpts
|
|
394
|
+
) {
|
|
395
|
+
ctx.defineHttpHandler(exportName);
|
|
396
|
+
ctx.moduleDef.httpHandlers.push({ sourceName: exportName });
|
|
397
|
+
|
|
398
|
+
if (opts?.name != null) {
|
|
399
|
+
ctx.moduleDef.explicitNames.entries.push({
|
|
400
|
+
tag: 'Function',
|
|
401
|
+
value: {
|
|
402
|
+
sourceName: exportName,
|
|
403
|
+
canonicalName: opts.name,
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (!fn.name) {
|
|
409
|
+
Object.defineProperty(fn, 'name', { value: exportName, writable: false });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
ctx.httpHandlers.push(fn as HandlerFn<UntypedSchemaDef>);
|
|
413
|
+
}
|