toiljs 0.0.73 → 0.0.75

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 (46) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/TYPESCRIPT_LAW.md +1 -3
  3. package/build/client/.tsbuildinfo +1 -1
  4. package/build/client/navigation/Link.js +5 -1
  5. package/build/client/routing/mount.js +19 -17
  6. package/build/compiler/.tsbuildinfo +1 -1
  7. package/build/compiler/index.js +11 -1
  8. package/build/devserver/.tsbuildinfo +1 -1
  9. package/build/devserver/analytics/index.d.ts +3 -0
  10. package/build/devserver/analytics/index.js +111 -0
  11. package/build/devserver/production.js +2 -1
  12. package/build/devserver/runtime/host.js +2 -0
  13. package/build/devserver/runtime/module.js +2 -0
  14. package/build/devserver/stream/index.d.ts +2 -1
  15. package/build/devserver/stream/index.js +3 -11
  16. package/build/devserver/stream/manager.d.ts +2 -0
  17. package/build/devserver/stream/manager.js +7 -6
  18. package/build/devserver/stream/ws.js +4 -0
  19. package/examples/basic/client/routes/analytics.tsx +55 -0
  20. package/examples/basic/client/routes/rest.tsx +1 -1
  21. package/examples/basic/server/main.ts +1 -0
  22. package/examples/basic/server/models/SiteAnalytics.ts +14 -0
  23. package/examples/basic/server/routes/Analytics.ts +30 -0
  24. package/examples/basic/server/streams/Echo.ts +19 -16
  25. package/package.json +2 -9
  26. package/src/client/navigation/Link.tsx +15 -1
  27. package/src/client/routing/mount.tsx +41 -34
  28. package/src/compiler/index.ts +14 -1
  29. package/src/devserver/analytics/index.ts +158 -0
  30. package/src/devserver/production.ts +7 -1
  31. package/src/devserver/runtime/host.ts +6 -0
  32. package/src/devserver/runtime/module.ts +3 -0
  33. package/src/devserver/stream/index.ts +8 -19
  34. package/src/devserver/stream/manager.ts +15 -11
  35. package/src/devserver/stream/ws.ts +2 -0
  36. package/test/assembly/aspect-shim.ts +33 -0
  37. package/test/assembly/cookie.spec.ts +1 -0
  38. package/test/assembly/example.spec.ts +1 -0
  39. package/test/assembly/ssr.spec.ts +1 -0
  40. package/test/assembly.test.ts +59 -0
  41. package/test/dom/Link.test.tsx +20 -0
  42. package/test/fixtures/stream-gate.ts +19 -15
  43. package/test/stream-emulation.test.ts +56 -10
  44. package/vitest.config.ts +2 -1
  45. package/as-pect.asconfig.json +0 -34
  46. package/as-pect.config.js +0 -71
@@ -81,10 +81,10 @@ export type StreamMessageOutcome =
81
81
  | { readonly kind: 'reply'; readonly frames: Buffer[] }
82
82
  | { readonly kind: 'reject'; readonly code: number };
83
83
 
84
- /** A `@connect` outcome: the guest accepted (the box is usable) or rejected with a `0x02xx` code. */
84
+ /** A `@connect` outcome: accepted/rejected plus any immediate frames staged during the hook. */
85
85
  export type StreamConnectOutcome =
86
- | { readonly kind: 'accept' }
87
- | { readonly kind: 'reject'; readonly code: number };
86
+ | { readonly kind: 'accept'; readonly initialEgress: Buffer[] }
87
+ | { readonly kind: 'reject'; readonly code: number; readonly initialEgress: Buffer[] };
88
88
 
