space-data-module-sdk 0.5.19 → 0.5.21

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,32 @@
1
+ # AGENTS
2
+
3
+ Apply the root and `src/AGENTS.md` files first.
4
+
5
+ ## Area Ownership
6
+
7
+ This directory owns the canonical runtime-host storage model: row handles,
8
+ region handles, FlatSQL-backed storage, and binary FlatBuffer ingest on the host
9
+ side.
10
+
11
+ ## Storage And Streaming Rules
12
+
13
+ - Keep durable row identity host-owned as `(schemaFileId, rowId)`.
14
+ - Keep runtime aligned-binary identity host-owned as `(regionId, recordIndex)`.
15
+ - The canonical ingest path is binary FlatBuffer bytes, not JSON.
16
+ - Use size-prefixed FlatBuffer frames for streaming transport.
17
+ - Do not coerce row payloads through JSON serialization.
18
+ - If the host owns persistence, use runtime-host ingest helpers.
19
+ - If a resident module owns state, keep the stream binary and push into the
20
+ module through the harness/pump path rather than inventing JSON wrappers.
21
+
22
+ ## Key Files
23
+
24
+ - `flatbufferStreamIngestor.js`
25
+ - `flatsqlRuntimeStore.js`
26
+ - `index.js`
27
+
28
+ ## Verification
29
+
30
+ - `npm run test:stream-ingest`
31
+ - `node --test test/flatsql-local-node.test.js`
32
+ - `npm run benchmark:stream-1gib` for large-stream changes
@@ -0,0 +1,181 @@
1
+ import { createFlatSqlRuntimeStore } from "./flatsqlRuntimeStore.js";
2
+
3
+ function toUint8Array(data) {
4
+ if (data instanceof Uint8Array) {
5
+ return data;
6
+ }
7
+ if (data instanceof ArrayBuffer) {
8
+ return new Uint8Array(data);
9
+ }
10
+ if (ArrayBuffer.isView(data)) {
11
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
12
+ }
13
+ throw new TypeError(
14
+ "FlatBuffer stream ingestor expects Uint8Array, ArrayBuffer, or ArrayBufferView chunks.",
15
+ );
16
+ }
17
+
18
+ function concatUint8Arrays(chunks) {
19
+ let totalLength = 0;
20
+ for (let index = 0; index < chunks.length; index += 1) {
21
+ totalLength += chunks[index].byteLength;
22
+ }
23
+
24
+ const combined = new Uint8Array(totalLength);
25
+ let offset = 0;
26
+ for (let index = 0; index < chunks.length; index += 1) {
27
+ combined.set(chunks[index], offset);
28
+ offset += chunks[index].byteLength;
29
+ }
30
+ return combined;
31
+ }
32
+
33
+ function readFrameSize(bytes, offset) {
34
+ return (
35
+ bytes[offset] |
36
+ (bytes[offset + 1] << 8) |
37
+ (bytes[offset + 2] << 16) |
38
+ ((bytes[offset + 3] << 24) >>> 0)
39
+ );
40
+ }
41
+
42
+ function readFileIdentifier(payload) {
43
+ if (!(payload instanceof Uint8Array) || payload.byteLength < 8) {
44
+ return null;
45
+ }
46
+ return String.fromCharCode(payload[4], payload[5], payload[6], payload[7]);
47
+ }
48
+
49
+ function normalizeSchemaFileId(fileIdentifier) {
50
+ if (typeof fileIdentifier !== "string") {
51
+ return null;
52
+ }
53
+ const normalized = fileIdentifier.trim();
54
+ return normalized.length > 0 ? normalized : null;
55
+ }
56
+
57
+ function resolveRouteHandler(frameRouter, rawFileIdentifier, schemaFileId) {
58
+ if (typeof frameRouter === "function") {
59
+ return frameRouter;
60
+ }
61
+ if (!frameRouter || typeof frameRouter !== "object") {
62
+ return null;
63
+ }
64
+ if (typeof frameRouter[schemaFileId] === "function") {
65
+ return frameRouter[schemaFileId];
66
+ }
67
+ if (typeof frameRouter[rawFileIdentifier] === "function") {
68
+ return frameRouter[rawFileIdentifier];
69
+ }
70
+ return null;
71
+ }
72
+
73
+ export function createFlatBufferStreamIngestor(options = {}) {
74
+ const rows = options.rows ?? createFlatSqlRuntimeStore();
75
+ const frameRouter = options.frameRouter ?? null;
76
+ const appendFrame =
77
+ typeof options.appendFrame === "function" ? options.appendFrame : null;
78
+
79
+ const stats = {
80
+ bytesReceived: 0,
81
+ chunksReceived: 0,
82
+ framesDecoded: 0,
83
+ framesAppended: 0,
84
+ framesRouted: 0,
85
+ parseErrors: 0,
86
+ };
87
+
88
+ let pending = new Uint8Array(0);
89
+
90
+ function appendDecodedFrame(payload, context) {
91
+ if (appendFrame) {
92
+ appendFrame(payload, context);
93
+ return;
94
+ }
95
+ rows.appendRow({
96
+ schemaFileId: context.schemaFileId,
97
+ payload,
98
+ });
99
+ }
100
+
101
+ function pushBytes(data) {
102
+ const bytes = toUint8Array(data);
103
+ stats.bytesReceived += bytes.byteLength;
104
+ stats.chunksReceived += 1;
105
+
106
+ const combined =
107
+ pending.byteLength > 0 ? concatUint8Arrays([pending, bytes]) : bytes;
108
+
109
+ let offset = 0;
110
+ let appendedCount = 0;
111
+
112
+ while (offset + 4 <= combined.byteLength) {
113
+ const frameSize = readFrameSize(combined, offset);
114
+ if (frameSize <= 0) {
115
+ stats.parseErrors += 1;
116
+ throw new Error("Invalid FlatBuffer stream frame size.");
117
+ }
118
+ if (offset + 4 + frameSize > combined.byteLength) {
119
+ break;
120
+ }
121
+
122
+ const payload = combined.subarray(offset + 4, offset + 4 + frameSize);
123
+ const rawFileIdentifier = readFileIdentifier(payload);
124
+ const schemaFileId = normalizeSchemaFileId(rawFileIdentifier);
125
+ if (!schemaFileId) {
126
+ stats.parseErrors += 1;
127
+ throw new Error(
128
+ "FlatBuffer stream frame is missing a readable file identifier.",
129
+ );
130
+ }
131
+
132
+ const context = {
133
+ rawFileIdentifier,
134
+ schemaFileId,
135
+ rows,
136
+ stats,
137
+ };
138
+ const routeHandler = resolveRouteHandler(
139
+ frameRouter,
140
+ rawFileIdentifier,
141
+ schemaFileId,
142
+ );
143
+
144
+ stats.framesDecoded += 1;
145
+ if (routeHandler) {
146
+ const routeResult = routeHandler(payload, context);
147
+ if (routeResult !== false) {
148
+ stats.framesRouted += 1;
149
+ offset += 4 + frameSize;
150
+ continue;
151
+ }
152
+ }
153
+
154
+ appendDecodedFrame(payload, context);
155
+ stats.framesAppended += 1;
156
+ appendedCount += 1;
157
+ offset += 4 + frameSize;
158
+ }
159
+
160
+ pending =
161
+ offset < combined.byteLength
162
+ ? combined.slice(offset)
163
+ : new Uint8Array(0);
164
+
165
+ return appendedCount;
166
+ }
167
+
168
+ function finish() {
169
+ if (pending.byteLength > 0) {
170
+ throw new Error("FlatBuffer stream ended with a partial frame.");
171
+ }
172
+ return 0;
173
+ }
174
+
175
+ return {
176
+ rows,
177
+ stats,
178
+ pushBytes,
179
+ finish,
180
+ };
181
+ }
@@ -5,7 +5,6 @@ const RUNTIME_ROW_SCHEMA = `
5
5
  table RuntimeHostRow {
6
6
  schemaFileId: string;
7
7
  rowId: ulong;
8
- payloadJson: string;
9
8
  }
10
9
  `;
