toiljs 0.0.60 → 0.0.61

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 (119) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/CHANGELOG.md +5 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +2 -2
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/index.d.ts +1 -1
  7. package/build/client/index.js +1 -1
  8. package/build/client/routing/mount.js +12 -1
  9. package/build/client/ssr/markers.d.ts +1 -0
  10. package/build/client/ssr/markers.js +3 -0
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/config.d.ts +21 -0
  13. package/build/compiler/config.js +35 -0
  14. package/build/compiler/docs.d.ts +2 -1
  15. package/build/compiler/docs.js +33 -304
  16. package/build/compiler/index.d.ts +13 -0
  17. package/build/compiler/index.js +113 -21
  18. package/build/compiler/template-build.d.ts +21 -1
  19. package/build/compiler/template-build.js +110 -26
  20. package/build/compiler/toil-docs.generated.d.ts +1 -0
  21. package/build/compiler/toil-docs.generated.js +20 -0
  22. package/build/devserver/.tsbuildinfo +1 -1
  23. package/build/devserver/daemon/catalog.d.ts +26 -0
  24. package/build/devserver/daemon/catalog.js +48 -0
  25. package/build/devserver/daemon/cron.d.ts +4 -0
  26. package/build/devserver/daemon/cron.js +50 -0
  27. package/build/devserver/daemon/host.d.ts +37 -0
  28. package/build/devserver/daemon/host.js +94 -0
  29. package/build/devserver/daemon/index.d.ts +34 -0
  30. package/build/devserver/daemon/index.js +241 -0
  31. package/build/devserver/db/catalog.d.ts +2 -1
  32. package/build/devserver/db/catalog.js +44 -44
  33. package/build/devserver/db/database.d.ts +27 -11
  34. package/build/devserver/db/database.js +539 -169
  35. package/build/devserver/db/index.d.ts +1 -1
  36. package/build/devserver/db/index.js +1 -1
  37. package/build/devserver/db/routeKinds.d.ts +8 -0
  38. package/build/devserver/db/routeKinds.js +139 -0
  39. package/build/devserver/db/types.d.ts +64 -1
  40. package/build/devserver/db/types.js +33 -1
  41. package/build/devserver/index.d.ts +10 -0
  42. package/build/devserver/index.js +7 -0
  43. package/build/devserver/mstore/store.d.ts +18 -0
  44. package/build/devserver/mstore/store.js +82 -0
  45. package/build/devserver/runtime/host.d.ts +6 -0
  46. package/build/devserver/runtime/host.js +45 -1
  47. package/build/devserver/runtime/module.d.ts +1 -0
  48. package/build/devserver/runtime/module.js +27 -1
  49. package/build/devserver/server.d.ts +6 -0
  50. package/build/devserver/server.js +59 -0
  51. package/build/devserver/ssr.d.ts +25 -0
  52. package/build/devserver/ssr.js +114 -0
  53. package/build/devserver/wasm/sections.d.ts +2 -0
  54. package/build/devserver/wasm/sections.js +42 -0
  55. package/build/devserver/wasm/surface.d.ts +18 -0
  56. package/build/devserver/wasm/surface.js +41 -0
  57. package/docs/README.md +4 -4
  58. package/docs/auth-todo.md +6 -6
  59. package/docs/caching.md +5 -5
  60. package/docs/cli.md +15 -0
  61. package/docs/client.md +40 -0
  62. package/docs/crypto.md +4 -4
  63. package/docs/data.md +6 -6
  64. package/docs/email.md +28 -28
  65. package/docs/environment.md +10 -10
  66. package/docs/index.md +26 -0
  67. package/docs/ratelimit.md +10 -10
  68. package/docs/routing.md +2 -2
  69. package/docs/server.md +61 -0
  70. package/docs/ssr.md +561 -113
  71. package/docs/styling.md +22 -0
  72. package/docs/time.md +1 -1
  73. package/eslint.config.js +10 -1
  74. package/examples/basic/client/components/Header.tsx +3 -0
  75. package/examples/basic/client/routes/features/actions.tsx +0 -2
  76. package/examples/basic/client/routes/hello.tsx +89 -19
  77. package/examples/basic/client/styles/main.css +48 -0
  78. package/examples/basic/server/SsrHelloRender.ts +97 -0
  79. package/examples/basic/server/main.ts +5 -0
  80. package/examples/basic/server/streams/Echo.ts +49 -0
  81. package/package.json +12 -10
  82. package/scripts/gen-toil-docs.mjs +96 -0
  83. package/src/cli/create.ts +2 -2
  84. package/src/client/index.ts +1 -1
  85. package/src/client/routing/mount.tsx +18 -2
  86. package/src/client/ssr/markers.tsx +22 -0
  87. package/src/compiler/config.ts +88 -2
  88. package/src/compiler/docs.ts +47 -308
  89. package/src/compiler/index.ts +236 -32
  90. package/src/compiler/ssr-codegen.ts +1 -1
  91. package/src/compiler/template-build.ts +247 -46
  92. package/src/compiler/toil-docs.generated.ts +26 -0
  93. package/src/devserver/daemon/catalog.ts +120 -0
  94. package/src/devserver/daemon/cron.ts +87 -0
  95. package/src/devserver/daemon/host.ts +224 -0
  96. package/src/devserver/daemon/index.ts +349 -0
  97. package/src/devserver/db/catalog.ts +61 -53
  98. package/src/devserver/db/database.ts +613 -149
  99. package/src/devserver/db/index.ts +1 -1
  100. package/src/devserver/db/routeKinds.ts +147 -0
  101. package/src/devserver/db/types.ts +65 -2
  102. package/src/devserver/index.ts +12 -0
  103. package/src/devserver/mstore/store.ts +121 -0
  104. package/src/devserver/runtime/host.ts +92 -1
  105. package/src/devserver/runtime/module.ts +35 -1
  106. package/src/devserver/server.ts +101 -0
  107. package/src/devserver/ssr.ts +166 -0
  108. package/src/devserver/wasm/sections.ts +59 -0
  109. package/src/devserver/wasm/surface.ts +88 -0
  110. package/test/daemon-build.test.ts +198 -0
  111. package/test/daemon-catalog.test.ts +265 -0
  112. package/test/daemon-emulation.test.ts +216 -0
  113. package/test/devserver-database.test.ts +396 -5
  114. package/test/email-preview.test.ts +6 -1
  115. package/test/fixtures/daemon-app.ts +56 -0
  116. package/test/global-setup.ts +17 -0
  117. package/test/ssr-render.test.ts +94 -27
  118. package/test/ssr-template.test.tsx +44 -1
  119. package/vitest.config.ts +3 -0
