toiljs 0.0.54 → 0.0.56

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 (105) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/build/backend/.tsbuildinfo +1 -1
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +9 -5
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/auth.js +1 -1
  7. package/build/client/components/Image.d.ts +1 -1
  8. package/build/client/dev/devtools.js +3 -1
  9. package/build/client/index.d.ts +2 -2
  10. package/build/client/index.js +2 -2
  11. package/build/client/routing/Router.js +1 -1
  12. package/build/client/routing/mount.js +1 -1
  13. package/build/compiler/.tsbuildinfo +1 -1
  14. package/build/compiler/docs.js +1 -1
  15. package/build/compiler/seo.js +1 -3
  16. package/build/compiler/template-build.js +1 -1
  17. package/build/devserver/.tsbuildinfo +1 -1
  18. package/build/devserver/cache.js +0 -0
  19. package/build/devserver/crypto.js +45 -17
  20. package/build/devserver/database.d.ts +8 -0
  21. package/build/devserver/database.js +416 -0
  22. package/build/devserver/email/caps.js +0 -0
  23. package/build/devserver/email/config.js +7 -2
  24. package/build/devserver/email/validate.js +1 -4
  25. package/build/devserver/host.d.ts +2 -0
  26. package/build/devserver/host.js +3 -2
  27. package/build/devserver/index.d.ts +1 -1
  28. package/build/devserver/index.js +3 -2
  29. package/build/devserver/module.js +52 -7
  30. package/build/devserver/proxy.js +2 -1
  31. package/build/io/.tsbuildinfo +1 -1
  32. package/build/io/codec.d.ts +5 -5
  33. package/build/io/codec.js +193 -77
  34. package/examples/basic/client/components/HoneycombBackground.tsx +1 -1
  35. package/examples/basic/client/public/images/logo.svg +37 -34
  36. package/examples/basic/client/public/index.html +14 -14
  37. package/examples/basic/client/routes/auth.tsx +18 -10
  38. package/examples/basic/client/routes/cookies.tsx +15 -24
  39. package/examples/basic/client/routes/crypto.tsx +4 -5
  40. package/examples/basic/client/routes/features/template/template.tsx +1 -1
  41. package/examples/basic/client/routes/hello.tsx +1 -1
  42. package/examples/basic/client/routes/pq.tsx +14 -14
  43. package/examples/basic/client/routes/rest.tsx +50 -1
  44. package/examples/basic/client/styles/main.css +25 -22
  45. package/examples/basic/client/toil.tsx +1 -1
  46. package/examples/basic/server/README.md +8 -8
  47. package/examples/basic/server/core/AppHandler.ts +4 -7
  48. package/examples/basic/server/main.ts +1 -0
  49. package/examples/basic/server/models/GuestEntry.ts +12 -0
  50. package/examples/basic/server/models/GuestbookView.ts +10 -0
  51. package/examples/basic/server/models/NewMessage.ts +6 -0
  52. package/examples/basic/server/routes/Auth.ts +50 -106
  53. package/examples/basic/server/routes/EnvDemo.ts +9 -3
  54. package/examples/basic/server/routes/Guestbook.ts +62 -0
  55. package/package.json +2 -2
  56. package/server/globals/auth.ts +3 -3
  57. package/server/globals/twofactor.ts +2 -1
  58. package/server/runtime/http/securecookies.ts +3 -2
  59. package/src/backend/index.ts +4 -2
  60. package/src/cli/doctor.ts +10 -3
  61. package/src/cli/notify.ts +1 -6
  62. package/src/cli/ui.ts +3 -3
  63. package/src/cli/version-check.ts +5 -1
  64. package/src/client/auth.ts +33 -10
  65. package/src/client/components/Form.tsx +2 -2
  66. package/src/client/components/Image.tsx +1 -1
  67. package/src/client/components/Script.tsx +1 -1
  68. package/src/client/components/Slot.tsx +1 -1
  69. package/src/client/dev/devtools.tsx +121 -54
  70. package/src/client/dev/error-overlay.tsx +7 -1
  71. package/src/client/head/metadata.ts +1 -1
  72. package/src/client/index.ts +13 -2
  73. package/src/client/routing/Router.tsx +2 -2
  74. package/src/client/routing/error-boundary.tsx +1 -1
  75. package/src/client/routing/loader.ts +2 -2
  76. package/src/client/routing/mount.tsx +5 -6
  77. package/src/compiler/docs.ts +1 -1
  78. package/src/compiler/email-preview.ts +1 -1
  79. package/src/compiler/generate.ts +1 -1
  80. package/src/compiler/seo.ts +1 -3
  81. package/src/compiler/ssg.ts +10 -4
  82. package/src/compiler/template-build.ts +2 -7
  83. package/src/compiler/template.ts +1 -4
  84. package/src/compiler/vite.ts +1 -1
  85. package/src/devserver/cache.ts +0 -0
  86. package/src/devserver/crypto.ts +140 -51
  87. package/src/devserver/database.ts +600 -0
  88. package/src/devserver/dotenv.ts +10 -2
  89. package/src/devserver/email/caps.ts +0 -0
  90. package/src/devserver/email/config.ts +8 -2
  91. package/src/devserver/email/index.ts +3 -3
  92. package/src/devserver/email/validate.ts +1 -4
  93. package/src/devserver/envelope.ts +3 -3
  94. package/src/devserver/host.ts +22 -9
  95. package/src/devserver/index.ts +15 -6
  96. package/src/devserver/module.ts +59 -11
  97. package/src/devserver/proxy.ts +5 -7
  98. package/src/io/codec.ts +226 -83
  99. package/test/devserver-database.test.ts +364 -0
  100. package/test/devserver-pqauth.test.ts +5 -65
  101. package/test/example-guestbook.test.ts +78 -0
  102. package/test/pqauth-e2e.test.ts +6 -6
  103. package/build/devserver/kv.d.ts +0 -3
  104. package/build/devserver/kv.js +0 -53
  105. package/src/devserver/kv.ts +0 -93
