@raisindb/function-assemblyscript 0.1.0 → 0.1.1

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 (2) hide show
  1. package/package.json +4 -2
  2. package/testing/host.mjs +118 -0
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@raisindb/function-assemblyscript",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Guest SDK for writing RaisinDB server functions in AssemblyScript, compiled to a WebAssembly component",
5
5
  "license": "BSL-1.1",
6
6
  "types": "assembly/index.ts",
7
7
  "ascMain": "assembly/index.ts",
8
8
  "files": [
9
9
  "assembly",
10
+ "testing",
10
11
  "wit",
11
12
  "README.md"
12
13
  ],
@@ -22,6 +23,7 @@
22
23
  "types": "./assembly/index.ts",
23
24
  "default": "./assembly/index.ts"
24
25
  },
25
- "./assembly/*": "./assembly/*"
26
+ "./assembly/*": "./assembly/*",
27
+ "./testing": "./testing/host.mjs"
26
28
  }
27
29
  }
@@ -0,0 +1,118 @@
1
+ // A mock host for AssemblyScript guests, so a function can be unit-tested with
2
+ // no server and no network — the same guarantee the Rust and Go SDKs give.
3
+ //
4
+ // It loads the CORE module (before `wasm-tools` wraps it as a component) and
5
+ // supplies `raisin:function/host` from JavaScript. That means this file speaks
6
+ // the Component Model's canonical ABI from the other side: it writes string
7
+ // arguments into guest memory through the guest's own `cabi_realloc`, and
8
+ // reads results back out of the return area.
9
+ //
10
+ // Layout it depends on, matching `assembly/abi.ts`:
11
+ // result<string, string> -> { u8 tag, 3 bytes padding, i32 ptr, i32 len }
12
+ // string -> { i32 ptr, i32 len }
13
+ // The tag is a u8 because the canonical ABI stores a variant discriminant in
14
+ // the smallest integer that fits its case count.
15
+
16
+ import { readFile } from 'node:fs/promises';
17
+
18
+ const HOST = 'raisin:function/host@0.1.0';
19
+ const LEVELS = ['debug', 'info', 'warn', 'error'];
20
+
21
+ /**
22
+ * Instantiate a guest with a scripted host.
23
+ *
24
+ * @param {string} corePath the `.wasm` produced by `asc` (not the component)
25
+ * @param {object} options
26
+ * @param {(method: string, argsJson: string) => unknown} [options.call]
27
+ * Answer a `raisin.*` call. Return a value to send the Ok arm; throw to
28
+ * send Err. The default throws, so a call the test did not plan for
29
+ * fails loudly instead of returning something plausible.
30
+ * @param {object} [options.context] what `context()` returns.
31
+ */
32
+ export async function loadGuest(corePath, options = {}) {
33
+ const bytes = await readFile(corePath);
34
+ const logs = [];
35
+ const calls = [];
36
+ let exports;
37
+
38
+ const encoder = new TextEncoder();
39
+ const decoder = new TextDecoder();
40
+
41
+ const mem = () => new Uint8Array(exports.memory.buffer);
42
+ const view = () => new DataView(exports.memory.buffer);
43
+
44
+ const readString = (ptr, len) => decoder.decode(mem().subarray(ptr, ptr + len));
45
+
46
+ /** Copy a string into guest memory using its own allocator. */
47
+ const writeString = (s) => {
48
+ const buf = encoder.encode(s);
49
+ const ptr = exports.cabi_realloc(0, 0, 1, buf.length);
50
+ mem().set(buf, ptr);
51
+ return [ptr, buf.length];
52
+ };
53
+
54
+ /** Write `{ptr, len}` into a return area the guest gave us. */
55
+ const writeStringAt = (area, s) => {
56
+ const [ptr, len] = writeString(s);
57
+ view().setInt32(area, ptr, true);
58
+ view().setInt32(area + 4, len, true);
59
+ };
60
+
61
+ const call = options.call ?? ((method) => {
62
+ throw new Error(
63
+ `unexpected host call ${method}. Script it with { call: (method, args) => ... }`
64
+ );
65
+ });
66
+
67
+ const imports = {
68
+ [HOST]: {
69
+ call(mp, ml, ap, al, ret) {
70
+ const method = readString(mp, ml);
71
+ const args = readString(ap, al);
72
+ calls.push({ method, args: JSON.parse(args) });
73
+ const v = view();
74
+ try {
75
+ const result = call(method, args);
76
+ const body = typeof result === 'string' ? result : JSON.stringify(result ?? null);
77
+ v.setUint8(ret, 0); // ok
78
+ const [ptr, len] = writeString(body);
79
+ v.setInt32(ret + 4, ptr, true);
80
+ v.setInt32(ret + 8, len, true);
81
+ } catch (error) {
82
+ v.setUint8(ret, 1); // err
83
+ const [ptr, len] = writeString(String(error?.message ?? error));
84
+ v.setInt32(ret + 4, ptr, true);
85
+ v.setInt32(ret + 8, len, true);
86
+ }
87
+ },
88
+ log(level, mp, ml) {
89
+ logs.push({ level: LEVELS[level] ?? String(level), message: readString(mp, ml) });
90
+ },
91
+ context(ret) {
92
+ writeStringAt(ret, JSON.stringify(options.context ?? { tenant_id: 'test' }));
93
+ },
94
+ 'abi-version': (ret) => writeStringAt(ret, '0.1.0'),
95
+ },
96
+ };
97
+
98
+ const { instance } = await WebAssembly.instantiate(bytes, imports);
99
+ exports = instance.exports;
100
+
101
+ return {
102
+ /** Call a handler by name. Returns the parsed JSON it produced. */
103
+ invoke(name, input) {
104
+ const [np, nl] = writeString(name);
105
+ const [ip, il] = writeString(typeof input === 'string' ? input : JSON.stringify(input));
106
+ const ret = exports.handler(np, nl, ip, il);
107
+ const v = view();
108
+ const tag = v.getUint8(ret); // u8 — see the header
109
+ const body = readString(v.getInt32(ret + 4, true), v.getInt32(ret + 8, true));
110
+ if (tag !== 0) throw new Error(body);
111
+ return JSON.parse(body);
112
+ },
113
+ /** Every log line the guest emitted, as `{ level, message }`. */
114
+ logs,
115
+ /** Every host call the guest made, as `{ method, args }`. */
116
+ calls,
117
+ };
118
+ }