11
10
 
@@ -19,14 +18,85 @@ function clonePayload(payload) {
19
18
  if (typeof structuredClone === "function") {
20
19
  return structuredClone(payload);
21
20
  }
21
+ return clonePayloadFallback(payload, new WeakMap());
22
+ }
23
+
24
+ function clonePayloadFallback(payload, seen) {
25
+ if (payload instanceof Date) {
26
+ return new Date(payload.getTime());
27
+ }
28
+ if (payload instanceof RegExp) {
29
+ return new RegExp(payload.source, payload.flags);
30
+ }
31
+ if (payload instanceof Map) {
32
+ if (seen.has(payload)) {
33
+ return seen.get(payload);
34
+ }
35
+ const cloned = new Map();
36
+ seen.set(payload, cloned);
37
+ for (const [key, value] of payload.entries()) {
38
+ cloned.set(clonePayloadFallback(key, seen), clonePayloadFallback(value, seen));
39
+ }
40
+ return cloned;
41
+ }
42
+ if (payload instanceof Set) {
43
+ if (seen.has(payload)) {
44
+ return seen.get(payload);
45
+ }
46
+ const cloned = new Set();
47
+ seen.set(payload, cloned);
48
+ for (const value of payload.values()) {
49
+ cloned.add(clonePayloadFallback(value, seen));
50
+ }
51
+ return cloned;
52
+ }
22
53
  if (ArrayBuffer.isView(payload)) {
23
- return payload.slice(0);
54
+ if (payload instanceof DataView) {
55
+ return new DataView(
56
+ payload.buffer.slice(
57
+ payload.byteOffset,
58
+ payload.byteOffset + payload.byteLength,
59
+ ),
60
+ );
61
+ }
62
+ const clonedBuffer = payload.buffer.slice(
63
+ payload.byteOffset,
64
+ payload.byteOffset + payload.byteLength,
65
+ );
66
+ return new payload.constructor(clonedBuffer, 0, payload.length);
24
67
  }
25
68
  if (payload instanceof ArrayBuffer) {
26
69
  return payload.slice(0);
27
70
  }
71
+ if (Array.isArray(payload)) {
72
+ if (seen.has(payload)) {
73
+ return seen.get(payload);
74
+ }
75
+ const cloned = [];
76
+ seen.set(payload, cloned);
77
+ for (const value of payload) {
78
+ cloned.push(clonePayloadFallback(value, seen));
79
+ }
80
+ return cloned;
81
+ }
28
82
  if (typeof payload === "object") {
29
- return JSON.parse(JSON.stringify(payload));
83
+ if (seen.has(payload)) {
84
+ return seen.get(payload);
85
+ }
86
+ const prototype = Object.getPrototypeOf(payload);
87
+ const cloned = Object.create(prototype ?? Object.prototype);
88
+ seen.set(payload, cloned);
89
+ for (const key of Reflect.ownKeys(payload)) {
90
+ const descriptor = Object.getOwnPropertyDescriptor(payload, key);
91
+ if (!descriptor) {
92
+ continue;
93
+ }
94
+ if ("value" in descriptor) {
95
+ descriptor.value = clonePayloadFallback(descriptor.value, seen);
96
+ }
97
+ Object.defineProperty(cloned, key, descriptor);
98
+ }
99
+ return cloned;
30
100
  }
31
101
  return payload;
32
102
  }