@@ -69,15 +69,15 @@ export function encodeRequestEnvelope(req: EnvelopeRequest): Buffer {
69
69
  if (path.length > U16_MAX) throw new Error(`path too long: ${String(path.length)} bytes`);
70
70
  if (req.headers.length > U16_MAX)
71
71
  throw new Error(`too many headers: ${String(req.headers.length)}`);
72
- if (req.body.length > U32_MAX) throw new Error(`body too long: ${String(req.body.length)} bytes`);
72
+ if (req.body.length > U32_MAX)
73
+ throw new Error(`body too long: ${String(req.body.length)} bytes`);
73
74
 
74
75
  const headers: { name: Buffer; value: Buffer }[] = [];
75
76
  let headersSize = 0;
76
77
  for (const [name, value] of req.headers) {
77
78
  const n = Buffer.from(name, 'utf8');
78
79
  const v = Buffer.from(value, 'utf8');
79
- if (n.length > U16_MAX || v.length > U16_MAX)
80
- throw new Error(`header too long: ${name}`);
80
+ if (n.length > U16_MAX || v.length > U16_MAX) throw new Error(`header too long: ${name}`);
81
81
  headers.push({ name: n, value: v });
82
82
  headersSize += 4 + n.length + v.length;
83
83
  }
@@ -17,8 +17,8 @@
17
17
  * `WebAssembly.Instance`, so offering the full surface costs nothing.
18
18
  */
19
19
 
20
- import { buildCryptoImports, freshCryptoState, type CryptoState } from './crypto.js';
21
- import { buildKvImports } from './kv.js';
20
+ import { buildCryptoImports, type CryptoState, freshCryptoState } from './crypto.js';
21
+ import { buildDatabaseImports, type DbDevState, freshDbState } from './database.js';
22
22
  import { EmailStatus, getEmailService } from './email/index.js';
23
23
  import { parseEmailBlob } from './email/wire.js';
24
24
  import { devEnvGet, devEnvGetSecure } from './env.js';
@@ -59,6 +59,8 @@ export interface DispatchState {
59
59
  clientIp: string;
60
60
  /** Per-dispatch Web Crypto keystore + result scratch (mirrors the edge). */
61
61
  crypto: CryptoState;
62
+ /** Per-dispatch ToilDB state: resolved collection handles + result stash. */
63
+ db: DbDevState;
62
64
  }
63
65
 
64
66
  /** A fresh, zeroed per-dispatch state (the edge resets the same way before each request). */
@@ -70,6 +72,7 @@ export function freshDispatchState(): DispatchState {
70
72
  sendfile: null,
71
73
  clientIp: '',
72
74
  crypto: freshCryptoState(),
75
+ db: freshDbState(),
73
76
  };
74
77
  }
75
78
 
@@ -155,7 +158,12 @@ export function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssem
155
158
  state.status = code >= 100 && code <= 599 ? code : 500;
156
159
  },
157
160
 
158
- set_header: (namePtr: number, nameLen: number, valPtr: number, valLen: number): void => {
161
+ set_header: (
162
+ namePtr: number,
163
+ nameLen: number,
164
+ valPtr: number,
165
+ valLen: number,
166
+ ): void => {
159
167
  if (nameLen > MAX_HEADER_NAME_LEN)
160
168
  throw new Error(`header name too long: ${String(nameLen)} bytes`);
161
169
  if (valLen > MAX_HEADER_VALUE_LEN)
@@ -225,7 +233,9 @@ export function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssem
225
233
  const svc = getEmailService();
226
234
  if (svc === null) {
227
235
  const to = parseEmailBlob(raw)?.to ?? '<unparsed>';
228
- process.stdout.write(` ✉ dev email_send -> ${to} (no email config; not sent)\n`);
236
+ process.stdout.write(
237
+ ` ✉ dev email_send -> ${to} (no email config; not sent)\n`,
238
+ );
229
239
  return EmailStatus.Sent;
230
240
  }
231
241
  const { status, parsed } = svc.prepare(raw);
@@ -240,7 +250,9 @@ export function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssem
240
250
  process.stdout.write(` ✉ dev email_send -> ${parsed.to} (${label})\n`);
241
251
  })