@@ -1,3 +1,3 @@
1
1
  export { __resetDbForTests, __setDbCatalogForTests, buildDatabaseImports, configureDbPersistence, DevDatabase, devDb, persistDb, setDbCatalog, } from './database.js';
2
2
  export { parseCatalog } from './catalog.js';
3
- export { type DbDevState, freshDbState } from './types.js';
3
+ export { CollectionFamily, DbFunctionKind, type DbDevState, freshDbState } from './types.js';
@@ -1,3 +1,3 @@
1
1
  export { __resetDbForTests, __setDbCatalogForTests, buildDatabaseImports, configureDbPersistence, DevDatabase, devDb, persistDb, setDbCatalog, } from './database.js';
2
2
  export { parseCatalog } from './catalog.js';
3
- export { freshDbState } from './types.js';
3
+ export { CollectionFamily, DbFunctionKind, freshDbState } from './types.js';
@@ -0,0 +1,8 @@
1
+ import { DbFunctionKind } from './types.js';
2
+ export interface RouteKindEntry {
3
+ readonly method: number;
4
+ readonly kind: DbFunctionKind;
5
+ readonly pattern: string;
6
+ }
7
+ export declare function parseRouteKinds(wasm: Buffer): readonly RouteKindEntry[];
8
+ export declare function routeKindForRequest(routes: readonly RouteKindEntry[], method: string, path: string): DbFunctionKind | null;
@@ -0,0 +1,139 @@
1
+ import { customSection } from '../wasm/sections.js';
2
+ import { DbFunctionKind } from './types.js';
3
+ const SECTION = 'toildb.route_kinds';
4
+ const VERSION = 1;
5
+ const MAX_SECTION_BYTES = 128 * 1024;
6
+ const MAX_ROUTES = 2048;
7
+ const MAX_PATTERN_BYTES = 2048;
8
+ const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
9
+ const METHOD_CODES = {
10
+ GET: 0,
11
+ POST: 1,
12
+ PUT: 2,
13
+ DELETE: 3,
14
+ PATCH: 4,
15
+ HEAD: 5,
16
+ OPTIONS: 6,
17
+ };
18
+ export function parseRouteKinds(wasm) {
19
+ let section;
20
+ try {
21
+ section = customSection(wasm, SECTION);
22
+ }
23
+ catch {
24
+ return [];
25
+ }
26
+ if (section === null)
27
+ return [];
28
+ if (section.length > MAX_SECTION_BYTES)
29
+ return [];
30
+ const r = new Reader(section);
31
+ const version = r.u16();
32
+ if (!r.ok || version !== VERSION)
33
+ return [];
34
+ const count = r.u16();
35
+ if (!r.ok || count > MAX_ROUTES)
36
+ return [];
37
+ const routes = [];
38
+ for (let i = 0; i < count && r.ok; i++) {
39
+ const method = r.u8();
40
+ const kindByte = r.u8();
41
+ const pattern = r.string();
42
+ const kind = kindByte === 0 ? DbFunctionKind.Query : kindByte === 1 ? DbFunctionKind.Action : null;
43
+ if (!r.ok || method < 0 || method > 6 || kind === null || !validPattern(pattern))
44
+ return [];
45
+ routes.push({ method, kind, pattern });
46
+ }
47
+ if (!r.ok || r.remaining() !== 0)
48
+ return [];
49
+ return routes;
50
+ }
51
+ export function routeKindForRequest(routes, method, path) {
52
+ const methodCode = METHOD_CODES[method.toUpperCase()];
53
+ if (methodCode === undefined)
54
+ return null;
55
+ for (const route of routes) {
56
+ if (route.method === methodCode && routeMatches(route.pattern, path))
57
+ return route.kind;
58
+ }
59
+ return null;
60
+ }
61
+ function validPattern(pattern) {
62
+ if (pattern.length === 0 || !pattern.startsWith('/'))
63
+ return false;
64
+ for (let i = 0; i < pattern.length; i++) {
65
+ const c = pattern.charCodeAt(i);
66
+ if (c < 0x20 || c > 0x7e)
67
+ return false;
68
+ }
69
+ return true;
70
+ }
71
+ function routeMatches(pattern, pathWithQuery) {
72
+ const q = pathWithQuery.indexOf('?');
73
+ const path = q >= 0 ? pathWithQuery.slice(0, q) : pathWithQuery;
74
+ const patternSegs = pattern.split('/').filter(Boolean);
75
+ const pathSegs = path.split('/').filter(Boolean);
76
+ if (patternSegs.length !== pathSegs.length)
77
+ return false;
78
+ for (let i = 0; i < patternSegs.length; i++) {
79
+ const p = patternSegs[i] ?? '';
80
+ const a = pathSegs[i] ?? '';
81
+ if (p.startsWith(':') && p.length > 1 && a.length > 0)
82
+ continue;
83
+ if (p !== a)
84
+ return false;
85
+ }
86
+ return true;
87
+ }
88
+ class Reader {
89
+ bytes;
90
+ pos = 0;
91
+ ok = true;
92
+ constructor(bytes) {
93
+ this.bytes = bytes;
94
+ }
95
+ remaining() {
96
+ return this.bytes.length - this.pos;
97
+ }
98
+ u8() {
99
+ if (!this.ok || this.pos + 1 > this.bytes.length) {
100
+ this.ok = false;
101
+ return 0;
102
+ }
103
+ return this.bytes[this.pos++] ?? 0;
104
+ }
105
+ u16() {
106
+ if (!this.ok || this.pos + 2 > this.bytes.length) {
107
+ this.ok = false;
108
+ return 0;
109
+ }
110
+ const out = this.bytes.readUInt16LE(this.pos);
111
+ this.pos += 2;
112
+ return out;
113
+ }
114
+ u32() {
115
+ if (!this.ok || this.pos + 4 > this.bytes.length) {
116
+ this.ok = false;
117
+ return 0;
118
+ }
119
+ const out = this.bytes.readUInt32LE(this.pos);
120
+ this.pos += 4;
121
+ return out;
122
+ }
123
+ string() {
124
+ const len = this.u32();
125
+ if (!this.ok || len > MAX_PATTERN_BYTES || this.pos + len > this.bytes.length) {
126
+ this.ok = false;
127
+ return '';
128
+ }
129
+ try {
130
+ const out = UTF8_DECODER.decode(this.bytes.subarray(this.pos, this.pos + len));
131
+ this.pos += len;
132
+ return out;
133
+ }
134
+ catch {
135
+ this.ok = false;
136
+ return '';
137
+ }
138
+ }
139
+ }
@@ -1,7 +1,42 @@
1
+ export declare enum CollectionFamily {
2
+ Record = 0,
3
+ View = 1,
4
+ Events = 2,
5
+ Counter = 3,
6
+ Membership = 4,
7
+ Unique = 5,
8
+ Capacity = 6
9
+ }
10
+ export declare enum DbFunctionKind {
11
+ Query = "query",
12
+ Action = "action",
13
+ Derive = "derive",
14
+ Job = "job",
15
+ Admin = "admin"
16
+ }
17
+ export declare function isCollectionFamily(value: number): value is CollectionFamily;
18
+ export interface DevCollectionHandle {
19
+ name: string;
20
+ family: CollectionFamily;
21
+ schemaVersion: number;
22
+ replication: number;
23
+ placement: number;
24
+ fillMaxWaitMs: number;
25
+ fillAllowStale: boolean;
26
+ }
27
+ export type DbCatalogState = {
28
+ kind: 'no-section';
29
+ } | {
30
+ kind: 'malformed';
31
+ } | {
32
+ kind: 'present';
33
+ collections: Map<string, DevCollectionHandle>;
34
+ };
1
35
  export interface DbDevState {
2
- handles: string[];
36
+ handles: DevCollectionHandle[];
3
37
  lastResult: Buffer | null;
4
38
  lastResultVersion: number;
39
+ functionKind: DbFunctionKind;
5
40
  }