@@ -54,48 +124,46 @@ function escapeSqlStringLiteral(value) {
54
124
  return String(value).replaceAll("'", "''");
55
125
  }
56
126
 
57
- function serializePayload(payload) {
58
- if (payload === null || payload === undefined) {
59
- return {
60
- kind: "json",
61
- value: null,
62
- };
63
- }
64
- if (ArrayBuffer.isView(payload)) {
65
- return {
66
- kind: "bytes",
67
- bytes: Array.from(
68
- new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength),
69
- ),
70
- };
127
+ function encodeRuntimeRowMetadata({ schemaFileId, rowId }) {
128
+ const schemaBytes = encoder.encode(normalizeSchemaFileId(schemaFileId));
129
+ const encoded = new Uint8Array(4 + schemaBytes.byteLength + 8);
130
+ const view = new DataView(encoded.buffer);
131
+ view.setUint32(0, schemaBytes.byteLength, true);
132
+ encoded.set(schemaBytes, 4);
133
+ view.setBigUint64(4 + schemaBytes.byteLength, BigInt(rowId), true);
134
+ return encoded;
135
+ }
136
+
137
+ function decodeRuntimeRowMetadata(data) {
138
+ const bytes =
139
+ data instanceof Uint8Array
140
+ ? data
141
+ : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
142
+ if (bytes.byteLength < 12) {
143
+ throw new Error("Invalid runtime row metadata buffer.");
71
144
  }
72
- if (payload instanceof ArrayBuffer) {
73
- return {
74
- kind: "bytes",
75
- bytes: Array.from(new Uint8Array(payload)),
76
- };
145
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
146
+ const schemaLength = view.getUint32(0, true);
147
+ const schemaStart = 4;
148
+ const schemaEnd = schemaStart + schemaLength;
149
+ const rowIdOffset = schemaEnd;
150
+ if (schemaEnd + 8 > bytes.byteLength) {
151
+ throw new Error("Invalid runtime row metadata layout.");
77
152
  }
78
153
  return {
79
- kind: "json",
80
- value: clonePayload(payload),
154
+ schemaFileId: decoder.decode(bytes.subarray(schemaStart, schemaEnd)),
155
+ rowId: Number(view.getBigUint64(rowIdOffset, true)),
81
156
  };
82
157
  }
83
158
 
84
- function deserializePayload(payloadJson) {
85
- const payload = JSON.parse(String(payloadJson ?? "null"));
86
- if (payload?.kind === "bytes") {
87
- return Uint8Array.from(payload.bytes ?? []);
88
- }
89
- if (payload?.kind === "json") {
90
- return clonePayload(payload.value);
91
- }
92
- return clonePayload(payload);
159
+ function buildRowKey(schemaFileId, rowId) {
160
+ return `${normalizeSchemaFileId(schemaFileId)}:${Number(rowId)}`;
93
161
  }
94
162
 
95
163
  function createRuntimeRowAccessor() {
96
164
  const accessor = new DirectAccessor();
97
165
  accessor.registerAccessor(RUNTIME_ROW_TABLE, (data, path) => {
98
- const row = JSON.parse(decoder.decode(data));
166
+ const row = decodeRuntimeRowMetadata(data);
99
167
  let current = row;
100
168
  for (const segment of Array.isArray(path) ? path : []) {
101
169
  if (current === null || current === undefined) {
@@ -106,13 +174,10 @@ function createRuntimeRowAccessor() {
106
174
  return current ?? null;
107
175
  });
108
176
  accessor.registerBuilder(RUNTIME_ROW_TABLE, (fields) =>
109
- encoder.encode(
110
- JSON.stringify({
111
- schemaFileId: normalizeSchemaFileId(fields.schemaFileId),
112
- rowId: Number(fields.rowId),
113
- payloadJson: String(fields.payloadJson ?? "null"),
114
- }),
115
- ),
177
+ encodeRuntimeRowMetadata({
178
+ schemaFileId: fields.schemaFileId,
179
+ rowId: Number(fields.rowId),
180
+ }),
116
181
  );
117
182
  return accessor;
118
183
  }
@@ -127,18 +192,21 @@ function cloneQueryResult(result) {
127
192
  };
128
193
  }
129
194
 
130
- function rowViewFromQueryRow(row) {
195
+ function rowViewFromQueryRow(row, payloadStore) {
196
+ const schemaFileId = String(row[0]);
197
+ const rowId = Number(row[1]);
131
198
  return {
132
199
  handle: {
133
- schemaFileId: String(row[0]),
134
- rowId: Number(row[1]),
200
+ schemaFileId,
201
+ rowId,
135
202
  },
136
- payload: deserializePayload(row[2]),
203
+ payload: clonePayload(payloadStore.get(buildRowKey(schemaFileId, rowId))),
137
204
  };
138
205
  }
139
206
 
140
207
  export function createFlatSqlRuntimeStore(options = {}) {
141
208
  const accessor = options.accessor ?? createRuntimeRowAccessor();
209
+ const payloadStore = options.payloadStore ?? new Map();
142
210
  const database =
143
211
  options.database ??
144
212
  FlatSQLDatabase.fromSchema(
@@ -168,8 +236,11 @@ export function createFlatSqlRuntimeStore(options = {}) {
168
236
  database.insert(RUNTIME_ROW_TABLE, {
169
237
  schemaFileId: normalizedSchemaFileId,
170
238
  rowId: nextRowId,
171
- payloadJson: JSON.stringify(serializePayload(payload)),
172
239
  });
240
+ payloadStore.set(
241
+ buildRowKey(normalizedSchemaFileId, nextRowId),
242
+ clonePayload(payload),
243
+ );
173
244
  return {
174
245
  schemaFileId: normalizedSchemaFileId,
175
246
  rowId: nextRowId,
@@ -193,12 +264,12 @@ export function createFlatSqlRuntimeStore(options = {}) {
193
264
  const result =
194
265
  normalizedSchemaFileId === null
195
266
  ? database.query(
196
- `SELECT schemaFileId, rowId, payloadJson FROM ${RUNTIME_ROW_TABLE} ORDER BY schemaFileId, rowId`,
267
+ `SELECT schemaFileId, rowId FROM ${RUNTIME_ROW_TABLE} ORDER BY schemaFileId, rowId`,
197
268
  )
198
269
  : database.query(
199
- `SELECT schemaFileId, rowId, payloadJson FROM ${RUNTIME_ROW_TABLE} WHERE schemaFileId = '${escapeSqlStringLiteral(normalizedSchemaFileId)}' ORDER BY rowId`,
270
+ `SELECT schemaFileId, rowId FROM ${RUNTIME_ROW_TABLE} WHERE schemaFileId = '${escapeSqlStringLiteral(normalizedSchemaFileId)}' ORDER BY rowId`,
200
271
  );
201
- return (result.rows ?? []).map(rowViewFromQueryRow);
272
+ return (result.rows ?? []).map((row) => rowViewFromQueryRow(row, payloadStore));
202
273
  }
203
274
 
204
275
  function query(sql) {
@@ -1,4 +1,5 @@
1
1
  import { createFlatSqlRuntimeStore } from "./flatsqlRuntimeStore.js";
2
+ import { createFlatBufferStreamIngestor } from "./flatbufferStreamIngestor.js";
2
3
  import { createRuntimeRegionStore } from "./runtimeRegionStore.js";
3
4
  import { createModuleRegistry } from "./moduleRegistry.js";
4
5
 
@@ -15,6 +16,7 @@ export function createRuntimeHost(options = {}) {
15
16
  }
16
17
 
17
18
  export {
19
+ createFlatBufferStreamIngestor,
18
20
  createFlatSqlRuntimeStore,
19
21
  createModuleRegistry,
20
22
  createRuntimeRegionStore,
@@ -0,0 +1,33 @@
1
+ # AGENTS
2
+
3
+ Apply the root and `src/AGENTS.md` files first.
4
+
5
+ ## Area Ownership
6
+
7
+ This directory owns SDK test harnesses and runtime-facing helper surfaces used
8
+ by examples and downstream consumers: browser harnesses, generic process invoke
9
+ clients, runtime-matrix helpers, and resident-module FlatBuffer pumps.
10
+
11
+ ## Harness Rules
12
+
13
+ - Keep browser harness behavior aligned with the same standalone artifacts that
14
+ WasmEdge runs.
15
+ - Prefer portable invoke envelopes and portable WASI behavior over runtime-
16
+ specific shortcuts.
17
+ - `createModuleFlatBufferStreamPump(...)` is the canonical no-JSON path for
18
+ feeding binary FlatBuffer streams into a resident module instance.
19
+ - Avoid hiding stateful behavior inside one-off demos; if a harness contract is
20
+ real, test it here.
21
+
22
+ ## Key Files
23
+
24
+ - `browserModuleHarness.js`
25
+ - `moduleFlatbufferStreamPump.js`
26
+ - `processInvoke.js`
27
+
28
+ ## Verification
29
+
30
+ - `node --test test/browser-harness.test.js`
31
+ - `node --test test/isomorphic-loader.test.js`
32
+ - `npm run test:module-stream`
33
+ - `node --test test/process-invoke.test.js`
@@ -26,6 +26,7 @@ export {
26
26
  createModuleHarness,
27
27
  resolveModuleHarnessLaunchPlan,
28
28
  } from "./moduleHarness.js";
29
+ export { createModuleFlatBufferStreamPump } from "./moduleFlatbufferStreamPump.js";
29
30
 
30
31
  const CapabilitySurfaceMatrix = Object.freeze({
31
32
  logging: Object.freeze({