242
252
  .catch((e: unknown) => {
243
- process.stdout.write(` ✉ dev email_send -> ${parsed.to} (error: ${String(e)})\n`);
253
+ process.stdout.write(
254
+ ` ✉ dev email_send -> ${parsed.to} (error: ${String(e)})\n`,
255
+ );
244
256
  });
245
257
  return EmailStatus.Sent; // optimistic; sync wasm can't await the send
246
258
  },
@@ -270,10 +282,11 @@ export function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssem
270
282
  // `crypto`. The dev server skips metering, so these charge nothing.
271
283
  ...buildCryptoImports(ref, state.crypto),
272
284
 
273
- // DEV-ONLY persistent KV (`env.kv.*`). REMOVE LATER scaffolding so
274
- // the auth example's register/login chain spans requests under
275
- // `toiljs dev`; not present on the production edge (see ./kv.ts).
276
- ...buildKvImports(ref),
285
+ // `env::data.*`: the ToilDB data API, emulated in process (see
286
+ // ./database.ts). Backs the auth example's accounts + login
287
+ // challenges so register/login spans requests under `toiljs dev`;
288
+ // the production edge backs the SAME imports with ScyllaDB.
289
+ ...buildDatabaseImports(ref, state.db),
277
290
  },
278
291
  };
279
292
  }
@@ -22,18 +22,23 @@
22
22
  import fs from 'node:fs';
23
23
  import path from 'node:path';
24
24
 
25
- import { Server, type Request, type Response } from '@dacely/hyper-express';
25
+ import { type Request, type Response, Server } from '@dacely/hyper-express';
26
26
  import pc from 'picocolors';
27
27
 
28
28
  import type { EmailBackendConfig } from 'toiljs/shared';
29
29
 
30
30
  import { applyCacheRule, lookupCache } from './cache.js';
31
31
  import { initEmailService } from './email/index.js';
32
- import { METHOD_CODES, type EnvelopeRequest } from './envelope.js';
32
+ import { type EnvelopeRequest, METHOD_CODES } from './envelope.js';
33
33
  import { WasmServerModule } from './module.js';
34
- import { proxyToVite, wireWebsocketProxy, type ViteTarget } from './proxy.js';
34
+ import { proxyToVite, type ViteTarget, wireWebsocketProxy } from './proxy.js';
35
35
 
36
- export { METHOD_CODES, encodeRequestEnvelope, decodeResponseEnvelope, unpackHandleResult } from './envelope.js';
36
+ export {
37
+ METHOD_CODES,
38
+ encodeRequestEnvelope,
39
+ decodeResponseEnvelope,
40
+ unpackHandleResult,
41
+ } from './envelope.js';
37
42
  export type { EnvelopeRequest, EnvelopeResponse } from './envelope.js';
38
43
  export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './module.js';
39
44
  export type { WasmDispatchResult } from './module.js';