6
41
  export declare function freshDbState(): DbDevState;
7
42
  export interface Reservation {
@@ -19,6 +54,12 @@ export interface DbSnapshot {
19
54
  v: string;
20
55
  sv: number;
21
56
  }>;
57
+ recordIdem?: Record<string, {
58
+ requestHash: string;
59
+ state: 'pending' | 'done';
60
+ outcome?: RecordOutcomeSnapshot;
61
+ }>;
62
+ uniqueIdem?: Record<string, string>;
22
63
  views: Record<string, {
23
64
  v: string;
24
65
  sv: number;
@@ -28,6 +69,7 @@ export interface DbSnapshot {
28
69
  sv: number;
29
70
  }>>;
30
71
  counters: Record<string, string>;
72
+ counterIdem?: Record<string, string>;
31
73
  events: Record<string, {
32
74
  v: string;
33
75
  sv: number;
@@ -43,16 +85,37 @@ export interface DbSnapshot {
43
85
  }][];
44
86
  }>;
45
87
  }
88
+ export type RecordOutcomeSnapshot = {
89
+ kind: 'unit';
90
+ } | {
91
+ kind: 'value';
92
+ v: string;
93
+ sv: number;
94
+ } | {
95
+ kind: 'absent';
96
+ } | {
97
+ kind: 'already_exists';
98
+ } | {
99
+ kind: 'not_found';
100
+ } | {
101
+ kind: 'conflict';
102
+ };
46
103
  export declare const MAX_RESERVATIONS = 4096;