89
89
  export class DevStreamBox {
90
90
  private constructor(
@@ -132,17 +132,16 @@ export class DevStreamBox {
132
132
 
133
133
  /**
134
134
  * Fire `@connect`: write the connect-info block (stream id + transport + authority + path) into the
135
- * guest's info region, dispatch `EVENT_CONNECT`, and decode the `StreamOutbound` accept/reject. On
136
- * accept, clear any `@connect`-staged egress (initial-egress is deferred, like the edge) so it does
137
- * not contaminate the first `@message` reply. A box without the bridge runs `@connect` context-free
135
+ * guest's info region, dispatch `EVENT_CONNECT`, decode the `StreamOutbound` accept/reject, and
136
+ * drain any staged egress as initial frames. A box without the bridge runs `@connect` context-free
138
137
  * (the write is a no-op) and always accepts unless it returns a negative code.
139
138
  */
140
139
  onConnect(streamId: bigint, authority: string, path: string): StreamConnectOutcome {
141
140
  this.writeConnectInfo(streamId, authority, path);
142
141
  const rc = this.dispatch(EVENT_CONNECT, streamId);
143
- if (rc < 0n) return { kind: 'reject', code: decodeRejectCode(rc) };
144
- this.resetEgressRing();
145
- return { kind: 'accept' };
142
+ const initialEgress = this.egressDrain();
143
+ if (rc < 0n) return { kind: 'reject', code: decodeRejectCode(rc), initialEgress };
144
+ return { kind: 'accept', initialEgress };
146
145
  }
147
146
 
148
147
  /** Fire `@close` (graceful close). */
@@ -241,16 +240,6 @@ export class DevStreamBox {
241
240
  if (pathLen > 0) memU8.set(pathBytes.subarray(0, pathLen), base + SI_BODY + authLen);
242
241
  }
243
242
 
244
- /** Zero the egress ring cursors, discarding any staged frames. Safe between dispatches (the guest,
245
- * the sole egress producer, is idle). */
246
- private resetEgressRing(): void {
247
- const rings = this.rings;
248
- if (!rings) return;
249
- const dv = new DataView(this.exports.memory.buffer);
250
- dv.setUint32(rings.egressOff + RC_WRITE, 0, true);
251
- dv.setUint32(rings.egressOff + RC_READ, 0, true);
252
- }
253
-
254
243
  /** Host (producer) writes ONE inbound RingFrame into the ingress ring with the drained-reset
255
244
  * (host owns write_cursor; reset read_cursor only when the guest has drained). */
256
245
  private ingressWrite(inbound: Buffer): void {
@@ -32,8 +32,8 @@ export const STREAM_HOOK_TRAPPED = 0x0200;
32
32
 
33
33
  /** The outcome of an upgrade accept attempt (mirrors the edge `StreamUpgradeOutcome`). */
34
34
  export type StreamUpgradeOutcome =
35
- | { readonly kind: 'accepted'; readonly streamId: bigint }
36
- | { readonly kind: 'rejected'; readonly code: number };
35
+ | { readonly kind: 'accepted'; readonly streamId: bigint; readonly initialEgress: Buffer[] }
36
+ | { readonly kind: 'rejected'; readonly code: number; readonly initialEgress: Buffer[] };
37
37
 
38
38
  /** The outcome of driving one inbound frame (mirrors the edge `StreamDatagramOutcome`). */
39
39
  export type StreamDispatchResult =
@@ -69,30 +69,34 @@ export class StreamDevHost {
69
69
  * Accept (or reject) an upgrade for `authority` + `path`, mirroring `StreamWorker::accept_upgrade`:
70
70
  * (re)load the artifact on mtime change, instantiate a resident box, fire `@connect` WITH the
71
71
  * connect context, and HONOR the guest's accept/reject. Returns the host-assigned stream id on
72
- * accept, or a `0x02xx` reject code. Fails closed (no connection registered, box dropped) on a
73
- * missing/unreadable artifact (`0x0208`), a `@connect` reject (the guest's code), or a load/connect
74
- * trap (`0x0200`). Throws only on a duplicate `connId`.
72
+ * accept, plus any initial egress staged by `@connect`, or a `0x02xx` reject code. Fails closed
73
+ * (no connection registered, box dropped) on a missing/unreadable artifact (`0x0208`), a
74
+ * `@connect` reject (the guest's code), or a load/connect trap (`0x0200`). Throws only on a
75
+ * duplicate `connId`.
75
76
  */
76
77
  acceptUpgrade(connId: string, authority: string, path: string): StreamUpgradeOutcome {
77
- if (this.conns.has(connId)) throw new Error(`stream connection '${connId}' is already open`);
78
+ if (this.conns.has(connId))
79
+ throw new Error(`stream connection '${connId}' is already open`);
78
80
  this.refresh();
79
- if (!this.bytes) return { kind: 'rejected', code: STREAM_REJECTED };
81
+ if (!this.bytes) return { kind: 'rejected', code: STREAM_REJECTED, initialEgress: [] };
80
82
  let box: DevStreamBox;
81
83
  try {
82
84
  box = DevStreamBox.load(this.bytes);
83
85
  } catch {
84
- return { kind: 'rejected', code: STREAM_REJECTED };
86
+ return { kind: 'rejected', code: STREAM_REJECTED, initialEgress: [] };
85
87
  }
86
88
  const streamId = this.allocStreamId();
87
89
  let outcome;
88
90
  try {
89
91
  outcome = box.onConnect(streamId, authority, path);
90
92
  } catch {
91
- return { kind: 'rejected', code: STREAM_HOOK_TRAPPED }; // @connect trapped
93
+ return { kind: 'rejected', code: STREAM_HOOK_TRAPPED, initialEgress: [] }; // @connect trapped
94
+ }
95
+ if (outcome.kind === 'reject') {
96
+ return { kind: 'rejected', code: outcome.code, initialEgress: outcome.initialEgress };
92
97
  }
93
- if (outcome.kind === 'reject') return { kind: 'rejected', code: outcome.code };
94
98
  this.conns.set(connId, { box, streamId });
95
- return { kind: 'accepted', streamId };
99
+ return { kind: 'accepted', streamId, initialEgress: outcome.initialEgress };
96
100
  }
97
101
 
98
102
  /**
@@ -45,10 +45,12 @@ export class StreamWsSession {
45
45
  onOpen(): boolean {
46
46
  const up = this.host.acceptUpgrade(this.connId, this.authority, this.path);
47
47
  if (up.kind === 'rejected') {
48
+ for (const frame of up.initialEgress) this.transport.send(frame);
48
49
  this.transport.close(up.code);
49
50
  return false;
50
51
  }
51
52
  this.open = true;
53
+ for (const frame of up.initialEgress) this.transport.send(frame);
52
54
  return true;
53
55
  }
54
56
 
@@ -0,0 +1,33 @@
1
+ // A minimal in-AssemblyScript test shim for this directory's assembly specs.
2
+ // `describe`/`it` run their bodies inline; `expect(x).toBe(y)` / `.toStrictEqual(y)` assert
3
+ // `x == y` (value-equality, which covers the bool / i32 / string the specs compare). A failed
4
+ // assertion aborts with the current test name; the vitest runner (../assembly.test.ts) compiles
5
+ // each spec with toilscript, runs `_start`, and surfaces the abort as a test failure.
6
+
7
+ let currentTest: string = '';
8
+
9
+ export function describe(_name: string, fn: () => void): void {
10
+ fn();
11
+ }
12
+
13
+ export function it(name: string, fn: () => void): void {
14
+ currentTest = name;
15
+ fn();
16
+ }
17
+
18
+ export class Expectation<T> {
19
+ private actual: T;
20
+ constructor(actual: T) {
21
+ this.actual = actual;
22
+ }
23
+ toBe(expected: T): void {
24
+ assert(this.actual == expected, currentTest);
25
+ }
26
+ toStrictEqual(expected: T): void {
27
+ assert(this.actual == expected, currentTest);
28
+ }
29
+ }
30
+
31
+ export function expect<T>(actual: T): Expectation<T> {
32
+ return new Expectation<T>(actual);
33
+ }
@@ -5,6 +5,7 @@
5
5
  // as-pect compiler does not ship. `SecureCookies`
6
6
  // is exercised end-to-end against the real toilscript-compiled wasm in
7
7
  // `test/devserver.test.ts`.
8
+ import { describe, it, expect } from './aspect-shim';
8
9
  import { Method, Request, Header } from '../../server/runtime/request';
9
10
  import { Response } from '../../server/runtime/response';
10
11
  import { Cookie, SameSite, CookieEncoding } from '../../server/runtime/http/cookie';
@@ -1,6 +1,7 @@
1
1
  // Imports the specific modules rather than the runtime index: the index
2
2
  // re-exports `SecureCookies`, which depends on the toilscript crypto std the
3
3
  // as-pect compiler does not ship (see test/assembly/cookie.spec.ts).
4
+ import { describe, it, expect } from './aspect-shim';
4
5
  import { Method } from '../../server/runtime/request';
5
6
  import { Response } from '../../server/runtime/response';
6
7
 
@@ -3,6 +3,7 @@
3
3
  // (escaping + buffer building + linear-memory encode), so they run under
4
4
  // as-pect; the full `render` export is exercised via the example wasm in
5
5
  // test/devserver.test.ts.
6
+ import { describe, it, expect } from './aspect-shim';
6
7
  import { escapeHtml, escapeJsonForScript } from '../../server/runtime/ssr/escape';
7
8
  import { HASH_LEN, HtmlBuilder, SlotKind, SlotValues } from '../../server/runtime/ssr/slots';
8
9
  import { encodeValues } from '../../server/runtime/ssr/encode';
@@ -0,0 +1,59 @@
1
+ // Runs the AssemblyScript specs in test/assembly/*.spec.ts by compiling each with toilscript and
2
+ // executing its `_start` (which runs the describe/it bodies + the shim's asserts). A failed assert
3
+ // aborts; this runner surfaces it as a vitest failure. Runs them via toilscript, no external runner.
4
+ import { describe, it } from 'vitest';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join, dirname } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const here = dirname(fileURLToPath(import.meta.url));
12
+ const TOILSCRIPT_BIN = join(here, '..', 'node_modules', 'toilscript', 'bin', 'toilscript.js');
13
+ const SPECS = ['example', 'cookie', 'ssr'] as const;
14
+
15
+ function liftString(ptr: number, mem: WebAssembly.Memory): string {
16
+ if (!ptr) return '(no message)';
17
+ const u32 = new Uint32Array(mem.buffer);
18
+ const len = (u32[(ptr - 4) >>> 2] >>> 1) >>> 0;
19
+ const u16 = new Uint16Array(mem.buffer);
20
+ let s = '';
21
+ const start = ptr >>> 1;
22
+ for (let i = 0; i < len; i++) s += String.fromCharCode(u16[start + i]);
23
+ return s;
24
+ }
25
+
26
+ describe('assembly specs (toilscript-compiled)', () => {
27
+ for (const spec of SPECS) {
28
+ it(`${spec}.spec.ts passes`, async () => {
29
+ const tmp = mkdtempSync(join(tmpdir(), 'toiljs-asm-'));
30
+ try {
31
+ const src = join(here, 'assembly', `${spec}.spec.ts`);
32
+ const out = join(tmp, `${spec}.wasm`);
33
+ const r = spawnSync(
34
+ 'node',
35
+ [TOILSCRIPT_BIN, src, '-o', out, '--exportStart', '_start', '--runtime', 'stub'],
36
+ { encoding: 'utf8' },
37
+ );
38
+ if (r.status !== 0) {
39
+ throw new Error(`toilscript compile ${spec}.spec.ts failed:\n${r.stderr}${r.stdout}`);
40
+ }
41
+ const wasm = readFileSync(out);
42
+ const holder: { mem: WebAssembly.Memory | null } = { mem: null };
43
+ const { instance } = await WebAssembly.instantiate(wasm, {
44
+ env: {
45
+ abort(msg: number, _file: number, line: number): void {
46
+ const m = holder.mem ? liftString(msg >>> 0, holder.mem) : '';
47
+ throw new Error(`${spec}.spec.ts assertion failed: "${m}" (line ${line})`);
48
+ },
49
+ seed: () => 0,
50
+ },
51
+ });
52
+ holder.mem = instance.exports.memory as WebAssembly.Memory;
53
+ (instance.exports._start as () => void)();
54
+ } finally {
55
+ rmSync(tmp, { recursive: true, force: true });
56
+ }
57
+ });
58
+ }
59
+ });
@@ -31,6 +31,26 @@ describe('Link', () => {
31
31
  expect(window.location.pathname).toBe('/');
32
32
  });
33
33
 
34
+ it('ignores a click when href is missing (data-driven href to a page that does not exist)', () => {
35
+ // A runtime-undefined href (e.g. `routeMap[missingKey]`) used to throw on `href.startsWith`
36
+ // inside the click handler. React reports a handler throw as an uncaught *window* error (what
37
+ // the dev overlay surfaces), not a synchronous throw, so assert no such error fires. The anchor
38
+ // is inert now: the click is left to the browser, the page stays put.
39
+ const errors: string[] = [];
40
+ const onError = (e: ErrorEvent): void => {
41
+ errors.push(e.message || String(e.error));
42
+ };
43
+ window.addEventListener('error', onError);
44
+ try {
45
+ const { getByText } = render(<Link href={undefined as unknown as string as never}>x</Link>);
46
+ fireEvent.click(getByText('x'));
47
+ } finally {
48
+ window.removeEventListener('error', onError);
49
+ }
50
+ expect(errors).toEqual([]);
51
+ expect(window.location.pathname).toBe('/');
52
+ });
53
+
34
54
  it('forwards anchor attributes (rel, target)', () => {
35
55
  const { getByText } = render(
36
56
  <Link
@@ -1,24 +1,28 @@
1
1
  // Dev @connect-bridge fixture: a `@stream('gate')` whose `@connect(c: StreamInbound): StreamOutbound`
2
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;
3
+ // other path; a "/greet" path stages an egress frame DURING @connect (the host returns it as initial
4
+ // egress and drains the ring so it does not contaminate the first @message reply). A @message echoes. Mirrors toil-backend's connect_src.ts;
5
5
  // exercises the whole @connect bridge (stream_info block -> StreamInbound.path() -> accept/reject).
6
6
 
7
7
  @stream('gate')
8
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();
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;
14
+ g[1] = 0x48;
15
+ g[2] = 0x49; // "GHI"
16
+ StreamOutbound.reply(g);
17
+ return StreamOutbound.accept();
18
+ }
19
+ return StreamOutbound.accept();
20
+ }
21
+ @message reply(p: StreamPacket): StreamOutbound {
22
+ return StreamOutbound.reply(p.bytes());
16
23
  }
17
- return StreamOutbound.accept();
18
- }
19
- @message reply(p: StreamPacket): StreamOutbound {
20
- return StreamOutbound.reply(p.bytes());
21
- }
22
24
  }
23
25
 
24
- export function probe(): i32 { return 1; }
26
+ export function probe(): i32 {
27
+ return 1;
28
+ }
@@ -30,8 +30,10 @@ import {
30
30
  import { StreamWsSession, type StreamWsTransport } from '../src/devserver/stream/ws.js';
31
31
 
32
32
  const here = dirname(fileURLToPath(import.meta.url));
33
- // The LOCAL toilscript build (the @message + @connect bridge codegen); the published dep predates it.
34
- const LOCAL_TOILSCRIPT_BIN = join(here, '..', '..', 'toilscript', 'bin', 'toilscript.js');
33
+ // The toilscript build with the @message + @connect bridge codegen (it ships in the dep now).
34
+ // Resolved from node_modules so this works both locally (the symlinked repo) and in CI (the npm
35
+ // dep), not from a sibling checkout that CI does not have.
36
+ const LOCAL_TOILSCRIPT_BIN = join(here, '..', 'node_modules', 'toilscript', 'bin', 'toilscript.js');
35
37
 
36
38
  let tmp: string;
37
39
  let ECHO_PATH: string;
@@ -50,7 +52,9 @@ function compile(srcName: string): { path: string; wasm: Buffer } {
50
52
  { encoding: 'utf8' },
51
53
  );
52
54
  if (r.status !== 0) {
53
- throw new Error(`toilscript compile ${srcName} failed (${String(r.status)}):\n${r.stderr}${r.stdout}`);
55
+ throw new Error(
56
+ `toilscript compile ${srcName} failed (${String(r.status)}):\n${r.stderr}${r.stdout}`,
57
+ );
54
58
  }
55
59
  return { path: out, wasm: readFileSync(out) };
56
60
  }
@@ -140,10 +144,17 @@ describe('dev stream box: the @connect info-block bridge', () => {
140
144
  it('reads the connect path and rejects /blocked while accepting others', () => {
141
145
  const box = DevStreamBox.load(GATE);
142
146
  expect(box.hasConnectBridge).toBe(true);
143
- expect(box.onConnect(id, 'acme.toil', '/blocked')).toEqual({ kind: 'reject', code: 0x0211 });
147
+ expect(box.onConnect(id, 'acme.toil', '/blocked')).toEqual({
148
+ kind: 'reject',
149
+ code: 0x0211,
150
+ initialEgress: [],
151
+ });
144
152
 
145
153
  const ok = DevStreamBox.load(GATE);
146
- expect(ok.onConnect(id, 'acme.toil', '/room/42')).toEqual({ kind: 'accept' });
154
+ expect(ok.onConnect(id, 'acme.toil', '/room/42')).toEqual({
155
+ kind: 'accept',
156
+ initialEgress: [],
157
+ });
147
158
  // An accepted connection is usable: its @message echoes.
148
159
  expect(ok.onMessage(id, Buffer.from('hi'))).toEqual({
149
160
  kind: 'reply',
@@ -151,11 +162,14 @@ describe('dev stream box: the @connect info-block bridge', () => {
151
162
  });
152
163
  });
153
164
 
154
- it('clears @connect-staged egress so the first @message reply is clean', () => {
165
+ it('returns @connect-staged egress and keeps the first @message reply clean', () => {
155
166
  const box = DevStreamBox.load(GATE);
156
- // /greet stages "GHI" during @connect; the host clears it on accept.
157
- expect(box.onConnect(id, 'acme.toil', '/greet')).toEqual({ kind: 'accept' });
158
- // The first @message must see ONLY its own reply, never the stale "GHI".
167
+ // /greet stages "GHI" during @connect; the host returns it as initial egress.
168
+ expect(box.onConnect(id, 'acme.toil', '/greet')).toEqual({
169
+ kind: 'accept',
170
+ initialEgress: [Buffer.from('GHI')],
171
+ });
172
+ // The first @message must see ONLY its own reply, never the drained initial "GHI".
159
173
  expect(box.onMessage(id, Buffer.from('hi'))).toEqual({
160
174
  kind: 'reply',
161
175
  frames: [Buffer.from('hi')],
@@ -185,6 +199,7 @@ describe('dev stream session driver (StreamDevHost)', () => {
185
199
  expect(host.acceptUpgrade('c1', 'acme.toil', '/blocked')).toEqual({
186
200
  kind: 'rejected',
187
201
  code: 0x0211,
202
+ initialEgress: [],
188
203
  });
189
204
  expect(host.activeConnections).toBe(0);
190
205
  expect(host.acceptUpgrade('c2', 'acme.toil', '/ok').kind).toBe('accepted');
@@ -211,6 +226,18 @@ describe('dev stream session driver (StreamDevHost)', () => {
211
226
  host.acceptUpgrade('c1', 'acme.toil', '/');
212
227
  expect(() => host.acceptUpgrade('c1', 'acme.toil', '/')).toThrow();
213
228
  });
229
+
230
+ it('returns initial egress from @connect while keeping dispatch clean', () => {
231
+ const host = new StreamDevHost(GATE_PATH);
232
+ expect(host.acceptUpgrade('c1', 'acme.toil', '/greet')).toMatchObject({
233
+ kind: 'accepted',
234
+ initialEgress: [Buffer.from('GHI')],
235
+ });
236
+ expect(host.dispatch('c1', Buffer.from('hi'))).toEqual({
237
+ kind: 'reply',
238
+ frames: [Buffer.from('hi')],
239
+ });
240
+ });
214
241
  });
215
242
 
216
243
  describe('dev stream WS session adapter (StreamWsSession)', () => {
@@ -237,6 +264,20 @@ describe('dev stream WS session adapter (StreamWsSession)', () => {
237
264
  expect(host.activeConnections).toBe(0);
238
265
  });
239
266
 
267
+ it('sends @connect initial egress on open', () => {
268
+ const host = new StreamDevHost(GATE_PATH);
269
+ const { sent, closed, t } = makeTransport();
270
+ const s = new StreamWsSession(host, 'ws1', 'acme.toil', '/greet', t);
271
+ expect(s.onOpen()).toBe(true);
272
+ expect(s.isOpen).toBe(true);
273
+ expect(sent).toEqual([Buffer.from('GHI')]);
274
+ expect(closed).toEqual([]);
275
+ s.onMessage(Buffer.from('hi'));
276
+ expect(sent).toEqual([Buffer.from('GHI'), Buffer.from('hi')]);
277
+ s.onClose();
278
+ expect(host.activeConnections).toBe(0);
279
+ });
280
+
240
281
  it('closes the socket with the code on a guest reject, then fires @close on socket close', () => {
241
282
  const host = new StreamDevHost(ECHO_PATH);
242
283
  const { closed, t } = makeTransport();
@@ -310,7 +351,12 @@ describe('dev stream router (doc 08 4.1/4.2)', () => {
310
351
  expect(router.matchRoute('/not-a-stream')).toBeNull(); // -> proxied to Vite
311
352
 
312
353
  const ws = new MockWs();
313
- const ctx: StreamUpgradeContext = { kind: 'stream', route, url: route, authority: 'acme.toil' };
354
+ const ctx: StreamUpgradeContext = {
355
+ kind: 'stream',
356
+ route,
357
+ url: route,
358
+ authority: 'acme.toil',
359
+ };
314
360
  router.onUpgrade(ws, ctx);
315
361
  expect(router.activeConnections).toBe(1);
316
362
  ws.emitMessage(Buffer.from('hi'));
package/vitest.config.ts CHANGED
@@ -10,7 +10,8 @@ export default defineConfig({
10
10
  // docs.ts) before the suite, so a fresh checkout can test without a build.
11
11
  globalSetup: ['./test/global-setup.ts'],
12
12
  include: ['test/**/*.test.ts', 'test/**/*.test.tsx', 'test/**/*.spec.ts'],
13
- // test/assembly holds toilscript specs run by as-pect, not vitest.
13
+ // test/assembly/*.spec.ts are AssemblyScript specs, compiled + run via toilscript by
14
+ // test/assembly.test.ts (not executed by vitest directly).
14
15
  exclude: [...configDefaults.exclude, 'test/assembly/**'],
15
16
  coverage: {
16
17
  provider: 'v8',
@@ -1,34 +0,0 @@
1
- {
2
- "targets": {
3
- "coverage": {
4
- "lib": [
5
- "./node_modules/@btc-vision/as-covers-assembly/index.ts"
6
- ],
7
- "transform": [
8
- "@btc-vision/as-covers-transform",
9
- "@btc-vision/as-pect-transform"
10
- ]
11
- },
12
- "noCoverage": {
13
- "transform": [
14
- "@btc-vision/as-pect-transform"
15
- ]
16
- }
17
- },
18
- "options": {
19
- "outFile": "output.wasm",
20
- "textFile": "output.wat",
21
- "debug": true,
22
- "bindings": "raw",
23
- "exportStart": "_start",
24
- "exportMemory": true,
25
- "exportRuntime": true,
26
- "exportTable": true,
27
- "use": [
28
- "RTRACE=1"
29
- ]
30
- },
31
- "entries": [
32
- "./node_modules/@btc-vision/as-pect-assembly/assembly/index.ts"
33
- ]
34
- }
package/as-pect.config.js DELETED
@@ -1,71 +0,0 @@
1
- function __liftString(pointer, memory) {
2
- if (!pointer) return null;
3
- const end = (pointer + new Uint32Array(memory.buffer)[(pointer - 4) >>> 2]) >>> 1,
4
- memoryU16 = new Uint16Array(memory.buffer);
5
- let start = pointer >>> 1,
6
- string = '';
7
- while (end - start > 1024) {
8
- const a = memoryU16.subarray(start, (start += 1024));
9
- string += String.fromCharCode(...a);
10
- }
11
- return string + String.fromCharCode(...memoryU16.subarray(start, end));
12
- }
13
-
14
- function log(text, memory) {
15
- text = __liftString(text >>> 0, memory);
16
- console.log(`CONTRACT LOG: ${text}`);
17
- }
18
-
19
- export default {
20
- /**
21
- * A set of globs passed to the glob package that qualify typescript files for testing.
22
- */
23
- entries: ['test/assembly/**/*.spec.ts'],
24
-
25
- /**
26
- * A set of globs passed to the glob package that quality files to be added to each test.
27
- */
28
- include: ['test/assembly/**/*.include.ts'],
29
-
30
- /**
31
- * A set of regexp that will disclude source files from testing.
32
- */
33
- disclude: [/node_modules/],
34
-
35
- /**
36
- * Add your required toilscript imports here.
37
- */
38
- async instantiate(memory, createImports, instantiate, binary) {
39
- let memory2;
40
- const resp = instantiate(
41
- binary,
42
- createImports({
43
- env: {
44
- memory,
45
- 'console.log': (data) => {
46
- log(data, memory2);
47
- },
48
- },
49
- }),
50
- );
51
-
52
- const { exports } = await resp;
53
- memory2 = exports.memory || memory;
54
-
55
- return resp;
56
- },
57
-
58
- /**
59
- * Enable code coverage. `securecookies.ts` is excluded: it depends on the
60
- * toilscript crypto std (`crypto` / `data` / `bindings/webcrypto`), which the
61
- * as-pect compiler does not ship, so it cannot
62
- * compile here. It is covered end-to-end against the real wasm in
63
- * `test/devserver.test.ts`.
64
- */
65
- coverage: ['server/**/*.ts', 'server/*.ts', '!server/runtime/http/securecookies.ts'],
66
-
67
- /**
68
- * Specify if the binary wasm file should be written to the file system.
69
- */
70
- outputBinary: false,
71
- };