toiljs 0.0.53 → 0.0.55

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.
@@ -0,0 +1,459 @@
1
+ /**
2
+ * DEV emulation of the ToilDB record-family data API (`env::data.*`).
3
+ *
4
+ * The compiler-emitted guest (`@database` + `App.users.get/create/...`) calls
5
+ * these; under `toiljs dev` we back them with a single-process in-memory store.
6
+ * The production edge backs the SAME imports with ScyllaDB via the off-core
7
+ * "scylla rail" (toil-backend). The wire ABI mirrors the edge exactly
8
+ * (toildb/ABI.md): every byte region is a (ptr,len) into guest memory; returns
9
+ * are `>= 0` success (a length/handle/flag), `-1` too-small, `-2` absent,
10
+ * `<= -1000` a typed error (`-(1000+TDLnnn)`); variable-length results use the
11
+ * two-step `take_result` pull.
12
+ *
13
+ * This is a DEV store: single-process, single-tenant, lost on restart - never a
14
+ * production path. Record family only (get/exists/create/patch/delete/
15
+ * get_delete + resolve/take_result); other families land with their host shims.
16
+ */
17
+
18
+ import type { MemoryRef } from './host.js';
19
+
20
+ /** Process-lifetime store: `"collection\0keyLatin1"` -> value. Shared across dispatches. */
21
+ const STORE = new Map<string, Buffer>();
22
+ /** View family: `"collection\0key"` -> the latest published view blob. */
23
+ const VIEWS = new Map<string, Buffer>();
24
+ /** Membership family: `"collection\0setKey"` -> (memberLatin1 -> member bytes). */
25
+ const MEMBERS = new Map<string, Map<string, Buffer>>();
26
+ /** Counter family: `"collection\0key"` -> saturating i64 sum of deltas. */
27
+ const COUNTERS = new Map<string, bigint>();
28
+ /** Events family: `"collection\0key"` -> append-ordered event blobs (oldest first). */
29
+ const EVENTS = new Map<string, Buffer[]>();
30
+
31
+ const MAX_NAME = 512;
32
+ const MAX_KEY = 4096;
33
+ const MAX_VALUE = 256 * 1024;
34
+
35
+ // i64 saturation bounds (the edge `MemEngine`/`ScyllaEngine` counters are i64).
36
+ const I64_MIN = -(2n ** 63n);
37
+ const I64_MAX = 2n ** 63n - 1n;
38
+ function satI64(v: bigint): bigint {
39
+ return v < I64_MIN ? I64_MIN : v > I64_MAX ? I64_MAX : v;
40
+ }
41
+
42
+ // Return codes, mirroring `toildb::observe::diagnostics`.
43
+ const ABSENT = -2;
44
+ const TOO_SMALL = -1;
45
+ const INVALID_HANDLE = -1001; // -(1000 + TDL001)
46
+ const PRODUCT_ERR = -1000; // AlreadyExists / NotFound / Conflict (TDL000)
47
+
48
+ /** Per-request data state: resolved handles + the last variable-length result. */
49
+ export interface DbDevState {
50
+ handles: string[];
51
+ lastResult: Buffer | null;
52
+ }
53
+
54
+ export function freshDbState(): DbDevState {
55
+ return { handles: [], lastResult: null };
56
+ }
57
+
58
+ function mem(ref: MemoryRef): Buffer {
59
+ if (!ref.memory) throw new Error('data host import called before memory was bound');
60
+ return Buffer.from(ref.memory.buffer);
61
+ }
62
+
63
+ /** Bounds-checked read that COPIES out of guest memory (the buffer is reused). */
64
+ function readCopy(ref: MemoryRef, ptr: number, len: number): Buffer {
65
+ const m = mem(ref);
66
+ if (ptr < 0 || len < 0 || ptr + len > m.length)
67
+ throw new Error(`data read out of bounds: ptr=${String(ptr)} len=${String(len)}`);
68
+ return Buffer.from(m.subarray(ptr, ptr + len));
69
+ }
70
+
71
+ function storeKey(collection: string, key: Buffer): string {
72
+ return collection + '\0' + key.toString('latin1');
73
+ }
74
+
75
+ function collOf(db: DbDevState, handle: number): string | null {
76
+ return handle >= 0 && handle < db.handles.length ? db.handles[handle] : null;
77
+ }
78
+
79
+ export function buildDatabaseImports(
80
+ ref: MemoryRef,
81
+ db: DbDevState,
82
+ ): Record<string, (...args: number[]) => number> {
83
+ return {
84
+ 'data.resolve_collection': (namePtr: number, nameLen: number, outHandlePtr: number): number => {
85
+ if (nameLen < 0 || nameLen > MAX_NAME) throw new Error('data: collection name too long');
86
+ const name = readCopy(ref, namePtr, nameLen).toString('utf8');
87
+ const handle = db.handles.length;
88
+ db.handles.push(name);
89
+ const m = mem(ref);
90
+ if (outHandlePtr < 0 || outHandlePtr + 4 > m.length)
91
+ throw new Error('data: resolve out-handle out of bounds');
92
+ m.writeUInt32LE(handle, outHandlePtr);
93
+ return 0;
94
+ },
95
+
96
+ 'data.get': (handle: number, keyPtr: number, keyLen: number): number => {
97
+ const coll = collOf(db, handle);
98
+ if (coll === null) return INVALID_HANDLE;
99
+ if (keyLen > MAX_KEY) throw new Error('data: key too long');
100
+ const v = STORE.get(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
101
+ if (v === undefined) return ABSENT;
102
+ db.lastResult = v;
103
+ return v.length;
104
+ },
105
+
106
+ // Bounded multi-get. Keys blob: u32 count + per key (u32 len + bytes).
107
+ // Result (stashed): u32 count + per item u8 present (+ u32 len + bytes),
108
+ // in request order. Mirrors the edge `op_get_many` framing byte-for-byte.
109
+ 'data.get_many': (handle: number, keysPtr: number, keysLen: number): number => {
110
+ const coll = collOf(db, handle);
111
+ if (coll === null) return INVALID_HANDLE;
112
+ if (keysLen > MAX_VALUE) throw new Error('data: keys blob too large');
113
+ const blob = readCopy(ref, keysPtr, keysLen);
114
+ let off = 0;
115
+ const count = blob.readUInt32LE(off);
116
+ off += 4;
117
+ if (count > 1024) return PRODUCT_ERR; // anti-OOM cap, mirrors the edge
118
+ const header = Buffer.alloc(4);
119
+ header.writeUInt32LE(count, 0);
120
+ const parts: Buffer[] = [header];
121
+ for (let i = 0; i < count; i++) {
122
+ const len = blob.readUInt32LE(off);
123
+ off += 4;
124
+ const key = blob.subarray(off, off + len);
125
+ off += len;
126
+ const v = STORE.get(storeKey(coll, key));
127
+ if (v === undefined) {
128
+ parts.push(Buffer.from([0]));
129
+ } else {
130
+ const h = Buffer.alloc(5);
131
+ h.writeUInt8(1, 0);
132
+ h.writeUInt32LE(v.length, 1);
133
+ parts.push(h, v);
134
+ }
135
+ }
136
+ db.lastResult = Buffer.concat(parts);
137
+ return db.lastResult.length;
138
+ },
139
+
140
+ 'data.exists': (handle: number, keyPtr: number, keyLen: number): number => {
141
+ const coll = collOf(db, handle);
142
+ if (coll === null) return INVALID_HANDLE;
143
+ return STORE.has(storeKey(coll, readCopy(ref, keyPtr, keyLen))) ? 1 : 0;
144
+ },
145
+
146
+ 'data.create': (
147
+ handle: number,
148
+ keyPtr: number,
149
+ keyLen: number,
150
+ valPtr: number,
151
+ valLen: number,
152
+ _idemPtr: number,
153
+ ): number => {
154
+ const coll = collOf(db, handle);
155
+ if (coll === null) return INVALID_HANDLE;
156
+ if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/value too large');
157
+ const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
158
+ if (STORE.has(sk)) return PRODUCT_ERR; // AlreadyExists
159
+ STORE.set(sk, readCopy(ref, valPtr, valLen));
160
+ return 0;
161
+ },
162
+
163
+ 'data.patch': (
164
+ handle: number,
165
+ keyPtr: number,
166
+ keyLen: number,
167
+ patchPtr: number,
168
+ patchLen: number,
169
+ _idemPtr: number,
170
+ ): number => {
171
+ const coll = collOf(db, handle);
172
+ if (coll === null) return INVALID_HANDLE;
173
+ if (keyLen > MAX_KEY || patchLen > MAX_VALUE) throw new Error('data: key/patch too large');
174
+ const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
175
+ if (!STORE.has(sk)) return PRODUCT_ERR; // NotFound
176
+ const v = readCopy(ref, patchPtr, patchLen);
177
+ STORE.set(sk, v);
178
+ db.lastResult = v; // patch returns the stored record
179
+ return v.length;
180
+ },
181
+
182
+ 'data.delete': (
183
+ handle: number,
184
+ keyPtr: number,
185
+ keyLen: number,
186
+ _idemPtr: number,
187
+ ): number => {
188
+ const coll = collOf(db, handle);
189
+ if (coll === null) return INVALID_HANDLE;
190
+ STORE.delete(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
191
+ return 0;
192
+ },
193
+
194
+ // Atomic fetch-and-delete (consume-once); deletes only on a real read.
195
+ 'data.get_delete': (
196
+ handle: number,
197
+ keyPtr: number,
198
+ keyLen: number,
199
+ _idemPtr: number,
200
+ ): number => {
201
+ const coll = collOf(db, handle);
202
+ if (coll === null) return INVALID_HANDLE;
203
+ const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
204
+ const v = STORE.get(sk);
205
+ if (v === undefined) return ABSENT;
206
+ STORE.delete(sk);
207
+ db.lastResult = v;
208
+ return v.length;
209
+ },
210
+
211
+ // --- unique family (lookup / claim / release) ---
212
+
213
+ 'data.unique_lookup': (handle: number, keyPtr: number, keyLen: number): number => {
214
+ const coll = collOf(db, handle);
215
+ if (coll === null) return INVALID_HANDLE;
216
+ const v = STORE.get(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
217
+ if (v === undefined) return ABSENT;
218
+ db.lastResult = v;
219
+ return v.length;
220
+ },
221
+
222
+ // Tag: 0 Claimed, 1 AlreadyClaimed (owner stashed), 2 AlreadyOwnedByCaller.
223
+ 'data.unique_claim': (
224
+ handle: number,
225
+ keyPtr: number,
226
+ keyLen: number,
227
+ valPtr: number,
228
+ valLen: number,
229
+ _idemPtr: number,
230
+ ): number => {
231
+ const coll = collOf(db, handle);
232
+ if (coll === null) return INVALID_HANDLE;
233
+ if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/value too large');
234
+ const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
235
+ const owner = readCopy(ref, valPtr, valLen);
236
+ const existing = STORE.get(sk);
237
+ if (existing === undefined) {
238
+ STORE.set(sk, owner);
239
+ return 0; // Claimed
240
+ }
241
+ if (existing.equals(owner)) return 2; // AlreadyOwnedByCaller
242
+ db.lastResult = existing;
243
+ return 1; // AlreadyClaimed (current owner stashed)
244
+ },
245
+
246
+ 'data.unique_release': (
247
+ handle: number,
248
+ keyPtr: number,
249
+ keyLen: number,
250
+ valPtr: number,
251
+ valLen: number,
252
+ _idemPtr: number,
253
+ ): number => {
254
+ const coll = collOf(db, handle);
255
+ if (coll === null) return INVALID_HANDLE;
256
+ const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
257
+ const existing = STORE.get(sk);
258
+ if (existing === undefined) return 0; // idempotent
259
+ if (!existing.equals(readCopy(ref, valPtr, valLen))) return PRODUCT_ERR; // not the owner
260
+ STORE.delete(sk);
261
+ return 0;
262
+ },
263
+
264
+ // --- membership family (contains / add / remove / list) ---
265
+
266
+ 'data.membership_contains': (
267
+ handle: number,
268
+ setPtr: number,
269
+ setLen: number,
270
+ memberPtr: number,
271
+ memberLen: number,
272
+ ): number => {
273
+ const coll = collOf(db, handle);
274
+ if (coll === null) return INVALID_HANDLE;
275
+ const set = MEMBERS.get(storeKey(coll, readCopy(ref, setPtr, setLen)));
276
+ if (set === undefined) return 0;
277
+ return set.has(readCopy(ref, memberPtr, memberLen).toString('latin1')) ? 1 : 0;
278
+ },
279
+
280
+ 'data.membership_add': (
281
+ handle: number,
282
+ setPtr: number,
283
+ setLen: number,
284
+ memberPtr: number,
285
+ memberLen: number,
286
+ _idemPtr: number,
287
+ ): number => {
288
+ const coll = collOf(db, handle);
289
+ if (coll === null) return INVALID_HANDLE;
290
+ if (setLen > MAX_KEY || memberLen > MAX_VALUE) throw new Error('data: set/member too large');
291
+ const sk = storeKey(coll, readCopy(ref, setPtr, setLen));
292
+ const member = readCopy(ref, memberPtr, memberLen);
293
+ let set = MEMBERS.get(sk);
294
+ if (set === undefined) {
295
+ set = new Map();
296
+ MEMBERS.set(sk, set);
297
+ }
298
+ set.set(member.toString('latin1'), member);
299
+ return 0;
300
+ },
301
+
302
+ 'data.membership_remove': (
303
+ handle: number,
304
+ setPtr: number,
305
+ setLen: number,
306
+ memberPtr: number,
307
+ memberLen: number,
308
+ _idemPtr: number,
309
+ ): number => {
310
+ const coll = collOf(db, handle);
311
+ if (coll === null) return INVALID_HANDLE;
312
+ const set = MEMBERS.get(storeKey(coll, readCopy(ref, setPtr, setLen)));
313
+ if (set !== undefined) set.delete(readCopy(ref, memberPtr, memberLen).toString('latin1'));
314
+ return 0;
315
+ },
316
+
317
+ // Frame the members (sorted by bytes, matching the edge BTreeMap) as
318
+ // `u32 count` + per member `u32 len + bytes`; stash + return the length.
319
+ 'data.membership_list': (handle: number, setPtr: number, setLen: number, limit: number): number => {
320
+ const coll = collOf(db, handle);
321
+ if (coll === null) return INVALID_HANDLE;
322
+ const set = MEMBERS.get(storeKey(coll, readCopy(ref, setPtr, setLen)));
323
+ const n = Math.max(0, Math.min(limit, 0xffff));
324
+ const members =
325
+ set === undefined ? [] : Array.from(set.values()).sort(Buffer.compare).slice(0, n);
326
+ const header = Buffer.alloc(4);
327
+ header.writeUInt32LE(members.length, 0);
328
+ const parts: Buffer[] = [header];
329
+ for (const m of members) {
330
+ const h = Buffer.alloc(4);
331
+ h.writeUInt32LE(m.length, 0);
332
+ parts.push(h, m);
333
+ }
334
+ db.lastResult = Buffer.concat(parts);
335
+ return db.lastResult.length;
336
+ },
337
+
338
+ // --- view family (get / publish) ---
339
+
340
+ 'data.view_get': (handle: number, keyPtr: number, keyLen: number): number => {
341
+ const coll = collOf(db, handle);
342
+ if (coll === null) return INVALID_HANDLE;
343
+ const v = VIEWS.get(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
344
+ if (v === undefined) return ABSENT;
345
+ db.lastResult = v;
346
+ return v.length;
347
+ },
348
+
349
+ // Publish overwrites (the host assigns the version; dev keeps the latest).
350
+ 'data.view_publish': (
351
+ handle: number,
352
+ keyPtr: number,
353
+ keyLen: number,
354
+ valPtr: number,
355
+ valLen: number,
356
+ _idemPtr: number,
357
+ ): number => {
358
+ const coll = collOf(db, handle);
359
+ if (coll === null) return INVALID_HANDLE;
360
+ if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/view too large');
361
+ VIEWS.set(storeKey(coll, readCopy(ref, keyPtr, keyLen)), readCopy(ref, valPtr, valLen));
362
+ return 0;
363
+ },
364
+
365
+ // --- counter family (get / add) ---
366
+
367
+ // Stash the i64 sum as 8 LE bytes; the guest pulls + loads it. A counter
368
+ // with no deltas reads as 0 (never absent).
369
+ 'data.counter_get': (handle: number, keyPtr: number, keyLen: number): number => {
370
+ const coll = collOf(db, handle);
371
+ if (coll === null) return INVALID_HANDLE;
372
+ const sum = COUNTERS.get(storeKey(coll, readCopy(ref, keyPtr, keyLen))) ?? 0n;
373
+ const out = Buffer.alloc(8);
374
+ out.writeBigInt64LE(sum);
375
+ db.lastResult = out;
376
+ return out.length;
377
+ },
378
+
379
+ // `delta` is the wasm i64 (a BigInt across the boundary); `BigInt()`
380
+ // normalizes the test's plain-number form too. Saturates like the edge.
381
+ 'data.counter_add': (
382
+ handle: number,
383
+ keyPtr: number,
384
+ keyLen: number,
385
+ delta: number | bigint,
386
+ _idemPtr: number,
387
+ ): number => {
388
+ const coll = collOf(db, handle);
389
+ if (coll === null) return INVALID_HANDLE;
390
+ const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
391
+ COUNTERS.set(sk, satI64((COUNTERS.get(sk) ?? 0n) + BigInt(delta)));
392
+ return 0;
393
+ },
394
+
395
+ // --- events family (append / latest) ---
396
+
397
+ 'data.append': (
398
+ handle: number,
399
+ keyPtr: number,
400
+ keyLen: number,
401
+ evPtr: number,
402
+ evLen: number,
403
+ _idemPtr: number,
404
+ ): number => {
405
+ const coll = collOf(db, handle);
406
+ if (coll === null) return INVALID_HANDLE;
407
+ if (keyLen > MAX_KEY || evLen > MAX_VALUE) throw new Error('data: key/event too large');
408
+ const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
409
+ const log = EVENTS.get(sk);
410
+ const ev = readCopy(ref, evPtr, evLen);
411
+ if (log === undefined) EVENTS.set(sk, [ev]);
412
+ else log.push(ev);
413
+ return 0;
414
+ },
415
+
416
+ // Frame the newest-`limit` events as `u32 count` then per event a
417
+ // length-prefixed blob (`u32 len + bytes`), newest first; stash + return
418
+ // the blob length. Matches the edge `op_latest` / `toildb::Writer` framing.
419
+ 'data.latest': (handle: number, keyPtr: number, keyLen: number, limit: number): number => {
420
+ const coll = collOf(db, handle);
421
+ if (coll === null) return INVALID_HANDLE;
422
+ const log = EVENTS.get(storeKey(coll, readCopy(ref, keyPtr, keyLen))) ?? [];
423
+ const n = Math.max(0, Math.min(limit, 0xffff));
424
+ const newest = log.slice(Math.max(0, log.length - n)).reverse();
425
+ let size = 4;
426
+ for (const ev of newest) size += 4 + ev.length;
427
+ const out = Buffer.alloc(size);
428
+ let off = out.writeUInt32LE(newest.length, 0);
429
+ for (const ev of newest) {
430
+ off = out.writeUInt32LE(ev.length, off);
431
+ off += ev.copy(out, off);
432
+ }
433
+ db.lastResult = out;
434
+ return out.length;
435
+ },
436
+
437
+ // Drain the last stashed variable-length result into the caller buffer.
438
+ 'data.take_result': (outPtr: number, outCap: number): number => {
439
+ const v = db.lastResult;
440
+ if (v === null) return 0;
441
+ if (v.length > outCap) return TOO_SMALL; // keep the stash for retry
442
+ const m = mem(ref);
443
+ if (outPtr < 0 || outPtr + v.length > m.length)
444
+ throw new Error('data: take_result out of bounds');
445
+ v.copy(m, outPtr);
446
+ db.lastResult = null;
447
+ return v.length;
448
+ },
449
+ };
450
+ }
451
+
452
+ /** Test-only: clear the stores between unit tests. */
453
+ export function __resetDbForTests(): void {
454
+ STORE.clear();
455
+ VIEWS.clear();
456
+ MEMBERS.clear();
457
+ COUNTERS.clear();
458
+ EVENTS.clear();
459
+ }
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import { buildCryptoImports, freshCryptoState, type CryptoState } from './crypto.js';
21
- import { buildKvImports } from './kv.js';
21
+ import { buildDatabaseImports, freshDbState, type DbDevState } 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
 
@@ -270,10 +273,11 @@ export function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssem
270
273
  // `crypto`. The dev server skips metering, so these charge nothing.
271
274
  ...buildCryptoImports(ref, state.crypto),
272
275
 
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),
276
+ // `env::data.*`: the ToilDB data API, emulated in process (see
277
+ // ./database.ts). Backs the auth example's accounts + login
278
+ // challenges so register/login spans requests under `toiljs dev`;
279
+ // the production edge backs the SAME imports with ScyllaDB.
280
+ ...buildDatabaseImports(ref, state.db),
277
281
  },
278
282
  };
279
283
  }
@@ -59,9 +59,15 @@ const PROVIDED_IMPORTS = new Set([
59
59
  'crypto.import_key', 'crypto.export_key', 'crypto.encrypt', 'crypto.decrypt',
60
60
  'crypto.sign', 'crypto.verify', 'crypto.derive_bits',
61
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',
62
+ // ToilDB data API (see ./database.ts). Backed by ScyllaDB on the production
63
+ // edge; backs the auth example's accounts + login challenges in dev.
64
+ 'data.resolve_collection', 'data.get', 'data.get_many', 'data.exists', 'data.create',
65
+ 'data.patch', 'data.delete', 'data.get_delete',
66
+ 'data.unique_lookup', 'data.unique_claim', 'data.unique_release',
67
+ 'data.view_get', 'data.view_publish',
68
+ 'data.membership_contains', 'data.membership_add', 'data.membership_remove', 'data.membership_list',
69
+ 'data.counter_get', 'data.counter_add', 'data.append', 'data.latest',
70
+ 'data.take_result',
65
71
  ]);
66
72
 
67
73
  export class WasmServerModule {