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.
- package/CHANGELOG.md +10 -0
- package/README.md +63 -61
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +13 -1
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/index.d.ts +2 -0
- package/build/client/index.js +1 -0
- package/build/client/rpc.js +21 -1
- package/build/client/stream/client.d.ts +11 -0
- package/build/client/stream/client.js +59 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +2 -0
- package/build/compiler/config.js +9 -7
- package/build/compiler/index.d.ts +1 -0
- package/build/compiler/index.js +16 -2
- package/build/compiler/toil-docs.generated.js +5 -4
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/runtime.d.ts +13 -0
- package/build/devserver/daemon/runtime.js +29 -0
- package/build/devserver/db/database.d.ts +1 -0
- package/build/devserver/db/database.js +10 -0
- package/build/devserver/db/derives.d.ts +7 -0
- package/build/devserver/db/derives.js +94 -0
- package/build/devserver/db/index.d.ts +1 -0
- package/build/devserver/db/index.js +1 -0
- package/build/devserver/db/types.d.ts +1 -0
- package/build/devserver/db/types.js +1 -0
- package/build/devserver/http/proxy.d.ts +5 -1
- package/build/devserver/http/proxy.js +39 -36
- package/build/devserver/http/runtime.d.ts +62 -0
- package/build/devserver/http/runtime.js +194 -0
- package/build/devserver/index.d.ts +2 -0
- package/build/devserver/index.js +1 -0
- package/build/devserver/production-ipc.d.ts +50 -0
- package/build/devserver/production-ipc.js +21 -0
- package/build/devserver/production-worker.d.ts +1 -0
- package/build/devserver/production-worker.js +73 -0
- package/build/devserver/production.d.ts +35 -0
- package/build/devserver/production.js +502 -0
- package/build/devserver/runtime/module.d.ts +5 -0
- package/build/devserver/runtime/module.js +47 -1
- package/build/devserver/server.d.ts +1 -0
- package/build/devserver/server.js +32 -145
- package/build/devserver/ssr.d.ts +2 -0
- package/build/devserver/ssr.js +19 -2
- package/build/devserver/stream/catalog.d.ts +20 -0
- package/build/devserver/stream/catalog.js +54 -0
- package/build/devserver/stream/host.d.ts +9 -0
- package/build/devserver/stream/host.js +15 -0
- package/build/devserver/stream/index.d.ts +37 -0
- package/build/devserver/stream/index.js +220 -0
- package/build/devserver/stream/manager.d.ts +34 -0
- package/build/devserver/stream/manager.js +103 -0
- package/build/devserver/stream/router.d.ts +25 -0
- package/build/devserver/stream/router.js +64 -0
- package/build/devserver/stream/wire.d.ts +5 -0
- package/build/devserver/stream/wire.js +33 -0
- package/build/devserver/stream/ws.d.ts +18 -0
- package/build/devserver/stream/ws.js +46 -0
- package/docs/cli.md +3 -1
- package/docs/derive.md +159 -0
- package/docs/getting-started.md +7 -7
- package/docs/index.md +1 -1
- package/docs/streams.md +46 -14
- package/examples/basic/server/routes/Guestbook.ts +38 -13
- package/package.json +2 -2
- package/src/cli/index.ts +14 -1
- package/src/client/index.ts +2 -0
- package/src/client/rpc.ts +25 -1
- package/src/client/stream/client.ts +109 -0
- package/src/compiler/config.ts +15 -7
- package/src/compiler/index.ts +24 -5
- package/src/compiler/toil-docs.generated.ts +5 -4
- package/src/devserver/daemon/runtime.ts +48 -0
- package/src/devserver/db/database.ts +14 -0
- package/src/devserver/db/derives.ts +121 -0
- package/src/devserver/db/index.ts +1 -0
- package/src/devserver/db/types.ts +6 -0
- package/src/devserver/http/proxy.ts +53 -39
- package/src/devserver/http/runtime.ts +287 -0
- package/src/devserver/index.ts +2 -0
- package/src/devserver/production-ipc.ts +63 -0
- package/src/devserver/production-worker.ts +83 -0
- package/src/devserver/production.ts +706 -0
- package/src/devserver/runtime/module.ts +95 -1
- package/src/devserver/server.ts +52 -201
- package/src/devserver/ssr.ts +23 -3
- package/src/devserver/stream/catalog.ts +106 -0
- package/src/devserver/stream/host.ts +42 -0
- package/src/devserver/stream/index.ts +308 -0
- package/src/devserver/stream/manager.ts +163 -0
- package/src/devserver/stream/router.ts +101 -0
- package/src/devserver/stream/wire.ts +58 -0
- package/src/devserver/stream/ws.ts +76 -0
- package/test/built-ssr.test.ts +98 -0
- package/test/devserver.test.ts +20 -4
- package/test/example-guestbook.test.ts +8 -5
- package/test/fixtures/stream-echo.ts +26 -0
- package/test/fixtures/stream-gate.ts +24 -0
- package/test/fixtures/stream-trap.ts +18 -0
- package/test/fixtures/stream-typed.ts +41 -0
- package/test/stream-emulation.test.ts +433 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport adapter that bridges ONE dev WebSocket connection to the {@link StreamDevHost} session
|
|
3
|
+
* driver. It is the per-socket glue the dev WS endpoint instantiates: socket-open -> `acceptUpgrade`
|
|
4
|
+
* (close on a `@connect`/gate reject), inbound frame -> `dispatch` (send reply frames back, or close
|
|
5
|
+
* on a reject/trap close code), socket-close -> `@close` + drop. It owns NO transport itself - the
|
|
6
|
+
* caller passes a {@link StreamWsTransport} of `send`/`close` callbacks - so it is unit-testable
|
|
7
|
+
* without a live socket and reusable across whatever WS/WebTransport the endpoint ends up speaking.
|
|
8
|
+
*
|
|
9
|
+
* The endpoint that wires hyper-express `app.ws` to this (the URL convention + coexistence with the
|
|
10
|
+
* Vite-HMR catch-all proxy + a `streamWasmFile` config) is the remaining live-server step; this is the
|
|
11
|
+
* transport-agnostic core it drives.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { StreamDevHost } from './manager.js';
|
|
15
|
+
|
|
16
|
+
/** The socket-side primitives this adapter needs: send one binary frame, and close with a code. */
|
|
17
|
+
export interface StreamWsTransport {
|
|
18
|
+
/** Send one outbound binary frame to the client. */
|
|
19
|
+
send(frame: Buffer): void;
|
|
20
|
+
/** Close the connection with a `0x02xx` stream close code. */
|
|
21
|
+
close(code: number): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class StreamWsSession {
|
|
25
|
+
private open = false;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly host: StreamDevHost,
|
|
29
|
+
private readonly connId: string,
|
|
30
|
+
private readonly authority: string,
|
|
31
|
+
private readonly path: string,
|
|
32
|
+
private readonly transport: StreamWsTransport,
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
/** Whether the connection is accepted + live. */
|
|
36
|
+
get isOpen(): boolean {
|
|
37
|
+
return this.open;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Socket open: accept the upgrade (node gate is the endpoint's job; this drives the box). On a
|
|
42
|
+
* `@connect`/artifact reject the connection is closed with the code and no box is held. Returns
|
|
43
|
+
* whether the connection was accepted.
|
|
44
|
+
*/
|
|
45
|
+
onOpen(): boolean {
|
|
46
|
+
const up = this.host.acceptUpgrade(this.connId, this.authority, this.path);
|
|
47
|
+
if (up.kind === 'rejected') {
|
|
48
|
+
this.transport.close(up.code);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
this.open = true;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** An inbound binary frame: dispatch it; send the reply frames, or close on a reject/trap code. */
|
|
56
|
+
onMessage(inbound: Buffer): void {
|
|
57
|
+
if (!this.open) return;
|
|
58
|
+
const r = this.host.dispatch(this.connId, inbound);
|
|
59
|
+
if (r.kind === 'reply') {
|
|
60
|
+
for (const frame of r.frames) this.transport.send(frame);
|
|
61
|
+
} else if (r.kind === 'close') {
|
|
62
|
+
// A guest reject or a TRAP close: close the socket; the socket-close event runs onClose,
|
|
63
|
+
// which fires @close + drops the box (a no-op if a trap already discarded it).
|
|
64
|
+
this.open = false;
|
|
65
|
+
this.transport.close(r.code);
|
|
66
|
+
}
|
|
67
|
+
// 'noConnection' cannot occur for a live, accepted session.
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Socket close (client-initiated or after our own close): fire `@close` + drop the box. */
|
|
71
|
+
onClose(): void {
|
|
72
|
+
if (!this.open && !this.host.has(this.connId)) return;
|
|
73
|
+
this.open = false;
|
|
74
|
+
this.host.close(this.connId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { loadBuiltSsrTemplates } from '../src/devserver/production';
|
|
8
|
+
import { assembleSsr, type SsrRoute } from '../src/devserver/ssr';
|
|
9
|
+
|
|
10
|
+
function slotsManifest(tmplLen: number, hash: Buffer): Buffer {
|
|
11
|
+
const buf = Buffer.alloc(46 + 8);
|
|
12
|
+
let o = 0;
|
|
13
|
+
buf.write('TSLT', o, 'ascii');
|
|
14
|
+
o += 4;
|
|
15
|
+
buf.writeUInt16LE(1, o);
|
|
16
|
+
o += 2;
|
|
17
|
+
buf.writeUInt16LE(0, o);
|
|
18
|
+
o += 2;
|
|
19
|
+
buf.writeUInt32LE(tmplLen, o);
|
|
20
|
+
o += 4;
|
|
21
|
+
hash.copy(buf, o);
|
|
22
|
+
o += 32;
|
|
23
|
+
buf.writeUInt16LE(1, o);
|
|
24
|
+
o += 2;
|
|
25
|
+
buf.writeUInt32LE(1, o); // insert after the first byte
|
|
26
|
+
o += 4;
|
|
27
|
+
buf.writeUInt16LE(0, o);
|
|
28
|
+
o += 2;
|
|
29
|
+
buf.writeUInt8(0, o); // text
|
|
30
|
+
o += 1;
|
|
31
|
+
buf.writeUInt8(0, o);
|
|
32
|
+
return buf;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function valuesEnvelope(hash: Buffer, value: string): Buffer {
|
|
36
|
+
const valueBytes = Buffer.from(value);
|
|
37
|
+
const buf = Buffer.alloc(2 + 32 + 2 + 2 + 2 + 1 + 4 + valueBytes.length);
|
|
38
|
+
let o = 0;
|
|
39
|
+
buf.writeUInt16LE(200, o);
|
|
40
|
+
o += 2;
|
|
41
|
+
hash.copy(buf, o);
|
|
42
|
+
o += 32;
|
|
43
|
+
buf.writeUInt16LE(0, o); // headers
|
|
44
|
+
o += 2;
|
|
45
|
+
buf.writeUInt16LE(1, o); // slots
|
|
46
|
+
o += 2;
|
|
47
|
+
buf.writeUInt16LE(0, o);
|
|
48
|
+
o += 2;
|
|
49
|
+
buf.writeUInt8(0, o);
|
|
50
|
+
o += 1;
|
|
51
|
+
buf.writeUInt32LE(valueBytes.length, o);
|
|
52
|
+
o += 4;
|
|
53
|
+
valueBytes.copy(buf, o);
|
|
54
|
+
return buf;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('built SSR templates', () => {
|
|
58
|
+
it('loads the built shell, including production CSS links, from _ssr artifacts', () => {
|
|
59
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'toil-built-ssr-'));
|
|
60
|
+
try {
|
|
61
|
+
const dir = path.join(root, '_ssr');
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
63
|
+
const tmpl = Buffer.from(
|
|
64
|
+
'<!doctype html><html><head><link rel="stylesheet" href="/css/style.css"></head><body>A</body></html>',
|
|
65
|
+
);
|
|
66
|
+
const hash = Buffer.alloc(32, 7);
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.join(dir, 'templates.json'),
|
|
69
|
+
JSON.stringify([{ route: '/x', name: 'x', hash: hash.toString('hex') }]),
|
|
70
|
+
);
|
|
71
|
+
fs.writeFileSync(path.join(dir, 'x.tmpl'), tmpl);
|
|
72
|
+
fs.writeFileSync(path.join(dir, 'x.slots'), slotsManifest(tmpl.length, hash));
|
|
73
|
+
|
|
74
|
+
const templates = loadBuiltSsrTemplates(root);
|
|
75
|
+
expect(templates).toHaveLength(1);
|
|
76
|
+
expect(Buffer.from(templates[0]!.tmpl).toString('utf8')).toContain('/css/style.css');
|
|
77
|
+
expect(Buffer.from(templates[0]!.hash!).equals(hash)).toBe(true);
|
|
78
|
+
expect(templates[0]!.entries).toEqual([{ id: 0, offset: 1 }]);
|
|
79
|
+
} finally {
|
|
80
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('checks the deployed template hash before splicing production SSR values', () => {
|
|
85
|
+
const hash = Buffer.alloc(32, 1);
|
|
86
|
+
const route: SsrRoute = {
|
|
87
|
+
test: () => true,
|
|
88
|
+
tmpl: Buffer.from('ab'),
|
|
89
|
+
entries: [{ id: 0, offset: 1 }],
|
|
90
|
+
hash,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
expect(Buffer.from(assembleSsr(route, valuesEnvelope(hash, 'X'))!.html).toString()).toBe(
|
|
94
|
+
'aXb',
|
|
95
|
+
);
|
|
96
|
+
expect(assembleSsr(route, valuesEnvelope(Buffer.alloc(32, 2), 'X'))).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
package/test/devserver.test.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* into the example project's ToilScript-compiled server wasm.
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { fileURLToPath } from 'node:url';
|
|
9
10
|
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
unpackHandleResult,
|
|
16
17
|
WasmServerModule,
|
|
17
18
|
} from '../src/devserver/index.js';
|
|
19
|
+
import { resolveStaticFile } from '../src/devserver/http/runtime.js';
|
|
18
20
|
|
|
19
21
|
const EXAMPLE_WASM = path.resolve(
|
|
20
22
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -121,10 +123,7 @@ describe('response envelope decoding', () => {
|
|
|
121
123
|
/status 0/,
|
|
122
124
|
);
|
|
123
125
|
// Claim a huge header name with too few bytes behind it.
|
|
124
|
-
const bad = Buffer.concat([
|
|
125
|
-
Buffer.from([200, 0, 1, 0, 255, 255, 0, 0]),
|
|
126
|
-
Buffer.from('hi'),
|
|
127
|
-
]);
|
|
126
|
+
const bad = Buffer.concat([Buffer.from([200, 0, 1, 0, 255, 255, 0, 0]), Buffer.from('hi')]);
|
|
128
127
|
expect(() => decodeResponseEnvelope(bad)).toThrow(/truncated/);
|
|
129
128
|
});
|
|
130
129
|
});
|
|
@@ -140,6 +139,23 @@ describe('handle() result unpacking', () => {
|
|
|
140
139
|
});
|
|
141
140
|
});
|
|
142
141
|
|
|
142
|
+
describe('static file resolver', () => {
|
|
143
|
+
it('resolves absolute request paths inside the static root', () => {
|
|
144
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'toil-static-'));
|
|
145
|
+
const file = path.join(root, 'assets', 'app.css');
|
|
146
|
+
try {
|
|
147
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
148
|
+
fs.writeFileSync(file, 'body{}');
|
|
149
|
+
|
|
150
|
+
expect(resolveStaticFile(root, '/assets/app.css')).toBe(file);
|
|
151
|
+
expect(resolveStaticFile(root, '/missing.css')).toBeNull();
|
|
152
|
+
expect(resolveStaticFile(root, '/../app.css')).toBeNull();
|
|
153
|
+
} finally {
|
|
154
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
143
159
|
describe.skipIf(!fs.existsSync(EXAMPLE_WASM))('dispatch into the example server wasm', () => {
|
|
144
160
|
const load = (): WasmServerModule => {
|
|
145
161
|
const m = new WasmServerModule(EXAMPLE_WASM);
|
|
@@ -64,17 +64,20 @@ describe.skipIf(!haveWasm)('guestbook demo: ToilDB events + counter persist acro
|
|
|
64
64
|
expect(json(r).total).toBe('1');
|
|
65
65
|
|
|
66
66
|
// A brand-new wasm instance - its memory is empty - still sees the prior
|
|
67
|
-
// signature, because it lives in ToilDB, not module state.
|
|
67
|
+
// signature, because it lives in ToilDB, not module state. The action
|
|
68
|
+
// acks with the running total; the entries list is served by the GET.
|
|
68
69
|
r = sign(load(), 'Linus', 'second');
|
|
69
|
-
|
|
70
|
+
expect(json(r).total).toBe('2');
|
|
71
|
+
|
|
72
|
+
// The newest entries are served by GET /guestbook from the materialized
|
|
73
|
+
// view that the @derive republishes after each signature (events.latest
|
|
74
|
+
// is a scan, barred in the request handlers, so it runs in the derive).
|
|
75
|
+
const v = json(list(load()));
|
|
70
76
|
expect(v.total).toBe('2');
|
|
71
77
|
expect(v.entries.length).toBe(2);
|
|
72
78
|
expect(v.entries[0].author).toBe('Linus'); // events.latest is newest-first
|
|
73
79
|
expect(v.entries[1].author).toBe('Ada');
|
|
74
80
|
expect(v.entries[1].message).toBe('first!');
|
|
75
|
-
|
|
76
|
-
// A read-only GET on yet another instance sees the same persisted state.
|
|
77
|
-
expect(json(list(load())).total).toBe('2');
|
|
78
81
|
});
|
|
79
82
|
|
|
80
83
|
// End-to-end proof that the `server/migrations/GuestEntry.migration.ts` demo
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Dev-stream emulation fixture: a `@stream('echo')` whose raw `@message` bridge echoes the inbound
|
|
2
|
+
// bytes back through the egress ring, rejects an 'X'-prefixed frame (0x0210), and `empty()`s a
|
|
3
|
+
// zero-length frame. Compiled by the dev test with the LOCAL toilscript (`--targetMode hot`), then
|
|
4
|
+
// driven through `DevStreamBox`. Mirrors toil-backend's `tests/fixtures/stream/echo_src.ts`.
|
|
5
|
+
|
|
6
|
+
let __count: i32 = 0;
|
|
7
|
+
|
|
8
|
+
// Observable so the @message hook's residency can be asserted across events.
|
|
9
|
+
export function messageCount(): i32 {
|
|
10
|
+
return __count;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@stream('echo')
|
|
14
|
+
class Echo {
|
|
15
|
+
@message reply(p: StreamPacket): StreamOutbound {
|
|
16
|
+
__count = __count + 1;
|
|
17
|
+
const n = p.length;
|
|
18
|
+
if (n == 0) return StreamOutbound.empty();
|
|
19
|
+
if (p.at(0) == 0x58) return StreamOutbound.reject(0x0210); // 'X' -> reject
|
|
20
|
+
return StreamOutbound.reply(p.bytes());
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function probe(): i32 {
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Dev @connect-bridge fixture: a `@stream('gate')` whose `@connect(c: StreamInbound): StreamOutbound`
|
|
2
|
+
// reads the host-written connect context (the path) and REJECTS "/blocked" with 0x0211, ACCEPTING any
|
|
3
|
+
// other path; a "/greet" path stages an egress frame DURING @connect (the host must clear it so it does
|
|
4
|
+
// not contaminate the first @message reply). A @message echoes. Mirrors toil-backend's connect_src.ts;
|
|
5
|
+
// exercises the whole @connect bridge (stream_info block -> StreamInbound.path() -> accept/reject).
|
|
6
|
+
|
|
7
|
+
@stream('gate')
|
|
8
|
+
class Gate {
|
|
9
|
+
@connect onConnect(c: StreamInbound): StreamOutbound {
|
|
10
|
+
if (c.path() == "/blocked") return StreamOutbound.reject(0x0211);
|
|
11
|
+
if (c.path() == "/greet") {
|
|
12
|
+
const g = new Uint8Array(3);
|
|
13
|
+
g[0] = 0x47; g[1] = 0x48; g[2] = 0x49; // "GHI"
|
|
14
|
+
StreamOutbound.reply(g);
|
|
15
|
+
return StreamOutbound.accept();
|
|
16
|
+
}
|
|
17
|
+
return StreamOutbound.accept();
|
|
18
|
+
}
|
|
19
|
+
@message reply(p: StreamPacket): StreamOutbound {
|
|
20
|
+
return StreamOutbound.reply(p.bytes());
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function probe(): i32 { return 1; }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Dev trap fixture: a `@stream('trap')` whose `@message` deliberately TRAPS (the wasm `unreachable`
|
|
2
|
+
// instruction). The dev has no gas-metering middleware, so this stands in for the edge's gas-kill: a
|
|
3
|
+
// real trap makes the dispatch throw, which StreamDevHost turns into a STREAM_HOOK_TRAPPED close that
|
|
4
|
+
// discards the poisoned box (mirrors toil-backend's poisoned-box containment, 05 7.4).
|
|
5
|
+
//
|
|
6
|
+
// The trap sits behind an always-true guard so the `return` stays reachable to the type checker.
|
|
7
|
+
|
|
8
|
+
@stream('trap')
|
|
9
|
+
class Trap {
|
|
10
|
+
@message boom(p: StreamPacket): StreamOutbound {
|
|
11
|
+
if (p.length >= 0) {
|
|
12
|
+
unreachable();
|
|
13
|
+
}
|
|
14
|
+
return StreamOutbound.empty();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function probe(): i32 { return 1; }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// A TYPED @stream fixture: @stream({ message: ChatMsg }) makes @message receive the DECODED @data
|
|
2
|
+
// (doc 03 2.5), not raw bytes. The hook replies with the decoded text's length, so a host round-trip
|
|
3
|
+
// proves the @data decode ran at runtime (not just compiled). The seed* exports hand the host a real
|
|
4
|
+
// ChatMsg.encode() payload so the test never hand-crafts the @data wire format.
|
|
5
|
+
|
|
6
|
+
@data
|
|
7
|
+
class ChatMsg {
|
|
8
|
+
text: string = '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@stream({ message: ChatMsg })
|
|
12
|
+
class TypedChat {
|
|
13
|
+
@message
|
|
14
|
+
onMsg(m: ChatMsg): StreamOutbound {
|
|
15
|
+
// Reply with the DECODED text length (one byte): "hi" -> 2 proves the decode produced the string.
|
|
16
|
+
const r = new Uint8Array(1);
|
|
17
|
+
r[0] = <u8>m.text.length;
|
|
18
|
+
return StreamOutbound.reply(r);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Seed buffer: the guest encodes a ChatMsg the host feeds back, so the host never hand-crafts @data
|
|
23
|
+
// bytes (the encode/decode pair is the wire contract). A StaticArray<u8>'s pointer IS its data start.
|
|
24
|
+
let seedBuf = new StaticArray<u8>(64);
|
|
25
|
+
let seedLen: i32 = 0;
|
|
26
|
+
|
|
27
|
+
export function seedHi(): void {
|
|
28
|
+
const m = new ChatMsg();
|
|
29
|
+
m.text = 'hi';
|
|
30
|
+
const b = m.encode();
|
|
31
|
+
seedLen = b.length;
|
|
32
|
+
for (let i = 0, n = b.length; i < n; i++) seedBuf[i] = b[i];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function seedOffset(): i32 {
|
|
36
|
+
return changetype<i32>(seedBuf);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function seedLength(): i32 {
|
|
40
|
+
return seedLen;
|
|
41
|
+
}
|