47
104
  export declare const MAX_RESERVATION_TTL_MS = 86400000;
48
105
  export declare const MAX_NAME = 512;
49
106
  export declare const MAX_KEY = 4096;
50
107
  export declare const MAX_VALUE: number;
108
+ export declare const DEFAULT_FILL_WAIT_MS = 50;
109
+ export declare const MAX_FILL_WAIT_MS = 60000;
51
110
  export declare function satI64(v: bigint): bigint;
52
111
  export declare const ABSENT = -2;
53
112
  export declare const TOO_SMALL = -1;
54
113
  export declare const INVALID_HANDLE = -1001;
55
114
  export declare const ALREADY_EXISTS = -1003;
56
115
  export declare const CONFLICT = -1004;
116
+ export declare const UNAVAILABLE = -1031;
57
117
  export declare const CODEC_ERR = -1006;
118
+ export declare const OP_NOT_ALLOWED_FOR_FAMILY = -1010;
119
+ export declare const OP_NOT_ALLOWED_IN_KIND = -1011;
58
120
  export declare const TOO_MANY_KEYS = -1020;
121
+ export declare const SCHEMA_UNAVAILABLE = -1070;
@@ -1,11 +1,39 @@
1
+ export var CollectionFamily;
2
+ (function (CollectionFamily) {
3
+ CollectionFamily[CollectionFamily["Record"] = 0] = "Record";
4
+ CollectionFamily[CollectionFamily["View"] = 1] = "View";
5
+ CollectionFamily[CollectionFamily["Events"] = 2] = "Events";
6
+ CollectionFamily[CollectionFamily["Counter"] = 3] = "Counter";
7
+ CollectionFamily[CollectionFamily["Membership"] = 4] = "Membership";
8
+ CollectionFamily[CollectionFamily["Unique"] = 5] = "Unique";
9
+ CollectionFamily[CollectionFamily["Capacity"] = 6] = "Capacity";
10
+ })(CollectionFamily || (CollectionFamily = {}));
11
+ export var DbFunctionKind;
12
+ (function (DbFunctionKind) {
13
+ DbFunctionKind["Query"] = "query";
14
+ DbFunctionKind["Action"] = "action";
15
+ DbFunctionKind["Derive"] = "derive";
16
+ DbFunctionKind["Job"] = "job";
17
+ DbFunctionKind["Admin"] = "admin";
18
+ })(DbFunctionKind || (DbFunctionKind = {}));
19
+ export function isCollectionFamily(value) {
20
+ return value >= CollectionFamily.Record && value <= CollectionFamily.Capacity;
21
+ }
1
22
  export function freshDbState() {
2
- return { handles: [], lastResult: null, lastResultVersion: -1 };
23
+ return {
24
+ handles: [],
25
+ lastResult: null,
26
+ lastResultVersion: -1,
27
+ functionKind: DbFunctionKind.Job,
28
+ };
3
29
  }