@@ -161,7 +166,10 @@ function sendWasmResponse(
161
166
  if (!hasContentType) {
162
167
  // The edge defaults file bodies to application/octet-stream; in dev we
163
168
  // guess from the extension so a guest-served asset renders in the browser.
164
- response.header('content-type', MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream');
169
+ response.header(
170
+ 'content-type',
171
+ MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream',
172
+ );
165
173
  }
166
174
  response.sendFile(file);
167
175
  return;
@@ -267,7 +275,8 @@ export async function startDevServer(options: DevServerOptions): Promise<Running
267
275
  // A trap (ToilScript abort, OOB, malformed envelope) is isolated to
268
276
  // this request, exactly like the edge poisoning one instance.
269
277
  process.stdout.write(
270
- pc.red(` ✗ ${request.method} ${request.path} server error: ${String(e)}`) + '\n',
278
+ pc.red(` ✗ ${request.method} ${request.path} server error: ${String(e)}`) +
279
+ '\n',
271
280
  );
272
281
  response.status(500).send('internal error\n');
273
282
  return;
@@ -16,8 +16,8 @@ import fs from 'node:fs';
16
16
  import {
17
17
  decodeResponseEnvelope,
18
18
  encodeRequestEnvelope,
19
- unpackHandleResult,
20
19
  type EnvelopeRequest,
20
+ unpackHandleResult,
21
21
  } from './envelope.js';
22
22
  import { buildHostImports, freshDispatchState, type MemoryRef } from './host.js';
23
23
 
@@ -52,16 +52,61 @@ interface HandleExports {
52
52
 
53
53
  /** Host functions the dev server provides under `env` (see `host.ts`). */
54
54
  const PROVIDED_IMPORTS = new Set([
55
- 'abort', 'set_status', 'set_header', 'respond_file', 'thread_spawn', 'Date.now',
56
- 'client_ip', 'ratelimit_check', 'email_send', 'env_get', 'env_get_secure',
55
+ 'abort',
56
+ 'set_status',
57
+ 'set_header',
58
+ 'respond_file',
59
+ 'thread_spawn',
60
+ 'Date.now',
61
+ 'client_ip',
62
+ 'ratelimit_check',
63
+ 'email_send',
64
+ 'env_get',
65
+ 'env_get_secure',
57
66
  // Web Crypto host functions (see ./crypto.ts).
58
- 'crypto.fill_random', 'crypto.random_uuid', 'crypto.take_result', 'crypto.digest',
59
- 'crypto.import_key', 'crypto.export_key', 'crypto.encrypt', 'crypto.decrypt',
60
- 'crypto.sign', 'crypto.verify', 'crypto.derive_bits',
61
- 'crypto.mldsa_verify', 'crypto.mlkem_decapsulate', 'crypto.voprf_evaluate',
62
- // DEV-ONLY persistent KV (see ./kv.ts). REMOVE once the example is backed by
63
- // a real external store; never ship as a production storage path.
64
- 'kv.put', 'kv.get', 'kv.getdel',
67
+ 'crypto.fill_random',
68
+ 'crypto.random_uuid',
69
+ 'crypto.take_result',
70
+ 'crypto.digest',
71
+ 'crypto.import_key',
72
+ 'crypto.export_key',
73
+ 'crypto.encrypt',
74
+ 'crypto.decrypt',
75
+ 'crypto.sign',
76
+ 'crypto.verify',
77
+ 'crypto.derive_bits',
78
+ 'crypto.mldsa_verify',
79
+ 'crypto.mlkem_decapsulate',
80
+ 'crypto.voprf_evaluate',
81
+ // ToilDB data API (see ./database.ts). Backed by ScyllaDB on the production
82
+ // edge; backs the auth example's accounts + login challenges in dev.
83
+ 'data.resolve_collection',
84
+ 'data.get',
85
+ 'data.get_many',
86
+ 'data.exists',
87
+ 'data.create',
88
+ 'data.patch',
89
+ 'data.delete',
90
+ 'data.get_delete',
91
+ 'data.unique_lookup',
92
+ 'data.unique_claim',
93
+ 'data.unique_release',
94
+ 'data.view_get',
95
+ 'data.view_publish',
96
+ 'data.membership_contains',
97
+ 'data.membership_add',
98
+ 'data.membership_remove',
99
+ 'data.membership_list',
100
+ 'data.counter_get',
101
+ 'data.counter_add',
102
+ 'data.append',
103
+ 'data.latest',
104
+ 'data.capacity_set_total',
105
+ 'data.capacity_available',
106
+ 'data.capacity_reserve',
107
+ 'data.capacity_confirm',
108
+ 'data.capacity_cancel',
109
+ 'data.take_result',
65
110
  ]);
66
111
 
67
112
  export class WasmServerModule {
@@ -198,7 +243,10 @@ export class WasmServerModule {
198
243
  /** Fail instantiation up front, with names, when the guest needs imports we do not provide. */
199
244
  private assertImportSurface(module: WebAssembly.Module): void {
200
245
  const missing = WebAssembly.Module.imports(module)
201
- .filter((i) => i.kind === 'function' && (i.module !== 'env' || !PROVIDED_IMPORTS.has(i.name)))
246
+ .filter(
247
+ (i) =>
248
+ i.kind === 'function' && (i.module !== 'env' || !PROVIDED_IMPORTS.has(i.name)),
249
+ )
202
250
  .map((i) => `${i.module}.${i.name}`);
203
251
  if (missing.length > 0)
204
252
  throw new Error(
@@ -6,12 +6,7 @@
6
6
  * websocket through Node's built-in `WebSocket` client, both loopback-only.
7
7
  */
8
8
 
9
- import {
10
- type Request,
11
- type Response,
12
- type Server,
13
- type Websocket,
14
- } from '@dacely/hyper-express';
9
+ import { type Request, type Response, type Server, type Websocket } from '@dacely/hyper-express';
15
10
 
16
11
  /** Where the internal Vite dev server listens (always loopback). */
17
12
  export interface ViteTarget {
@@ -151,7 +146,10 @@ export function wireWebsocketProxy(app: Server, target: ViteTarget): void {
151
146
  else pending.push(m);
152
147
  });
153
148
  ws.on('close', () => {
154
- if (upstream.readyState === WebSocket.OPEN || upstream.readyState === WebSocket.CONNECTING) {
149
+ if (
150
+ upstream.readyState === WebSocket.OPEN ||
151
+ upstream.readyState === WebSocket.CONNECTING
152
+ ) {
155
153
  upstream.close();
156
154
  }
157
155
  });
package/src/io/codec.ts CHANGED
@@ -34,60 +34,81 @@ export class DataWriter {
34
34
  this.view = new DataView(this.buf.buffer);
35
35
  }
36
36
 
37
- /**
38
- * Ensures room for `extra` more bytes and returns the offset to write at. Grows
39
- * (doubling) when needed, reassigning `buf`/`view`. Callers MUST read the returned
40
- * offset into a local before touching `this.view`/`this.buf`, since a grow swaps
41
- * them out from under a stale receiver.
42
- */
43
- private reserve(extra: number): number {
44
- const need = this.off + extra;
45
- if (need > this.buf.length) {
46
- let n = this.buf.length;
47
- while (n < need) n <<= 1;
48
- const bigger = new Uint8Array(n);
49
- bigger.set(this.buf.subarray(0, this.off));
50
- this.buf = bigger;
51
- this.view = new DataView(this.buf.buffer);
52
- }
53
- const at = this.off;
54
- this.off += extra;
55
- return at;
37
+ /** Writes an unsigned 8-bit byte (the low 8 bits of `v`). */
38
+ writeU8(v: number): this {
39
+ const at = this.reserve(1);
40
+ this.view.setUint8(at, v & 0xff);
41
+ return this;
56
42
  }
57
43
 
58
- /** Writes an unsigned 8-bit byte (the low 8 bits of `v`). */
59
- writeU8(v: number): this { const at = this.reserve(1); this.view.setUint8(at, v & 0xff); return this; }
60
44
  /** Writes an unsigned 16-bit integer. @param be - big-endian if true (default little-endian). */
61
- writeU16(v: number, be?: boolean): this { const at = this.reserve(2); this.view.setUint16(at, v & 0xffff, !be); return this; }
45
+ writeU16(v: number, be?: boolean): this {
46
+ const at = this.reserve(2);
47
+ this.view.setUint16(at, v & 0xffff, !be);
48
+ return this;
49
+ }
50
+
62
51
  /** Writes an unsigned 32-bit integer. @param be - big-endian if true (default little-endian). */
63
- writeU32(v: number, be?: boolean): this { const at = this.reserve(4); this.view.setUint32(at, v >>> 0, !be); return this; }
52
+ writeU32(v: number, be?: boolean): this {
53
+ const at = this.reserve(4);
54
+ this.view.setUint32(at, v >>> 0, !be);
55
+ return this;
56
+ }
57
+
64
58
  /** Writes an unsigned 64-bit integer (low 64 bits of `v`). @param be - big-endian if true. */
65
- writeU64(v: bigint, be?: boolean): this { const at = this.reserve(8); this.view.setBigUint64(at, v & MASK64, !be); return this; }
59
+ writeU64(v: bigint, be?: boolean): this {
60
+ const at = this.reserve(8);
61
+ this.view.setBigUint64(at, v & MASK64, !be);
62
+ return this;
63
+ }
64
+
66
65
  /** Writes a signed 8-bit integer. */
67
- writeI8(v: number): this { const at = this.reserve(1); this.view.setInt8(at, v); return this; }
66
+ writeI8(v: number): this {
67
+ const at = this.reserve(1);
68
+ this.view.setInt8(at, v);
69
+ return this;
70
+ }
71
+
68
72
  /** Writes a signed 16-bit integer. @param be - big-endian if true (default little-endian). */
69
- writeI16(v: number, be?: boolean): this { const at = this.reserve(2); this.view.setInt16(at, v, !be); return this; }
73
+ writeI16(v: number, be?: boolean): this {
74
+ const at = this.reserve(2);
75
+ this.view.setInt16(at, v, !be);
76
+ return this;
77
+ }
78
+
70
79
  /** Writes a signed 32-bit integer. @param be - big-endian if true (default little-endian). */
71
- writeI32(v: number, be?: boolean): this { const at = this.reserve(4); this.view.setInt32(at, v | 0, !be); return this; }
80
+ writeI32(v: number, be?: boolean): this {
81
+ const at = this.reserve(4);
82
+ this.view.setInt32(at, v | 0, !be);
83
+ return this;
84
+ }
85
+
72
86
  /** Writes a signed 64-bit integer. @param be - big-endian if true (default little-endian). */
73
- writeI64(v: bigint, be?: boolean): this { const at = this.reserve(8); this.view.setBigInt64(at, BigInt.asIntN(64, v), !be); return this; }
87
+ writeI64(v: bigint, be?: boolean): this {
88
+ const at = this.reserve(8);
89
+ this.view.setBigInt64(at, BigInt.asIntN(64, v), !be);
90
+ return this;
91
+ }
92
+
74
93
  /** Writes a 32-bit float. @param be - big-endian if true (default little-endian). */
75
- writeF32(v: number, be?: boolean): this { const at = this.reserve(4); this.view.setFloat32(at, v, !be); return this; }
76
- /** Writes a 64-bit float. @param be - big-endian if true (default little-endian). */
77
- writeF64(v: number, be?: boolean): this { const at = this.reserve(8); this.view.setFloat64(at, v, !be); return this; }
78
- /** Writes a boolean as one byte (1 or 0). */
79
- writeBool(v: boolean): this { return this.writeU8(v ? 1 : 0); }
94
+ writeF32(v: number, be?: boolean): this {
95
+ const at = this.reserve(4);
96
+ this.view.setFloat32(at, v, !be);
97
+ return this;
98
+ }
80
99
 
81
- /** Writes the `count` 64-bit limbs of `u` (low limb first in LE, high limb first in BE). */
82
- private writeLimbs(u: bigint, count: number, be: boolean): this {
83
- if (be) {
84
- for (let i = count - 1; i >= 0; i--) this.writeU64((u >> BigInt(i * 64)) & MASK64, true);
85
- } else {
86
- for (let i = 0; i < count; i++) this.writeU64((u >> BigInt(i * 64)) & MASK64, false);
87
- }
100
+ /** Writes a 64-bit float. @param be - big-endian if true (default little-endian). */
101
+ writeF64(v: number, be?: boolean): this {
102
+ const at = this.reserve(8);
103
+ this.view.setFloat64(at, v, !be);
88
104
  return this;
89
105
  }
90
106
 
107
+ /** Writes a boolean as one byte (1 or 0). */
108
+ writeBool(v: boolean): this {
109
+ return this.writeU8(v ? 1 : 0);
110
+ }
111
+
91
112
  /** Writes a `u32` length prefix followed by the raw bytes. @param be - endianness of the prefix. */
92
113
  writeBytes(bytes: Uint8Array, be?: boolean): this {
93
114
  this.writeU32(bytes.length, be);
@@ -110,19 +131,66 @@ export class DataWriter {
110
131
  }
111
132
 
112
133
  /** Writes an unsigned 128-bit integer as two 64-bit limbs. @param be - big-endian if true. */
113
- writeU128(v: bigint, be?: boolean): this { return this.writeLimbs(BigInt.asUintN(128, v), 2, !!be); }
134
+ writeU128(v: bigint, be?: boolean): this {
135
+ return this.writeLimbs(BigInt.asUintN(128, v), 2, !!be);
136
+ }
137
+
114
138
  /** Writes a signed 128-bit integer as two 64-bit limbs (two's complement). @param be - big-endian if true. */
115
- writeI128(v: bigint, be?: boolean): this { return this.writeLimbs(BigInt.asUintN(128, v), 2, !!be); }
139
+ writeI128(v: bigint, be?: boolean): this {
140
+ return this.writeLimbs(BigInt.asUintN(128, v), 2, !!be);
141
+ }
142
+
116
143
  /** Writes an unsigned 256-bit integer as four 64-bit limbs. @param be - big-endian if true. */
117
- writeU256(v: bigint, be?: boolean): this { return this.writeLimbs(BigInt.asUintN(256, v), 4, !!be); }
144
+ writeU256(v: bigint, be?: boolean): this {
145
+ return this.writeLimbs(BigInt.asUintN(256, v), 4, !!be);
146
+ }
147
+
118
148
  /** Writes a signed 256-bit integer as four 64-bit limbs (two's complement). @param be - big-endian if true. */
119
- writeI256(v: bigint, be?: boolean): this { return this.writeLimbs(BigInt.asUintN(256, v), 4, !!be); }
149
+ writeI256(v: bigint, be?: boolean): this {
150
+ return this.writeLimbs(BigInt.asUintN(256, v), 4, !!be);
151
+ }
120
152
 
121
153
  /** Number of bytes written so far. */
122
- length(): number { return this.off; }
154
+ length(): number {
155
+ return this.off;
156
+ }
123
157
 
124
158
  /** A fresh copy of exactly the bytes written. */
125
- toBytes(): Uint8Array<ArrayBuffer> { return this.buf.slice(0, this.off); }
159
+ toBytes(): Uint8Array<ArrayBuffer> {
160
+ return this.buf.slice(0, this.off);
161
+ }
162
+
163
+ /**
164
+ * Ensures room for `extra` more bytes and returns the offset to write at. Grows
165
+ * (doubling) when needed, reassigning `buf`/`view`. Callers MUST read the returned
166
+ * offset into a local before touching `this.view`/`this.buf`, since a grow swaps
167
+ * them out from under a stale receiver.
168
+ */
169
+ private reserve(extra: number): number {
170
+ const need = this.off + extra;
171
+ if (need > this.buf.length) {
172
+ let n = this.buf.length;
173
+ while (n < need) n <<= 1;
174
+ const bigger = new Uint8Array(n);
175
+ bigger.set(this.buf.subarray(0, this.off));
176
+ this.buf = bigger;
177
+ this.view = new DataView(this.buf.buffer);
178
+ }
179
+ const at = this.off;
180
+ this.off += extra;
181
+ return at;
182
+ }
183
+
184
+ /** Writes the `count` 64-bit limbs of `u` (low limb first in LE, high limb first in BE). */
185
+ private writeLimbs(u: bigint, count: number, be: boolean): this {
186
+ if (be) {
187
+ for (let i = count - 1; i >= 0; i--)
188
+ this.writeU64((u >> BigInt(i * 64)) & MASK64, true);
189
+ } else {
190
+ for (let i = 0; i < count; i++) this.writeU64((u >> BigInt(i * 64)) & MASK64, false);
191
+ }
192
+ return this;
193
+ }
126
194
  }
127
195
 
128
196
  /**
@@ -132,11 +200,11 @@ export class DataWriter {
132
200
  * big-endian writer.
133
201
  */
134
202
  export class DataReader {
203
+ /** Cleared to false if any read ran past the end of the buffer. */
204
+ ok = true;
135
205
  private buf: Uint8Array;
136
206
  private view: DataView;
137
207
  private off = 0;
138
- /** Cleared to false if any read ran past the end of the buffer. */
139
- ok = true;
140
208
 
141
209
  /** @param bytes - the buffer to read from (its byteOffset/length are respected). */
142
210
  constructor(bytes: Uint8Array) {
@@ -144,47 +212,89 @@ export class DataReader {
144
212
  this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
145
213
  }
146
214
 
147
- /** Returns true (and leaves `off` advanceable) if `n` more bytes are available; else clears `ok`. */
148
- private has(n: number): boolean {
149
- if (n < 0 || this.off + n > this.buf.length) {
150
- this.ok = false;
151
- return false;
152
- }
153
- return true;
215
+ /** Reads an unsigned 8-bit byte (0 past end). */
216
+ readU8(): number {
217
+ if (!this.has(1)) return 0;
218
+ const v = this.view.getUint8(this.off);
219
+ this.off += 1;
220
+ return v;
154
221
  }
155
222
 
156
- /** Reads an unsigned 8-bit byte (0 past end). */
157
- readU8(): number { if (!this.has(1)) return 0; const v = this.view.getUint8(this.off); this.off += 1; return v; }
158
223
  /** Reads an unsigned 16-bit integer. @param be - big-endian if true (default little-endian). */
159
- readU16(be?: boolean): number { if (!this.has(2)) return 0; const v = this.view.getUint16(this.off, !be); this.off += 2; return v; }
224
+ readU16(be?: boolean): number {
225
+ if (!this.has(2)) return 0;
226
+ const v = this.view.getUint16(this.off, !be);
227
+ this.off += 2;
228
+ return v;
229
+ }
230
+
160
231
  /** Reads an unsigned 32-bit integer. @param be - big-endian if true (default little-endian). */
161
- readU32(be?: boolean): number { if (!this.has(4)) return 0; const v = this.view.getUint32(this.off, !be); this.off += 4; return v >>> 0; }
232
+ readU32(be?: boolean): number {
233
+ if (!this.has(4)) return 0;
234
+ const v = this.view.getUint32(this.off, !be);
235
+ this.off += 4;
236
+ return v >>> 0;
237
+ }
238
+
162
239
  /** Reads an unsigned 64-bit integer. @param be - big-endian if true (default little-endian). */
163
- readU64(be?: boolean): bigint { if (!this.has(8)) return 0n; const v = this.view.getBigUint64(this.off, !be); this.off += 8; return v; }
240
+ readU64(be?: boolean): bigint {
241
+ if (!this.has(8)) return 0n;
242
+ const v = this.view.getBigUint64(this.off, !be);
243
+ this.off += 8;
244
+ return v;
245
+ }
246
+
164
247
  /** Reads a signed 8-bit integer (0 past end). */
165
- readI8(): number { if (!this.has(1)) return 0; const v = this.view.getInt8(this.off); this.off += 1; return v; }
248
+ readI8(): number {
249
+ if (!this.has(1)) return 0;
250
+ const v = this.view.getInt8(this.off);
251
+ this.off += 1;
252
+ return v;
253
+ }
254
+
166
255
  /** Reads a signed 16-bit integer. @param be - big-endian if true (default little-endian). */
167
- readI16(be?: boolean): number { if (!this.has(2)) return 0; const v = this.view.getInt16(this.off, !be); this.off += 2; return v; }
256
+ readI16(be?: boolean): number {
257
+ if (!this.has(2)) return 0;
258
+ const v = this.view.getInt16(this.off, !be);
259
+ this.off += 2;
260
+ return v;
261
+ }
262
+
168
263
  /** Reads a signed 32-bit integer. @param be - big-endian if true (default little-endian). */
169
- readI32(be?: boolean): number { if (!this.has(4)) return 0; const v = this.view.getInt32(this.off, !be); this.off += 4; return v; }
264
+ readI32(be?: boolean): number {
265
+ if (!this.has(4)) return 0;
266
+ const v = this.view.getInt32(this.off, !be);
267
+ this.off += 4;
268
+ return v;
269
+ }
270
+
170
271
  /** Reads a signed 64-bit integer. @param be - big-endian if true (default little-endian). */
171
- readI64(be?: boolean): bigint { if (!this.has(8)) return 0n; const v = this.view.getBigInt64(this.off, !be); this.off += 8; return v; }
272
+ readI64(be?: boolean): bigint {
273
+ if (!this.has(8)) return 0n;
274
+ const v = this.view.getBigInt64(this.off, !be);
275
+ this.off += 8;
276
+ return v;
277
+ }
278
+
172
279
  /** Reads a 32-bit float. @param be - big-endian if true (default little-endian). */
173
- readF32(be?: boolean): number { if (!this.has(4)) return 0; const v = this.view.getFloat32(this.off, !be); this.off += 4; return v; }
280
+ readF32(be?: boolean): number {
281
+ if (!this.has(4)) return 0;
282
+ const v = this.view.getFloat32(this.off, !be);
283
+ this.off += 4;
284
+ return v;
285
+ }
286
+
174
287
  /** Reads a 64-bit float. @param be - big-endian if true (default little-endian). */
175
- readF64(be?: boolean): number { if (!this.has(8)) return 0; const v = this.view.getFloat64(this.off, !be); this.off += 8; return v; }
176
- /** Reads a boolean (any non-zero byte is true). */
177
- readBool(): boolean { return this.readU8() !== 0; }
288
+ readF64(be?: boolean): number {
289
+ if (!this.has(8)) return 0;
290
+ const v = this.view.getFloat64(this.off, !be);
291
+ this.off += 8;
292
+ return v;
293
+ }
178
294
 
179
- /** Reads `count` 64-bit limbs and recombines them (low limb first in LE, high first in BE). */
180
- private readLimbs(count: number, be: boolean): bigint {
181
- let result = 0n;
182
- if (be) {
183
- for (let i = count - 1; i >= 0; i--) result |= this.readU64(true) << BigInt(i * 64);
184
- } else {
185
- for (let i = 0; i < count; i++) result |= this.readU64(false) << BigInt(i * 64);
186
- }
187
- return result;
295
+ /** Reads a boolean (any non-zero byte is true). */
296
+ readBool(): boolean {
297
+ return this.readU8() !== 0;
188
298
  }
189
299
 
190
300
  /** Reads a `u32`-length-prefixed byte blob (empty past end). @param be - endianness of the prefix. */
@@ -199,21 +309,54 @@ export class DataReader {
199
309
  /** Reads a `u32`-byte-length-prefixed UTF-8 string (empty past end). @param be - endianness of the prefix. */
200
310
  readString(be?: boolean): string {
201
311
  const len = this.readU32(be);
202
- if (!this.has(len)) return "";
312
+ if (!this.has(len)) return '';
203
313
  const s = utf8Decoder.decode(this.buf.subarray(this.off, this.off + len));
204
314
  this.off += len;
205
315
  return s;
206
316
  }
207
317
 
208
318
  /** Reads an unsigned 128-bit integer. @param be - big-endian if true (default little-endian). */
209
- readU128(be?: boolean): bigint { return this.readLimbs(2, !!be); }
319
+ readU128(be?: boolean): bigint {
320
+ return this.readLimbs(2, !!be);
321
+ }
322
+
210
323
  /** Reads a signed 128-bit integer (two's complement). @param be - big-endian if true. */
211
- readI128(be?: boolean): bigint { return BigInt.asIntN(128, this.readLimbs(2, !!be)); }
324
+ readI128(be?: boolean): bigint {
325
+ return BigInt.asIntN(128, this.readLimbs(2, !!be));
326
+ }
327
+
212
328
  /** Reads an unsigned 256-bit integer. @param be - big-endian if true (default little-endian). */
213
- readU256(be?: boolean): bigint { return this.readLimbs(4, !!be); }
329
+ readU256(be?: boolean): bigint {
330
+ return this.readLimbs(4, !!be);
331
+ }
332
+
214
333
  /** Reads a signed 256-bit integer (two's complement). @param be - big-endian if true. */
215
- readI256(be?: boolean): bigint { return BigInt.asIntN(256, this.readLimbs(4, !!be)); }
334
+ readI256(be?: boolean): bigint {
335
+ return BigInt.asIntN(256, this.readLimbs(4, !!be));
336
+ }
216
337
 
217
338
  /** Bytes left to read. */
218
- remaining(): number { return this.buf.length - this.off; }
339
+ remaining(): number {
340
+ return this.buf.length - this.off;
341
+ }
342
+
343
+ /** Returns true (and leaves `off` advanceable) if `n` more bytes are available; else clears `ok`. */
344
+ private has(n: number): boolean {
345
+ if (n < 0 || this.off + n > this.buf.length) {
346
+ this.ok = false;
347
+ return false;
348
+ }
349
+ return true;
350
+ }
351
+
352
+ /** Reads `count` 64-bit limbs and recombines them (low limb first in LE, high first in BE). */
353
+ private readLimbs(count: number, be: boolean): bigint {
354
+ let result = 0n;
355
+ if (be) {
356
+ for (let i = count - 1; i >= 0; i--) result |= this.readU64(true) << BigInt(i * 64);
357
+ } else {
358
+ for (let i = 0; i < count; i++) result |= this.readU64(false) << BigInt(i * 64);
359
+ }
360
+ return result;
361
+ }
219
362
  }