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.
- package/CHANGELOG.md +5 -0
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +9 -5
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.js +1 -1
- package/build/client/components/Image.d.ts +1 -1
- package/build/client/dev/devtools.js +3 -1
- package/build/client/index.d.ts +2 -2
- package/build/client/index.js +2 -2
- package/build/client/routing/Router.js +1 -1
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/docs.js +1 -1
- package/build/compiler/seo.js +1 -3
- package/build/compiler/template-build.js +1 -1
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/cache.js +0 -0
- package/build/devserver/crypto.js +45 -17
- package/build/devserver/database.d.ts +8 -0
- package/build/devserver/database.js +416 -0
- package/build/devserver/email/caps.js +0 -0
- package/build/devserver/email/config.js +7 -2
- package/build/devserver/email/validate.js +1 -4
- package/build/devserver/host.d.ts +2 -0
- package/build/devserver/host.js +3 -2
- package/build/devserver/index.d.ts +1 -1
- package/build/devserver/index.js +3 -2
- package/build/devserver/module.js +52 -7
- package/build/devserver/proxy.js +2 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/io/codec.d.ts +5 -5
- package/build/io/codec.js +193 -77
- package/examples/basic/client/components/HoneycombBackground.tsx +1 -1
- package/examples/basic/client/public/images/logo.svg +37 -34
- package/examples/basic/client/public/index.html +14 -14
- package/examples/basic/client/routes/auth.tsx +18 -10
- package/examples/basic/client/routes/cookies.tsx +15 -24
- package/examples/basic/client/routes/crypto.tsx +4 -5
- package/examples/basic/client/routes/features/template/template.tsx +1 -1
- package/examples/basic/client/routes/hello.tsx +1 -1
- package/examples/basic/client/routes/pq.tsx +14 -14
- package/examples/basic/client/routes/rest.tsx +50 -1
- package/examples/basic/client/styles/main.css +25 -22
- package/examples/basic/client/toil.tsx +1 -1
- package/examples/basic/server/README.md +8 -8
- package/examples/basic/server/core/AppHandler.ts +4 -7
- package/examples/basic/server/main.ts +1 -0
- package/examples/basic/server/models/GuestEntry.ts +12 -0
- package/examples/basic/server/models/GuestbookView.ts +10 -0
- package/examples/basic/server/models/NewMessage.ts +6 -0
- package/examples/basic/server/routes/Auth.ts +50 -106
- package/examples/basic/server/routes/EnvDemo.ts +9 -3
- package/examples/basic/server/routes/Guestbook.ts +62 -0
- package/package.json +2 -2
- package/server/globals/auth.ts +3 -3
- package/server/globals/twofactor.ts +2 -1
- package/server/runtime/http/securecookies.ts +3 -2
- package/src/backend/index.ts +4 -2
- package/src/cli/doctor.ts +10 -3
- package/src/cli/notify.ts +1 -6
- package/src/cli/ui.ts +3 -3
- package/src/cli/version-check.ts +5 -1
- package/src/client/auth.ts +33 -10
- package/src/client/components/Form.tsx +2 -2
- package/src/client/components/Image.tsx +1 -1
- package/src/client/components/Script.tsx +1 -1
- package/src/client/components/Slot.tsx +1 -1
- package/src/client/dev/devtools.tsx +121 -54
- package/src/client/dev/error-overlay.tsx +7 -1
- package/src/client/head/metadata.ts +1 -1
- package/src/client/index.ts +13 -2
- package/src/client/routing/Router.tsx +2 -2
- package/src/client/routing/error-boundary.tsx +1 -1
- package/src/client/routing/loader.ts +2 -2
- package/src/client/routing/mount.tsx +5 -6
- package/src/compiler/docs.ts +1 -1
- package/src/compiler/email-preview.ts +1 -1
- package/src/compiler/generate.ts +1 -1
- package/src/compiler/seo.ts +1 -3
- package/src/compiler/ssg.ts +10 -4
- package/src/compiler/template-build.ts +2 -7
- package/src/compiler/template.ts +1 -4
- package/src/compiler/vite.ts +1 -1
- package/src/devserver/cache.ts +0 -0
- package/src/devserver/crypto.ts +140 -51
- package/src/devserver/database.ts +600 -0
- package/src/devserver/dotenv.ts +10 -2
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +8 -2
- package/src/devserver/email/index.ts +3 -3
- package/src/devserver/email/validate.ts +1 -4
- package/src/devserver/envelope.ts +3 -3
- package/src/devserver/host.ts +22 -9
- package/src/devserver/index.ts +15 -6
- package/src/devserver/module.ts +59 -11
- package/src/devserver/proxy.ts +5 -7
- package/src/io/codec.ts +226 -83
- package/test/devserver-database.test.ts +364 -0
- package/test/devserver-pqauth.test.ts +5 -65
- package/test/example-guestbook.test.ts +78 -0
- package/test/pqauth-e2e.test.ts +6 -6
- package/build/devserver/kv.d.ts +0 -3
- package/build/devserver/kv.js +0 -53
- 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)
|
|
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
|
}
|
package/src/devserver/host.ts
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* `WebAssembly.Instance`, so offering the full surface costs nothing.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { buildCryptoImports,
|
|
21
|
-
import {
|
|
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: (
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
274
|
-
// the auth example's
|
|
275
|
-
//
|
|
276
|
-
|
|
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
|
}
|
package/src/devserver/index.ts
CHANGED
|
@@ -22,18 +22,23 @@
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
|
|
25
|
-
import {
|
|
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 {
|
|
32
|
+
import { type EnvelopeRequest, METHOD_CODES } from './envelope.js';
|
|
33
33
|
import { WasmServerModule } from './module.js';
|
|
34
|
-
import { proxyToVite,
|
|
34
|
+
import { proxyToVite, type ViteTarget, wireWebsocketProxy } from './proxy.js';
|
|
35
35
|
|
|
36
|
-
export {
|
|
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(
|
|
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)}`) +
|
|
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;
|
package/src/devserver/module.ts
CHANGED
|
@@ -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',
|
|
56
|
-
'
|
|
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',
|
|
59
|
-
'crypto.
|
|
60
|
-
'crypto.
|
|
61
|
-
'crypto.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
'
|
|
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(
|
|
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(
|
package/src/devserver/proxy.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
154
|
+
length(): number {
|
|
155
|
+
return this.off;
|
|
156
|
+
}
|
|
123
157
|
|
|
124
158
|
/** A fresh copy of exactly the bytes written. */
|
|
125
|
-
toBytes(): Uint8Array<ArrayBuffer> {
|
|
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
|
-
/**
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
}
|