toiljs 0.0.67 → 0.0.69

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 (103) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +63 -61
  3. package/build/backend/.tsbuildinfo +1 -1
  4. package/build/cli/.tsbuildinfo +1 -1
  5. package/build/cli/index.js +13 -1
  6. package/build/client/.tsbuildinfo +1 -1
  7. package/build/client/index.d.ts +2 -0
  8. package/build/client/index.js +1 -0
  9. package/build/client/rpc.js +21 -1
  10. package/build/client/stream/client.d.ts +11 -0
  11. package/build/client/stream/client.js +59 -0
  12. package/build/compiler/.tsbuildinfo +1 -1
  13. package/build/compiler/config.d.ts +2 -0
  14. package/build/compiler/config.js +9 -7
  15. package/build/compiler/index.d.ts +1 -0
  16. package/build/compiler/index.js +16 -2
  17. package/build/compiler/toil-docs.generated.js +5 -4
  18. package/build/devserver/.tsbuildinfo +1 -1
  19. package/build/devserver/daemon/runtime.d.ts +13 -0
  20. package/build/devserver/daemon/runtime.js +29 -0
  21. package/build/devserver/db/database.d.ts +1 -0
  22. package/build/devserver/db/database.js +10 -0
  23. package/build/devserver/db/derives.d.ts +7 -0
  24. package/build/devserver/db/derives.js +94 -0
  25. package/build/devserver/db/index.d.ts +1 -0
  26. package/build/devserver/db/index.js +1 -0
  27. package/build/devserver/db/types.d.ts +1 -0
  28. package/build/devserver/db/types.js +1 -0
  29. package/build/devserver/http/proxy.d.ts +5 -1
  30. package/build/devserver/http/proxy.js +39 -36
  31. package/build/devserver/http/runtime.d.ts +62 -0
  32. package/build/devserver/http/runtime.js +194 -0
  33. package/build/devserver/index.d.ts +2 -0
  34. package/build/devserver/index.js +1 -0
  35. package/build/devserver/production-ipc.d.ts +50 -0
  36. package/build/devserver/production-ipc.js +21 -0
  37. package/build/devserver/production-worker.d.ts +1 -0
  38. package/build/devserver/production-worker.js +73 -0
  39. package/build/devserver/production.d.ts +35 -0
  40. package/build/devserver/production.js +502 -0
  41. package/build/devserver/runtime/module.d.ts +5 -0
  42. package/build/devserver/runtime/module.js +47 -1
  43. package/build/devserver/server.d.ts +1 -0
  44. package/build/devserver/server.js +32 -145
  45. package/build/devserver/ssr.d.ts +2 -0
  46. package/build/devserver/ssr.js +19 -2
  47. package/build/devserver/stream/catalog.d.ts +20 -0
  48. package/build/devserver/stream/catalog.js +54 -0
  49. package/build/devserver/stream/host.d.ts +9 -0
  50. package/build/devserver/stream/host.js +15 -0
  51. package/build/devserver/stream/index.d.ts +37 -0
  52. package/build/devserver/stream/index.js +220 -0
  53. package/build/devserver/stream/manager.d.ts +34 -0
  54. package/build/devserver/stream/manager.js +103 -0
  55. package/build/devserver/stream/router.d.ts +25 -0
  56. package/build/devserver/stream/router.js +64 -0
  57. package/build/devserver/stream/wire.d.ts +5 -0
  58. package/build/devserver/stream/wire.js +33 -0
  59. package/build/devserver/stream/ws.d.ts +18 -0
  60. package/build/devserver/stream/ws.js +46 -0
  61. package/docs/cli.md +3 -1
  62. package/docs/derive.md +159 -0
  63. package/docs/getting-started.md +7 -7
  64. package/docs/index.md +1 -1
  65. package/docs/streams.md +46 -14
  66. package/examples/basic/server/routes/Guestbook.ts +38 -13
  67. package/package.json +2 -2
  68. package/src/cli/index.ts +14 -1
  69. package/src/client/index.ts +2 -0
  70. package/src/client/rpc.ts +25 -1
  71. package/src/client/stream/client.ts +109 -0
  72. package/src/compiler/config.ts +15 -7
  73. package/src/compiler/index.ts +24 -5
  74. package/src/compiler/toil-docs.generated.ts +5 -4
  75. package/src/devserver/daemon/runtime.ts +48 -0
  76. package/src/devserver/db/database.ts +14 -0
  77. package/src/devserver/db/derives.ts +121 -0
  78. package/src/devserver/db/index.ts +1 -0
  79. package/src/devserver/db/types.ts +6 -0
  80. package/src/devserver/http/proxy.ts +53 -39
  81. package/src/devserver/http/runtime.ts +287 -0
  82. package/src/devserver/index.ts +2 -0
  83. package/src/devserver/production-ipc.ts +63 -0
  84. package/src/devserver/production-worker.ts +83 -0
  85. package/src/devserver/production.ts +706 -0
  86. package/src/devserver/runtime/module.ts +95 -1
  87. package/src/devserver/server.ts +52 -201
  88. package/src/devserver/ssr.ts +23 -3
  89. package/src/devserver/stream/catalog.ts +106 -0
  90. package/src/devserver/stream/host.ts +42 -0
  91. package/src/devserver/stream/index.ts +308 -0
  92. package/src/devserver/stream/manager.ts +163 -0
  93. package/src/devserver/stream/router.ts +101 -0
  94. package/src/devserver/stream/wire.ts +58 -0
  95. package/src/devserver/stream/ws.ts +76 -0
  96. package/test/built-ssr.test.ts +98 -0
  97. package/test/devserver.test.ts +20 -4
  98. package/test/example-guestbook.test.ts +8 -5
  99. package/test/fixtures/stream-echo.ts +26 -0
  100. package/test/fixtures/stream-gate.ts +24 -0
  101. package/test/fixtures/stream-trap.ts +18 -0
  102. package/test/fixtures/stream-typed.ts +41 -0
  103. package/test/stream-emulation.test.ts +433 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "toiljs",