4
30
  export const MAX_RESERVATIONS = 4096;
5
31
  export const MAX_RESERVATION_TTL_MS = 86_400_000;
6
32
  export const MAX_NAME = 512;
7
33
  export const MAX_KEY = 4096;
8
34
  export const MAX_VALUE = 256 * 1024;
35
+ export const DEFAULT_FILL_WAIT_MS = 50;
36
+ export const MAX_FILL_WAIT_MS = 60_000;
9
37
  const I64_MIN = -(2n ** 63n);
10
38
  const I64_MAX = 2n ** 63n - 1n;
11
39
  export function satI64(v) {
@@ -16,5 +44,9 @@ export const TOO_SMALL = -1;
16
44
  export const INVALID_HANDLE = -1001;
17
45
  export const ALREADY_EXISTS = -1003;
18
46
  export const CONFLICT = -1004;
47
+ export const UNAVAILABLE = -1031;
19
48
  export const CODEC_ERR = -1006;
49
+ export const OP_NOT_ALLOWED_FOR_FAMILY = -1010;
50
+ export const OP_NOT_ALLOWED_IN_KIND = -1011;
20
51
  export const TOO_MANY_KEYS = -1020;
52
+ export const SCHEMA_UNAVAILABLE = -1070;
@@ -7,3 +7,13 @@ export type { WasmDispatchResult } from './runtime/module.js';
7
7
  export { buildHostImports, freshDispatchState } from './runtime/host.js';
8
8
  export type { DispatchState, MemoryRef } from './runtime/host.js';
9
9
  export type { ViteTarget } from './http/proxy.js';
10
+ export { DaemonHost, daemonEmulationEnabled } from './daemon/index.js';
11
+ export { parseDaemonCatalog } from './daemon/catalog.js';
12
+ export type { DaemonCatalog, ScheduledTask, CronMasks } from './daemon/catalog.js';
13
+ export { buildDaemonImports, freshDaemonState } from './daemon/host.js';
14
+ export type { DaemonState, DaemonRuntime, ResolvedDaemonConfig } from './daemon/host.js';
15
+ export { cronMatches, cronNeverFires, nextCronFireMs } from './daemon/cron.js';
16
+ export { parseSurface } from './wasm/surface.js';
17
+ export type { Surface, SurfaceFlags } from './wasm/surface.js';
18
+ export { customSection } from './wasm/sections.js';
19
+ export { DevMemoryStore, devMemoryStore } from './mstore/store.js';
@@ -2,3 +2,10 @@ export { startDevServer } from './server.js';
2
2
  export { METHOD_CODES, encodeRequestEnvelope, decodeResponseEnvelope, unpackHandleResult, } from './http/envelope.js';
3
3
  export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './runtime/module.js';
4
4
  export { buildHostImports, freshDispatchState } from './runtime/host.js';
5
+ export { DaemonHost, daemonEmulationEnabled } from './daemon/index.js';
6
+ export { parseDaemonCatalog } from './daemon/catalog.js';
7
+ export { buildDaemonImports, freshDaemonState } from './daemon/host.js';
8
+ export { cronMatches, cronNeverFires, nextCronFireMs } from './daemon/cron.js';
9
+ export { parseSurface } from './wasm/surface.js';
10
+ export { customSection } from './wasm/sections.js';
11
+ export { DevMemoryStore, devMemoryStore } from './mstore/store.js';
@@ -0,0 +1,18 @@
1
+ export declare class DevMemoryStore {
2
+ private readonly map;
3
+ private now;
4
+ private live;
5
+ private exp;
6
+ get(key: string): Buffer | null;
7
+ set(key: string, value: Buffer, ttlSecs: number): void;
8
+ delete(key: string): boolean;
9
+ incr(key: string, delta: bigint, ttlSecs: number): bigint | null;
10
+ cas(key: string, expect: Buffer | null, next: Buffer, ttlSecs: number): boolean;
11
+ expire(key: string, ttlSecs: number): boolean;
12
+ scan(prefix: string, cursor: bigint): {
13
+ next: bigint;
14
+ keys: string[];
15
+ } | null;
16
+ __reset(): void;
17
+ }
18
+ export declare const devMemoryStore: DevMemoryStore;
@@ -0,0 +1,82 @@
1
+ export class DevMemoryStore {
2
+ map = new Map();
3
+ now() {
4
+ return Date.now();
5
+ }
6
+ live(key) {
7
+ const e = this.map.get(key);
8
+ if (e === undefined)
9
+ return null;
10
+ if (e.expiresAtMs !== 0 && e.expiresAtMs <= this.now()) {
11
+ this.map.delete(key);
12
+ return null;
13
+ }
14
+ return e;
15
+ }
16
+ exp(ttlSecs) {
17
+ return ttlSecs > 0 ? this.now() + ttlSecs * 1000 : 0;
18
+ }
19
+ get(key) {
20
+ const e = this.live(key);
21
+ return e ? e.value : null;
22
+ }
23
+ set(key, value, ttlSecs) {
24
+ this.map.set(key, { value: Buffer.from(value), expiresAtMs: this.exp(ttlSecs) });
25
+ }
26
+ delete(key) {
27
+ return this.map.delete(key);
28
+ }
29
+ incr(key, delta, ttlSecs) {
30
+ const e = this.live(key);
31
+ let cur = 0n;
32
+ if (e !== null) {
33
+ const s = e.value.toString('utf8').trim();
34
+ if (!/^-?\d+$/.test(s))
35
+ return null;
36
+ try {
37
+ cur = BigInt(s);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ const next = BigInt.asIntN(64, cur + delta);
44
+ this.map.set(key, {
45
+ value: Buffer.from(next.toString(), 'utf8'),
46
+ expiresAtMs: ttlSecs > 0 ? this.exp(ttlSecs) : (e?.expiresAtMs ?? 0),
47
+ });
48
+ return next;
49
+ }
50
+ cas(key, expect, next, ttlSecs) {
51
+ const e = this.live(key);
52
+ if (expect === null) {
53
+ if (e !== null)
54
+ return false;
55
+ }
56
+ else if (e === null || !e.value.equals(expect)) {
57
+ return false;
58
+ }
59
+ this.map.set(key, { value: Buffer.from(next), expiresAtMs: this.exp(ttlSecs) });
60
+ return true;
61
+ }
62
+ expire(key, ttlSecs) {
63
+ const e = this.live(key);
64
+ if (!e)
65
+ return false;
66
+ e.expiresAtMs = this.exp(ttlSecs);
67
+ return true;
68
+ }
69
+ scan(prefix, cursor) {
70
+ const live = [...this.map.keys()].filter((k) => this.live(k) !== null && k.startsWith(prefix));
71
+ live.sort();
72
+ const start = Number(cursor);
73
+ if (start < 0 || start > live.length)
74
+ return null;
75
+ const batch = live.slice(start);
76
+ return { next: BigInt(live.length), keys: batch };
77
+ }
78
+ __reset() {
79
+ this.map.clear();
80
+ }
81
+ }
82
+ export const devMemoryStore = new DevMemoryStore();
@@ -16,4 +16,10 @@ export declare function freshDispatchState(): DispatchState;
16
16
  export interface MemoryRef {
17
17
  memory: WebAssembly.Memory | null;
18
18
  }
19
+ export declare function readBytes(ref: MemoryRef, ptr: number, len: number): Buffer;
20
+ export declare function writeBytesOut(ref: MemoryRef, bytes: Buffer, outPtr: number, outCap: number): number;
21
+ export declare function buildEnvImports(ref: MemoryRef, _state: {
22
+ crypto: CryptoState;
23
+ db: DbDevState;
24
+ }): Record<string, (...a: never[]) => unknown>;
19
25
  export declare function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssembly.Imports;
@@ -32,12 +32,21 @@ function mem(ref) {
32
32
  throw new Error('host import called before memory was bound');
33
33
  return Buffer.from(ref.memory.buffer);
34
34
  }
35
- function readBytes(ref, ptr, len) {
35
+ export function readBytes(ref, ptr, len) {
36
36
  const m = mem(ref);
37
37
  if (ptr < 0 || len < 0 || ptr + len > m.length)
38
38
  throw new Error(`host import read out of bounds: ptr=${String(ptr)} len=${String(len)}`);
39
39
  return m.subarray(ptr, ptr + len);
40
40
  }
41
+ export function writeBytesOut(ref, bytes, outPtr, outCap) {
42
+ if (bytes.length > outCap)
43
+ return -1;
44
+ const m = mem(ref);
45
+ if (outPtr < 0 || outPtr + bytes.length > m.length)
46
+ throw new Error('host import write out of bounds');
47
+ bytes.copy(m, outPtr);
48
+ return bytes.length;
49
+ }
41
50
  function readGuestString(ref, ptr) {
42
51
  if (ptr === 0)
43
52
  return '';
@@ -80,6 +89,41 @@ function envLookup(ref, keyPtr, keyLen, outPtr, outCap, secure) {
80
89
  bytes.copy(m, outPtr);
81
90
  return bytes.length;
82
91
  }
92
+ export function buildEnvImports(ref, _state) {
93
+ return {
94
+ abort: (msgPtr, filePtr, line, col) => {
95
+ throw new WasmAbortError(readGuestString(ref, msgPtr), readGuestString(ref, filePtr), line, col);
96
+ },
97
+ env_get: (keyPtr, keyLen, outPtr, outCap) => envLookup(ref, keyPtr, keyLen, outPtr, outCap, false),
98
+ env_get_secure: (keyPtr, keyLen, outPtr, outCap) => envLookup(ref, keyPtr, keyLen, outPtr, outCap, true),
99
+ thread_spawn: (_startArg) => -1,
100
+ 'Date.now': () => BigInt(Date.now()),
101
+ email_send: (reqPtr, reqLen) => {
102
+ const raw = readBytes(ref, reqPtr, reqLen);
103
+ const svc = getEmailService();
104
+ if (svc === null) {
105
+ const to = parseEmailBlob(raw)?.to ?? '<unparsed>';
106
+ process.stdout.write(` ✉ dev email_send -> ${to} (no email config; not sent)\n`);
107
+ return EmailStatus.Sent;
108
+ }
109
+ const { status, parsed } = svc.prepare(raw);
110
+ if (parsed === null) {
111
+ process.stdout.write(` ✉ dev email_send -> ${EmailStatus[status]}\n`);
112
+ return status;
113
+ }
114
+ void svc
115
+ .deliver(parsed)
116
+ .then((s) => {
117
+ const label = s === EmailStatus.Sent ? 'sent' : EmailStatus[s];
118
+ process.stdout.write(` ✉ dev email_send -> ${parsed.to} (${label})\n`);
119
+ })
120
+ .catch((e) => {
121
+ process.stdout.write(` ✉ dev email_send -> ${parsed.to} (error: ${String(e)})\n`);
122
+ });
123
+ return EmailStatus.Sent;
124
+ },
125
+ };
126
+ }
83
127
  export function buildHostImports(ref, state) {
84
128
  return {
85
129
  env: {
@@ -12,6 +12,7 @@ export declare class WasmServerModule {
12
12
  private readonly wasmPath;
13
13
  private module;
14
14
  private loadedMtimeMs;
15
+ private routeKinds;
15
16
  constructor(wasmPath: string);
16
17
  get available(): boolean;
17
18
  refresh(): boolean;
@@ -1,10 +1,25 @@
1
1
  import fs from 'node:fs';
2
- import { persistDb, setDbCatalog } from '../db/index.js';
2
+ import { DbFunctionKind, persistDb, setDbCatalog } from '../db/index.js';
3
+ import { parseRouteKinds, routeKindForRequest } from '../db/routeKinds.js';
3
4
  import { decodeResponseEnvelope, encodeRequestEnvelope, unpackHandleResult, } from '../http/envelope.js';
4
5
  import { buildHostImports, freshDispatchState } from './host.js';
5
6
  export { WasmAbortError } from './host.js';
6
7
  export const UNHANDLED_HEADER = 'x-toil-unhandled';
7
8
  const WASM_PAGE = 65536;
9
+ function dbKindForHttpMethod(method) {
10
+ switch (method.toUpperCase()) {
11
+ case 'GET':
12
+ case 'HEAD':
13
+ case 'OPTIONS':
14
+ return DbFunctionKind.Query;
15
+ case 'POST':
16
+ case 'PUT':
17
+ case 'PATCH':
18
+ case 'DELETE':
19
+ default:
20
+ return DbFunctionKind.Action;
21
+ }
22
+ }
8
23
  const PROVIDED_IMPORTS = new Set([
9
24
  'abort',
10
25
  'set_status',
@@ -67,6 +82,7 @@ export class WasmServerModule {
67
82
  wasmPath;
68
83
  module = null;
69
84
  loadedMtimeMs = -1;
85
+ routeKinds = [];
70
86
  constructor(wasmPath) {
71
87
  this.wasmPath = wasmPath;
72
88
  }
@@ -80,6 +96,7 @@ export class WasmServerModule {
80
96
  }
81
97
  catch {
82
98
  this.module = null;
99
+ this.routeKinds = [];
83
100
  this.loadedMtimeMs = -1;
84
101
  return false;
85
102
  }
@@ -90,6 +107,7 @@ export class WasmServerModule {
90
107
  this.assertImportSurface(module);
91
108
  this.assertExportSurface(module);
92
109
  setDbCatalog(bytes);
110
+ this.routeKinds = parseRouteKinds(bytes);
93
111
  this.module = module;
94
112
  this.loadedMtimeMs = mtimeMs;
95
113
  return true;
@@ -101,6 +119,13 @@ export class WasmServerModule {
101
119
  const ref = { memory: null };
102
120
  const state = freshDispatchState();
103
121
  state.clientIp = req.clientIp ?? '';
122
+ const routeKind = routeKindForRequest(this.routeKinds, req.method, req.path);
123
+ state.db.functionKind =
124
+ this.routeKinds.length === 0
125
+ ? DbFunctionKind.Job
126
+ : routeKind === DbFunctionKind.Query
127
+ ? DbFunctionKind.Query
128
+ : dbKindForHttpMethod(req.method);
104
129
  const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
105
130
  const exports = instance.exports;
106
131
  ref.memory = exports.memory;
@@ -132,6 +157,7 @@ export class WasmServerModule {
132
157
  const ref = { memory: null };
133
158
  const state = freshDispatchState();
134
159
  state.clientIp = req.clientIp ?? '';
160
+ state.db.functionKind = DbFunctionKind.Query;
135
161
  const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
136
162
  const exports = instance.exports;
137
163
  if (typeof exports.render !== 'function')
@@ -1,13 +1,19 @@
1
1
  import type { EmailBackendConfig } from 'toiljs/shared';
2
+ import type { ResolvedDaemonConfig } from './daemon/host.js';
2
3
  import { type ViteTarget } from './http/proxy.js';
4
+ import { type DevSsrTemplate } from './ssr.js';
3
5
  export interface DevServerOptions {
4
6
  readonly root: string;
5
7
  readonly port: number;
6
8
  readonly host?: string;
7
9
  readonly wasmFile: string;
10
+ readonly coldWasmFile?: string;
11
+ readonly nodeMode?: string;
12
+ readonly daemon?: ResolvedDaemonConfig;
8
13
  readonly vite: ViteTarget;
9
14
  readonly maxBodyLength?: number;
10
15
  readonly email?: EmailBackendConfig;
16
+ readonly ssrTemplates?: readonly DevSsrTemplate[];
11
17
  }
12
18
  export interface RunningDevServer {
13
19
  readonly port: number;