3
3
  "type": "module",
4
- "version": "0.0.67",
4
+ "version": "0.0.69",
5
5
  "author": "Dacely",
6
6
  "description": "The modern React framework: a file-based React frontend and a ToilScript-compiled WebAssembly backend.",
7
7
  "repository": {
@@ -134,7 +134,7 @@
134
134
  "nodemailer": "^9.0.1",
135
135
  "picocolors": "^1.1.1",
136
136
  "sharp": "^0.35.2",
137
- "toilscript": "^0.1.42",
137
+ "toilscript": "^0.1.44",
138
138
  "typescript-eslint": "^8.62.0",
139
139
  "vite": "^8.1.0",
140
140
  "vite-imagetools": "^10.0.1",
package/src/cli/index.ts CHANGED
@@ -19,6 +19,7 @@ interface Flags {
19
19
  root?: string;
20
20
  port?: number;
21
21
  host?: string;
22
+ threads?: number;
22
23
  name?: string;
23
24
  template?: Template;
24
25
  preprocessor?: Preprocessor;
@@ -51,6 +52,12 @@ function parseArgs(argv: string[]): Flags {
51
52
  case '--host':
52
53
  flags.host = argv[++i];
53
54
  break;
55
+ case '--threads':
56
+ case '--workers': {
57
+ const threads = Number(argv[++i]);
58
+ if (!Number.isNaN(threads)) flags.threads = threads;
59
+ break;
60
+ }
54
61
  case '--template':
55
62
  case '-t': {
56
63
  const t = argv[++i];
@@ -138,6 +145,7 @@ function printHelp(): void {
138
145
  bold('Options'),
139
146
  cmd('--root <dir>', 'project root (default: current directory)'),
140
147
  cmd('--port <n>', 'dev server port'),
148
+ cmd('--threads <n>', 'start: production HTTP worker count'),
141
149
  cmd('-t, --template', 'create: app | minimal'),
142
150
  cmd('--style <name>', 'create/configure: css | sass | less | stylus'),
143
151
  cmd('--tailwind', 'create/configure: enable Tailwind (--no-tailwind to remove)'),
@@ -226,7 +234,12 @@ async function main(): Promise<void> {
226
234
  case 'start': {
227
235
  banner();
228
236
  process.stdout.write(dim(' self-hosting the built app…') + '\n\n');
229
- const server = await start({ root: flags.root, port: flags.port, host: flags.host });
237
+ const server = await start({
238
+ root: flags.root,
239
+ port: flags.port,
240
+ host: flags.host,
241
+ threads: flags.threads,
242
+ });
230
243
  process.stdout.write(
231
244
  accent(' ➜ ') +
232
245
  bold(`http://localhost:${String(server.port)}`) +
@@ -79,6 +79,8 @@ export { matchRoute } from './routing/match.js';
79
79
  export type { RouteParams } from './routing/match.js';
80
80
  export { connectChannel, useChannel, resolveChannelUrl } from './channel/channel.js';
81
81
  export type { Channel, ChannelOptions, ChannelHook, ChannelData } from './channel/channel.js';
82
+ export { makeStreamClient } from './stream/client.js';
83
+ export type { StreamChannel, StreamConnectable, StreamClient } from './stream/client.js';
82
84
  export { useHead, useTitle, Head, mergeHead } from './head/head.js';
83
85
  export type { HeadSpec, MetaTag, LinkTag, ResolvedHead } from './head/head.js';
84
86
  export { resolveMetadata, useMetadata, Metadata } from './head/metadata.js';
package/src/client/rpc.ts CHANGED
@@ -24,7 +24,26 @@ function restMissingStub(path: string): unknown {
24
24
  return new Proxy(call, {
25
25
  get(_target, prop) {
26
26
  if (typeof prop === 'symbol' || prop === 'then') return undefined;
27
- return restMissingStub(`${path}.${String(prop)}`);
27
+ return restMissingStub(`${path}.${prop}`);
28
+ },
29
+ apply() {
30
+ return call();
31
+ },
32
+ });
33
+ }
34
+
35
+ /** A recursive proxy that throws on call, used when the stream client hasn't loaded. */
36
+ function streamMissingStub(path: string): unknown {
37
+ const call = (): never => {
38
+ throw new Error(
39
+ `toiljs Stream: ${path}() is unavailable. The generated stream client has not loaded - ` +
40
+ `import a type from your 'shared/server' (so the client attaches), or run 'npm run build:server'.`,
41
+ );
42
+ };
43
+ return new Proxy(call, {
44
+ get(_target, prop) {
45
+ if (typeof prop === 'symbol' || prop === 'then') return undefined;
46
+ return streamMissingStub(`${path}.${prop}`);
28
47
  },
29
48
  apply() {
30
49
  return call();
@@ -49,6 +68,11 @@ function rpcStub(path: string): unknown {
49
68
  const rest = (globalThis as { __toilRest?: unknown }).__toilRest;
50
69
  return rest !== undefined ? rest : restMissingStub('Server.REST');
51
70
  }
71
+ // `Server.Stream` surfaces the generated stream client attached by shared/server.ts.
72
+ if (path === 'Server' && prop === 'Stream') {
73
+ const stream = (globalThis as { __toilStream?: unknown }).__toilStream;
74
+ return stream !== undefined ? stream : streamMissingStub('Server.Stream');
75
+ }
52
76
  return rpcStub(`${path}.${prop}`);
53
77
  },
54
78
  apply() {
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Client runtime for the typed `@stream` channel (doc 08 section 8.2). `Server.Stream.<Class>.connect(path?)`
3
+ * opens a browser `WebSocket` to the class's `route` (same origin) and returns a channel:
4
+ * `onMessage` / `send` / `onClose` / `close`. A raw `@stream` channel sends `Uint8Array`; a typed
5
+ * `@stream({ message: T })` channel sends the `@data` class and encodes it on send (the per-class
6
+ * encoder the generated module passes). The inbound reply is always raw bytes. The generated
7
+ * `shared/server.ts` (toilscript hot pass) attaches `makeStreamClient(routes, undefined, encoders)` to
8
+ * `globalThis.__toilStream`, and `Server.Stream`
9
+ * (`rpc.ts`) surfaces it - the same wiring the REST client uses via `globalThis.__toilRest`.
10
+ */
11
+
12
+ /** A live `@stream` connection. `TSend` is the outbound message type: `Uint8Array` for a raw `@stream`,
13
+ * or the `@data` class for a typed `@stream({ message: T })` (the channel encodes it before sending).
14
+ * The inbound reply is ALWAYS raw bytes - the server's `StreamOutbound` is raw (doc 03 2.5). */
15
+ export interface StreamChannel<TSend = Uint8Array> {
16
+ /** Register the inbound-frame handler (one call per server reply frame). */
17
+ onMessage(cb: (data: Uint8Array) => void): void;
18
+ /** Send one outbound message (one `@message` on the server); a typed channel encodes it first. */
19
+ send(data: TSend): void;
20
+ /** Register the close handler (`code` is the `0x02xx` stream close code, or the WS code). */
21
+ onClose(cb: (code: number) => void): void;
22
+ /** Close the connection. */
23
+ close(): void;
24
+ }
25
+
26
+ /** The connect factory for one `@stream` class. `path` is the `@connect` path (default `''`). */
27
+ export interface StreamConnectable<TSend = Uint8Array> {
28
+ connect(path?: string): Promise<StreamChannel<TSend>>;
29
+ }
30
+
31
+ /** `Server.Stream`: one `connect()` factory per `@stream` class name. */
32
+ export type StreamClient = Record<string, StreamConnectable>;
33
+
34
+ /** Open a WebSocket to `url` and resolve a channel once the upgrade completes; reject if the socket
35
+ * closes/errors BEFORE it opens (a 421 redirect / wrong-node / unreachable). A server close AFTER
36
+ * open (e.g. a `@connect` reject or a guest reject) surfaces through `onClose(code)`. */
37
+ function connectStream<TSend = Uint8Array>(
38
+ url: string,
39
+ encode?: (msg: never) => Uint8Array,
40
+ ): Promise<StreamChannel<TSend>> {
41
+ return new Promise((resolve, reject) => {
42
+ const ws = new WebSocket(url);
43
+ ws.binaryType = 'arraybuffer';
44
+ let opened = false;
45
+ let messageCb: ((data: Uint8Array) => void) | undefined;
46
+ let closeCb: ((code: number) => void) | undefined;
47
+
48
+ const channel: StreamChannel<TSend> = {
49
+ onMessage: (cb): void => {
50
+ messageCb = cb;
51
+ },
52
+ onClose: (cb): void => {
53
+ closeCb = cb;
54
+ },
55
+ send: (data): void => {
56
+ if (ws.readyState !== WebSocket.OPEN) return;
57
+ // A typed channel encodes the @data message; a raw channel sends the bytes as-is.
58
+ const bytes = encode ? encode(data as never) : (data as unknown as Uint8Array);
59
+ ws.send(bytes as BufferSource);
60
+ },
61
+ close: (): void => {
62
+ ws.close();
63
+ },
64
+ };
65
+
66
+ ws.addEventListener('open', () => {
67
+ opened = true;
68
+ resolve(channel);
69
+ });
70
+ ws.addEventListener('message', (event: MessageEvent) => {
71
+ if (event.data instanceof ArrayBuffer) messageCb?.(new Uint8Array(event.data));
72
+ });
73
+ ws.addEventListener('close', (event: CloseEvent) => {
74
+ if (!opened) reject(new Error(`stream connect failed (closed ${String(event.code)})`));
75
+ else closeCb?.(event.code);
76
+ });
77
+ ws.addEventListener('error', () => {
78
+ if (!opened) reject(new Error('stream connect error'));
79
+ });
80
+ });
81
+ }
82
+
83
+ /** The same-origin WebSocket base (`ws://` / `wss://` per the page protocol). */
84
+ function defaultOrigin(): string {
85
+ const loc = globalThis.location;
86
+ return `${loc.protocol === 'https:' ? 'wss:' : 'ws:'}//${loc.host}`;
87
+ }
88
+
89
+ /**
90
+ * Build the `Server.Stream` client from the generated route map (`{ ClassName: route }`). `origin`
91
+ * defaults to the page origin. `encoders` carries one `@data` encoder per typed `@stream({ message: T })`
92
+ * class (the generated `(m) => m.encode()`); a class with no entry is a raw byte channel. The generated
93
+ * `shared/server.ts` calls this and assigns the result to `globalThis.__toilStream`.
94
+ */
95
+ export function makeStreamClient(
96
+ routes: Record<string, string>,
97
+ origin?: string,
98
+ encoders?: Record<string, (msg: never) => Uint8Array>,
99
+ ): StreamClient {
100
+ const base = origin ?? defaultOrigin();
101
+ const client: StreamClient = {};
102
+ for (const [name, route] of Object.entries(routes)) {
103
+ const encode = encoders?.[name];
104
+ client[name] = {
105
+ connect: (path = ''): Promise<StreamChannel> => connectStream(`${base}${route}${path}`, encode),
106
+ };
107
+ }
108
+ return client;
109
+ }
@@ -142,6 +142,11 @@ export interface ServerConfig {
142
142
  readonly nodeMode?: DevNodeMode;
143
143
  /** Daemon (L4) config mirror (dev / self-host). */
144
144
  readonly daemon?: DaemonConfig;
145
+ /**
146
+ * Self-host HTTP worker count for `toiljs start`. Default `auto`
147
+ * (`os.availableParallelism()`). Set `1` to disable the worker pool.
148
+ */
149
+ readonly threads?: number | 'auto';
145
150
  }
146
151
 
147
152
  /** Fully-resolved {@link DaemonConfig}; every field non-optional, defaults applied. */
@@ -199,6 +204,8 @@ export interface ResolvedToilConfig {
199
204
  readonly nodeMode: DevNodeMode;
200
205
  /** Daemon (L4) config mirror (dev / self-host), every field resolved. */
201
206
  readonly daemon: ResolvedDaemonConfig;
207
+ /** Self-host HTTP worker count for `toiljs start`. */
208
+ readonly threads: number | 'auto';
202
209
  /** Absolute path to the framework client runtime (`toiljs/client`). */
203
210
  readonly runtimePath: string;
204
211
  readonly vite: InlineConfig;
@@ -273,18 +280,13 @@ export async function loadConfig(
273
280
  email: user.server?.email ?? null,
274
281
  nodeMode: resolveNodeMode(user.server?.nodeMode),
275
282
  daemon: resolveDaemonConfig(user.server?.daemon),
283
+ threads: resolveThreads(user.server?.threads),
276
284
  runtimePath: resolveRuntimePath(),
277
285
  vite: client.vite ?? {},
278
286
  };
279
287
  }
280
288
 
281
- const DEV_NODE_MODES: readonly DevNodeMode[] = [
282
- 'hot',
283
- 'regional',
284
- 'continental',
285
- 'daemon',
286
- 'all',
287
- ];
289
+ const DEV_NODE_MODES: readonly DevNodeMode[] = ['hot', 'regional', 'continental', 'daemon', 'all'];
288
290
 
289
291
  /** A `nodeMode` outside the enum falls back to `all` with a warning (fail-soft:
290
292
  * the authoritative role is the edge's TCF + plan, so dev never bricks on it). */
@@ -319,3 +321,9 @@ function resolveDaemonConfig(d: DaemonConfig | undefined): ResolvedDaemonConfig
319
321
  maxTasks,
320
322
  };
321
323
  }
324
+
325
+ function resolveThreads(threads: number | 'auto' | undefined): number | 'auto' {
326
+ if (threads === undefined || threads === 'auto') return 'auto';
327
+ if (!Number.isFinite(threads)) return 'auto';
328
+ return Math.max(1, Math.min(128, Math.floor(threads)));
329
+ }
@@ -524,6 +524,8 @@ export interface ToilCommandOptions {
524
524
  readonly port?: number;
525
525
  /** Bind host for `start`. Defaults to loopback (`127.0.0.1`); pass `0.0.0.0` to expose. */
526
526
  readonly host?: string;
527
+ /** `start` only: production HTTP worker count. Defaults to `server.threads` / auto. */
528
+ readonly threads?: number;
527
529
  /** `build` only: build the server (regenerate `shared/server.ts` + the wasm) and skip the client. */
528
530
  readonly serverOnly?: boolean;
529
531
  }
@@ -699,6 +701,10 @@ export async function dev(opts: ToilCommandOptions = {}): Promise<ViteDevServer>
699
701
  // The daemon (cold) emulator drives `release-cold.wasm` per `nodeMode`; absent for a
700
702
  // project with no `@daemon` (the cold artifact never gets built, so the host stays idle).
701
703
  coldWasmFile: serverArtifacts(cfg.root).cold,
704
+ // The stream router serves `@stream`-route WebSocket upgrades from `release-stream.wasm` per
705
+ // `nodeMode`; the path points at the (maybe-not-yet-built) stream artifact, mtime-reloaded so
706
+ // a `@stream` build activates it, and harmless for a project with no `@stream` (no routes).
707
+ streamWasmFile: serverArtifacts(cfg.root).stream,
702
708
  nodeMode: cfg.nodeMode,
703
709
  daemon: cfg.daemon,
704
710
  vite: { host: '127.0.0.1', port: vitePort },
@@ -779,9 +785,9 @@ export async function build(opts: ToilCommandOptions = {}): Promise<void> {
779
785
  }
780
786
 
781
787
  /**
782
- * Self-hosts the built client over the high-performance hyper-express backend (uWebSockets.js),
783
- * serving the configured `outDir` with an SPA fallback plus a WebSocket channel. Requires a prior
784
- * `build`. Returns the running backend.
788
+ * Self-hosts the built app over the high-performance hyper-express backend (uWebSockets.js).
789
+ * Server projects use the built wasm + SSR templates before the SPA fallback; client-only projects
790
+ * use the static backend. Requires a prior `build`. Returns the running backend.
785
791
  */
786
792
  export async function start(opts: ToilCommandOptions = {}): Promise<RunningBackend> {
787
793
  const cfg = await loadConfig(opts);
@@ -789,8 +795,21 @@ export async function start(opts: ToilCommandOptions = {}): Promise<RunningBacke
789
795
  if (!fs.existsSync(path.join(outDir, 'index.html'))) {
790
796
  throw new Error(`No build found in ${outDir}. Run \`toiljs build\` first.`);
791
797
  }
792
- const { startBackend } = await import('toiljs/backend');
793
- return startBackend({ root: outDir, port: cfg.port, host: opts.host });
798
+ const wasmFile = serverWasmFile(cfg.root);
799
+ const { startBuiltServer } = await import('toiljs/devserver');
800
+ const artifacts = serverArtifacts(cfg.root);
801
+ return startBuiltServer({
802
+ root: cfg.root,
803
+ staticRoot: outDir,
804
+ wasmFile: fs.existsSync(wasmFile) ? wasmFile : undefined,
805
+ coldWasmFile: artifacts.cold,
806
+ nodeMode: cfg.nodeMode,
807
+ daemon: cfg.daemon,
808
+ threads: opts.threads ?? cfg.threads,
809
+ port: cfg.port,
810
+ host: opts.host,
811
+ email: cfg.email ?? undefined,
812
+ });
794
813
  }
795
814
 
796
815
  export { defineConfig, loadConfig, AiProvider } from './config.js';