@simpleplatform/sdk 1.2.0 → 2.0.0

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/README.md CHANGED
@@ -40,6 +40,96 @@ The TypeScript SDK is organized into focused modules for different capabilities:
40
40
  | **Security** | `@simpleplatform/sdk/security` | Security policy authoring |
41
41
  | **Settings** | `@simpleplatform/sdk/settings` | Application settings retrieval |
42
42
  | **Storage** | `@simpleplatform/sdk/storage` | File upload and management |
43
+ | **Space** | `@simpleplatform/sdk/space` | Behavior-aware record workflows in a Space |
44
+
45
+ ## Embedded Spaces
46
+
47
+ Use the explicit Space subpaths inside an embedded browser Space. The package
48
+ root remains the Action/WASM API, so Action code never imports browser globals
49
+ by accident.
50
+
51
+ ```typescript
52
+ import { connectSpace } from '@simpleplatform/sdk/space'
53
+
54
+ const hostOrigin = new URL(document.referrer).origin
55
+ const simple = await connectSpace({ targetOrigin: hostOrigin })
56
+ const record = await simple.records.current()
57
+
58
+ await record.update({ first_name: 'Ada' }) // Stages values and runs update Behavior.
59
+ const result = await record.submit() // Runs submit Behavior, then persists on success.
60
+
61
+ if (!result.ok) {
62
+ const { errors, fields } = record.snapshot()
63
+ // Render every field's error/info plus form-level errors.
64
+ }
65
+ ```
66
+
67
+ Connect once when the Space starts and reuse the returned client. One embedded iframe has one host MessagePort handshake.
68
+
69
+ `records.current()` returns the platform-owned record for the current record
70
+ page. Its handle exposes immutable snapshots, `update(values)`, and `submit()`.
71
+ The host enforces permissions and runs Record Behaviors; the Space only renders
72
+ the returned state.
73
+
74
+ ### Space context
75
+
76
+ `simple.context` is explicit host-provided page context. It is never inferred
77
+ from a Space URL or the iframe DOM.
78
+
79
+ ```ts
80
+ switch (simple.context.kind) {
81
+ case 'standalone':
82
+ break
83
+ case 'record':
84
+ console.log(
85
+ simple.context.applicationId,
86
+ simple.context.tableName,
87
+ simple.context.recordId,
88
+ )
89
+ break
90
+ }
91
+ ```
92
+
93
+ The two exact context forms are `{ kind: 'standalone' }` and
94
+ `{ kind: 'record', applicationId, tableName, recordId }`. There is no
95
+ `unknown` context variant. A missing or malformed context rejects
96
+ `connectSpace()` with `SpaceProtocolError` code `invalid_response`.
97
+
98
+ `connectSpace()` works in any embedded Space, including standalone dashboards
99
+ and tools. In a non-record Space, `simple.data` remains available while
100
+ `simple.records.current()` rejects with `SpaceProtocolError` code `unavailable`
101
+ and explains that the Space must be configured as a record view.
102
+
103
+ ### Space data access
104
+
105
+ Use `simple.data` for application data that is not the record form currently
106
+ being edited. It uses the Space's existing, host-authorized GraphQL bridge.
107
+
108
+ ```typescript
109
+ const users = await simple.data.query<{ users: Array<{ id: string, email: string }> }>(
110
+ `query ListUsers($limit: Int!) {
111
+ users: dev_simple_system__users(limit: $limit) {
112
+ id
113
+ email
114
+ }
115
+ }`,
116
+ { limit: 10 },
117
+ )
118
+
119
+ const result = await simple.data.mutate<{ insert_demo__note: { id: string } }>(
120
+ `mutation CreateNote($body: String!) {
121
+ insert_demo__note(object: { body: $body }) {
122
+ id
123
+ }
124
+ }`,
125
+ { body: 'Follow up with the customer.' },
126
+ )
127
+ ```
128
+
129
+ Use `record.update()` and `record.submit()` for writes to the current record.
130
+ Those commands preserve Record Behaviors, validation, documents, and the shared
131
+ record state used by the platform header. `simple.data.mutate()` is for other
132
+ authorized application data; it must not be used to bypass a record workflow.
43
133
 
44
134
  ---
45
135
 
package/dist/ai.js CHANGED
@@ -266,21 +266,6 @@ export async function transcribe(input, options, context) {
266
266
  // ============================================================================
267
267
  // Face Recognition API
268
268
  // ============================================================================
269
- let _collectionEnsured = false;
270
- /**
271
- * Internal helper to ensure the face collection exists before making face operations.
272
- * Caches the result per Javascript isolate execution to minimize RPC overhead over
273
- * sequential calls in the same logic.
274
- */
275
- async function _ensureFaceCollection(context) {
276
- if (_collectionEnsured)
277
- return;
278
- const response = await hostExecute('action:ai/face/ensure-collection', {}, context);
279
- if (!response.ok) {
280
- throw new Error(response.error?.message || 'Failed to ensure face collection.');
281
- }
282
- _collectionEnsured = true;
283
- }
284
269
  /**
285
270
  * Enrolls a face for a subject. The face is associated with the given `subjectId`
286
271
  * within the tenant's secure collection.
@@ -298,9 +283,7 @@ export async function enrollFace(subjectId, image, context) {
298
283
  if (!image) {
299
284
  throw new Error('The `image` parameter is required for `enrollFace`.');
300
285
  }
301
- // Ensure the tenant's collection is ready before enrolling
302
- await _ensureFaceCollection(context);
303
- // Upload pending documents if given handles
286
+ // Upload pending DocumentHandles to ephemeral storage
304
287
  const processedImage = await _uploadPendingFiles(image, context);
305
288
  const payload = {
306
289
  image: processedImage,
@@ -325,8 +308,6 @@ export async function searchFace(image, options = {}, context) {
325
308
  if (!image) {
326
309
  throw new Error('The `image` parameter is required for `searchFace`.');
327
310
  }
328
- // Ensure collection exists before searching
329
- await _ensureFaceCollection(context);
330
311
  const processedImage = await _uploadPendingFiles(image, context);
331
312
  // Map JS property names to Elixir convention if needed, though they are passed as options
332
313
  // and parsed correctly in simple_logic if expected.
@@ -358,7 +339,6 @@ export async function deleteFace(faceIds, context) {
358
339
  if (!faceIds || !Array.isArray(faceIds) || faceIds.length === 0) {
359
340
  throw new Error('The `faceIds` parameter must be a non-empty array for `deleteFace`.');
360
341
  }
361
- await _ensureFaceCollection(context);
362
342
  const payload = {
363
343
  face_ids: faceIds,
364
344
  };
package/dist/graphql.js CHANGED
@@ -18,8 +18,6 @@ export async function execute(query, variables, context) {
18
18
  if (!response.ok) {
19
19
  // eslint-disable-next-line no-console
20
20
  console.log('[GraphQL] response.error:', JSON.stringify(response.error, null, 2));
21
- // eslint-disable-next-line no-console
22
- console.log('[GraphQL] response.data:', JSON.stringify(response.data, null, 2));
23
21
  throw new Error(response.error?.message ?? 'GraphQL query failed');
24
22
  }
25
23
  return response.data;
package/dist/host.d.ts CHANGED
@@ -1,28 +1,17 @@
1
1
  import type { Context, SimpleResponse } from './types';
2
2
  /**
3
- * Executes an action synchronously on the host system and returns the response.
3
+ * Calls an action on the host and returns its response.
4
4
  *
5
- * NOTE: The memory model for this execution is that the entire WASM instance is
6
- * ephemeral. Memory is allocated for this single execution and then discarded
7
- * when the instance is terminated. Therefore, a manual memory reset is not required.
8
- *
9
- * This function has two internal implementations selected at build time:
10
- * 1. ASYNC_BUILD = true: For browsers, uses Asyncify to pause/resume execution.
11
- * 2. ASYNC_BUILD = false: For Elixir, uses a synchronous call-and-get-result pattern.
5
+ * The runtime owns serialization and memory for this boundary. The SDK passes
6
+ * the action name and parameters as JavaScript values, and the host returns the
7
+ * parsed response as a value.
12
8
  */
13
9
  export declare function execute<T = any>(actionName: string, params: any, context: Context): SimpleResponse<T>;
14
10
  /**
15
- * Executes an action asynchronously on the host system (fire-and-forget).
11
+ * Calls an action on the host without waiting for it to answer.
16
12
  */
17
13
  export declare function executeAsync(actionName: string, params: any, context: Context): void;
18
14
  /**
19
- * Retrieves the initial context payload from the host. This function orchestrates
20
- * the "pull" mechanism required by the execution environment.
21
- *
22
- * It first asks the host for the payload size, then allocates memory within
23
- * the WASM module, and finally asks the host to write the payload into the
24
- * allocated buffer.
25
- *
26
- * @returns A Uint8Array containing the context payload.
15
+ * Returns the execution context the host assembled for this run.
27
16
  */
28
- export declare function getContext(): Uint8Array;
17
+ export declare function getContext(): any;
package/dist/host.js CHANGED
@@ -1,148 +1,40 @@
1
- import { allocate, deallocate, decoder, readBufferSlice, stringToPtr } from './internal/memory';
1
+ const ABI_MISMATCH_MESSAGE = 'Simple SDK 2.0.0 requires the value-based runtime ABI: __host.call and __host.cast accept values, and __host.getContext returns the execution context. Install a runtime plugin released with the matching SDK.';
2
+ function assertRuntimeAbi() {
3
+ const host = globalThis.__host;
4
+ const hasValueBridge = typeof host?.call === 'function'
5
+ && typeof host.cast === 'function'
6
+ && typeof host.getContext === 'function';
7
+ const hasLegacyBridge = typeof host?.getContextSize === 'function'
8
+ || typeof host?.getExecutionResult === 'function'
9
+ || typeof host?.getExecutionResultSize === 'function';
10
+ if (!hasValueBridge || hasLegacyBridge) {
11
+ throw new Error(ABI_MISMATCH_MESSAGE);
12
+ }
13
+ }
2
14
  /**
3
- * Executes an action synchronously on the host system and returns the response.
4
- *
5
- * NOTE: The memory model for this execution is that the entire WASM instance is
6
- * ephemeral. Memory is allocated for this single execution and then discarded
7
- * when the instance is terminated. Therefore, a manual memory reset is not required.
15
+ * Calls an action on the host and returns its response.
8
16
  *
9
- * This function has two internal implementations selected at build time:
10
- * 1. ASYNC_BUILD = true: For browsers, uses Asyncify to pause/resume execution.
11
- * 2. ASYNC_BUILD = false: For Elixir, uses a synchronous call-and-get-result pattern.
17
+ * The runtime owns serialization and memory for this boundary. The SDK passes
18
+ * the action name and parameters as JavaScript values, and the host returns the
19
+ * parsed response as a value.
12
20
  */
13
21
  export function execute(actionName, params, context) {
14
- // We must track the pointers for the parameters so we can free them after the call.
15
- // They are declared here to be accessible in the `finally` block.
16
- let actionNamePtr = 0;
17
- let actionNameLen = 0;
18
- let paramsPtr = 0;
19
- let paramsLen = 0;
20
- let contextPtr = 0;
21
- let contextLen = 0;
22
- try {
23
- const paramsJSON = JSON.stringify(params ?? null);
24
- const contextJSON = JSON.stringify(context);
25
- [actionNamePtr, actionNameLen] = stringToPtr(actionName);
26
- [paramsPtr, paramsLen] = stringToPtr(paramsJSON);
27
- [contextPtr, contextLen] = stringToPtr(contextJSON);
28
- if (__ASYNC_BUILD__) {
29
- // --- ASYNCIFY-AWARE PATH (FOR BROWSER) ---
30
- let responsePtr = 0;
31
- let responseLen = 0;
32
- try {
33
- // State 2 is "Rewinding". This check implements the stateful "double call"
34
- // pattern required by Asyncify.
35
- if (asyncify_get_state() === 2) {
36
- // This is the second call, during the rewind. We just need to stop the
37
- // rewind so that normal execution can resume.
38
- asyncify_stop_rewind();
39
- }
40
- else {
41
- // This is the first call. Call the host to start the async operation.
42
- // The host will then trigger the unwind.
43
- __host.call(actionNamePtr, actionNameLen, paramsPtr, paramsLen, contextPtr, contextLen);
44
- }
45
- // --- EXECUTION PAUSES HERE OR CONTINUES AFTER REWIND IS STOPPED ---
46
- // The host has placed the response in memory. Read it using pointers
47
- // retrieved from the Javy plugin.
48
- responsePtr = __wasm.get_response_ptr();
49
- responseLen = __wasm.get_response_len();
50
- const resultBytes = readBufferSlice(responsePtr, responseLen);
51
- const resultJSON = decoder.decode(resultBytes);
52
- // Immediately clear the response buffer pointers in the Javy plugin to
53
- // prevent reading stale data on subsequent nested calls.
54
- __wasm.clear_response_buffer();
55
- return JSON.parse(resultJSON);
56
- }
57
- finally {
58
- // 1. Free the result buffer allocated by the host.
59
- if (responsePtr > 0) {
60
- deallocate(responsePtr, responseLen);
61
- }
62
- }
63
- }
64
- else {
65
- // --- SYNCHRONOUS PATH (FOR ELIXIR BACKEND) ---
66
- let resultPtr = 0;
67
- let resultLen = 0;
68
- try {
69
- // 1. Make the synchronous call. The host now holds the result.
70
- __host.call(actionNamePtr, actionNameLen, paramsPtr, paramsLen, contextPtr, contextLen);
71
- // 2. Ask the host for the size of the result.
72
- resultLen = __host.getExecutionResultSize();
73
- if (resultLen === 0) {
74
- return { error: { message: 'Host returned an empty result.' }, ok: false };
75
- }
76
- // 3. Allocate memory inside WASM for the result.
77
- resultPtr = allocate(resultLen);
78
- // 4. Ask the host to write the result into our buffer.
79
- __host.getExecutionResult(resultPtr);
80
- // 5. Read the result from our buffer and return it.
81
- const resultBytes = readBufferSlice(resultPtr, resultLen);
82
- const resultJSON = decoder.decode(resultBytes);
83
- return JSON.parse(resultJSON);
84
- }
85
- finally {
86
- // Free the result buffer allocated by us.
87
- if (resultPtr > 0) {
88
- deallocate(resultPtr, resultLen);
89
- }
90
- }
91
- }
92
- }
93
- finally {
94
- // 2. Free the parameter buffers we allocated before the host call.
95
- if (actionNamePtr > 0) {
96
- deallocate(actionNamePtr, actionNameLen);
97
- }
98
- if (paramsPtr > 0) {
99
- deallocate(paramsPtr, paramsLen);
100
- }
101
- if (contextPtr > 0) {
102
- deallocate(contextPtr, contextLen);
103
- }
104
- }
22
+ assertRuntimeAbi();
23
+ void context;
24
+ return __host.call(actionName, params ?? null);
105
25
  }
106
26
  /**
107
- * Executes an action asynchronously on the host system (fire-and-forget).
27
+ * Calls an action on the host without waiting for it to answer.
108
28
  */
109
29
  export function executeAsync(actionName, params, context) {
110
- const paramsJSON = JSON.stringify(params ?? null);
111
- const contextJSON = JSON.stringify(context);
112
- const [actionNamePtr, actionNameLen] = stringToPtr(actionName);
113
- const [paramsPtr, paramsLen] = stringToPtr(paramsJSON);
114
- const [contextPtr, contextLen] = stringToPtr(contextJSON);
115
- // Make the asynchronous (fire-and-forget) call to the host.
116
- __host.cast(actionNamePtr, actionNameLen, paramsPtr, paramsLen, contextPtr, contextLen);
30
+ assertRuntimeAbi();
31
+ void context;
32
+ __host.cast(actionName, params ?? null);
117
33
  }
118
34
  /**
119
- * Retrieves the initial context payload from the host. This function orchestrates
120
- * the "pull" mechanism required by the execution environment.
121
- *
122
- * It first asks the host for the payload size, then allocates memory within
123
- * the WASM module, and finally asks the host to write the payload into the
124
- * allocated buffer.
125
- *
126
- * @returns A Uint8Array containing the context payload.
35
+ * Returns the execution context the host assembled for this run.
127
36
  */
128
37
  export function getContext() {
129
- // Ask the host for the size of the data.
130
- const size = __host.getContextSize();
131
- if (size === 0) {
132
- return new Uint8Array(0);
133
- }
134
- // Allocate memory for the data inside the WASM module's buffer.
135
- const ptr = allocate(size);
136
- let buffer;
137
- try {
138
- // Ask the host to write the data into the allocated memory region.
139
- __host.getContext(ptr);
140
- // Now that the data is in our memory, create a slice to read it.
141
- buffer = readBufferSlice(ptr, size);
142
- }
143
- finally {
144
- // Free the memory we allocated now that the data is in a JS-owned buffer.
145
- deallocate(ptr, size);
146
- }
147
- return buffer;
38
+ assertRuntimeAbi();
39
+ return __host.getContext();
148
40
  }
@@ -37,30 +37,38 @@ export class TextDecoder {
37
37
  let string = '';
38
38
  let i = 0;
39
39
  while (i < octets.length) {
40
- let octet = octets[i];
40
+ // `i < octets.length` already bounds this read; the explicit guard is what
41
+ // narrows the element type away from `undefined` for the arithmetic below.
42
+ const leadingOctet = octets[i];
43
+ if (leadingOctet === undefined)
44
+ break;
41
45
  let bytesNeeded = 0;
42
46
  let codePoint = 0;
43
- if (octet <= 0x7F) {
47
+ if (leadingOctet <= 0x7F) {
44
48
  bytesNeeded = 0;
45
- codePoint = octet & 0xFF;
49
+ codePoint = leadingOctet & 0xFF;
46
50
  }
47
- else if (octet <= 0xDF) {
51
+ else if (leadingOctet <= 0xDF) {
48
52
  bytesNeeded = 1;
49
- codePoint = octet & 0x1F;
53
+ codePoint = leadingOctet & 0x1F;
50
54
  }
51
- else if (octet <= 0xEF) {
55
+ else if (leadingOctet <= 0xEF) {
52
56
  bytesNeeded = 2;
53
- codePoint = octet & 0x0F;
57
+ codePoint = leadingOctet & 0x0F;
54
58
  }
55
- else if (octet <= 0xF4) {
59
+ else if (leadingOctet <= 0xF4) {
56
60
  bytesNeeded = 3;
57
- codePoint = octet & 0x07;
61
+ codePoint = leadingOctet & 0x07;
58
62
  }
59
63
  if (octets.length - i - bytesNeeded > 0) {
60
64
  let k = 0;
61
65
  while (k < bytesNeeded) {
62
- octet = octets[i + k + 1];
63
- codePoint = (codePoint << 6) | (octet & 0x3F);
66
+ // The branch condition guarantees `i + bytesNeeded < octets.length`,
67
+ // and `k < bytesNeeded`, so this index is always in range.
68
+ const continuationOctet = octets[i + k + 1];
69
+ if (continuationOctet === undefined)
70
+ break;
71
+ codePoint = (codePoint << 6) | (continuationOctet & 0x3F);
64
72
  k += 1;
65
73
  }
66
74
  }
@@ -0,0 +1,138 @@
1
+ export declare const PROTOCOL_VERSION: 1;
2
+ export type SpaceContext = {
3
+ applicationId: string;
4
+ kind: 'record';
5
+ recordId: string;
6
+ tableName: string;
7
+ } | {
8
+ kind: 'standalone';
9
+ };
10
+ export interface RecordFieldSnapshot {
11
+ error: null | string;
12
+ info: null | string;
13
+ readOnly: boolean;
14
+ required: boolean;
15
+ visible: boolean;
16
+ }
17
+ export interface RecordFormError {
18
+ code: string;
19
+ message: string;
20
+ }
21
+ export interface RecordErrorSnapshot {
22
+ fields: Readonly<Record<string, readonly string[]>>;
23
+ form: readonly RecordFormError[];
24
+ }
25
+ export interface RecordSnapshot {
26
+ errors: RecordErrorSnapshot;
27
+ fields: Readonly<Record<string, RecordFieldSnapshot>>;
28
+ revision: number;
29
+ values: Readonly<Record<string, unknown>>;
30
+ }
31
+ export type GraphQLVariables = Readonly<Record<string, unknown>>;
32
+ export interface SpaceDataTransport {
33
+ execute: <TResult = unknown>(document: string, variables?: GraphQLVariables) => Promise<TResult>;
34
+ }
35
+ export interface SimpleDataClient {
36
+ mutate: <TResult = unknown>(document: string, variables?: GraphQLVariables) => Promise<TResult>;
37
+ query: <TResult = unknown>(document: string, variables?: GraphQLVariables) => Promise<TResult>;
38
+ }
39
+ export interface RecordHandle {
40
+ readonly id: string;
41
+ snapshot: () => RecordSnapshot;
42
+ submit: () => Promise<RecordSubmitResult>;
43
+ update: (values: Readonly<Record<string, unknown>>) => Promise<RecordUpdateResult>;
44
+ }
45
+ export interface SimpleClient {
46
+ context: SpaceContext;
47
+ data: SimpleDataClient;
48
+ records: {
49
+ current: () => Promise<RecordHandle>;
50
+ };
51
+ }
52
+ export interface SpaceProtocolErrorPayload {
53
+ code: string;
54
+ details?: unknown;
55
+ message: string;
56
+ }
57
+ export declare class SpaceProtocolError extends Error {
58
+ readonly code: string;
59
+ readonly details?: unknown;
60
+ constructor({ code, details, message }: SpaceProtocolErrorPayload);
61
+ }
62
+ export interface SpaceDataErrorPayload {
63
+ code: string;
64
+ details?: unknown;
65
+ message: string;
66
+ }
67
+ export declare class SpaceDataError extends Error {
68
+ readonly code: string;
69
+ readonly details?: unknown;
70
+ constructor({ code, details, message }: SpaceDataErrorPayload);
71
+ }
72
+ export interface CurrentRecordRequest {
73
+ operation: 'record.current';
74
+ payload: Record<string, never>;
75
+ protocol: typeof PROTOCOL_VERSION;
76
+ requestId: string;
77
+ }
78
+ export interface CurrentRecordResult {
79
+ sessionId: string;
80
+ snapshot: RecordSnapshot;
81
+ }
82
+ export interface RecordUpdateRequest {
83
+ operation: 'record.update';
84
+ payload: {
85
+ sessionId: string;
86
+ values: Readonly<Record<string, unknown>>;
87
+ };
88
+ protocol: typeof PROTOCOL_VERSION;
89
+ requestId: string;
90
+ }
91
+ export interface RecordUpdateResult {
92
+ ok: boolean;
93
+ snapshot: RecordSnapshot;
94
+ }
95
+ export interface RecordSubmitRequest {
96
+ operation: 'record.submit';
97
+ payload: {
98
+ sessionId: string;
99
+ };
100
+ protocol: typeof PROTOCOL_VERSION;
101
+ requestId: string;
102
+ }
103
+ export interface RecordSubmitResult {
104
+ ok: boolean;
105
+ snapshot: RecordSnapshot;
106
+ }
107
+ export type ProtocolRequest = CurrentRecordRequest | RecordSubmitRequest | RecordUpdateRequest;
108
+ export interface ProtocolSuccessResponse<TResult> {
109
+ ok: true;
110
+ protocol: typeof PROTOCOL_VERSION;
111
+ requestId: string;
112
+ result: TResult;
113
+ }
114
+ export interface ProtocolErrorResponse {
115
+ error: SpaceProtocolErrorPayload;
116
+ ok: false;
117
+ protocol: typeof PROTOCOL_VERSION;
118
+ requestId: string;
119
+ }
120
+ export type ProtocolResponse<TResult> = ProtocolErrorResponse | ProtocolSuccessResponse<TResult>;
121
+ export interface SpaceTransport {
122
+ request: <TResult>(request: ProtocolRequest) => Promise<ProtocolResponse<TResult>>;
123
+ }
124
+ export interface SimpleClientOptions {
125
+ context?: SpaceContext;
126
+ dataTransport?: SpaceDataTransport;
127
+ nextRequestId?: () => string;
128
+ transport?: SpaceTransport;
129
+ }
130
+ /**
131
+ * Creates a framework-neutral Space client around a protocol transport.
132
+ *
133
+ * MessagePort setup is intentionally outside this factory so the same public
134
+ * contract can be exercised in a first-party direct adapter and in any UI
135
+ * framework without importing browser-specific code.
136
+ */
137
+ export declare function createSimpleClient({ context, dataTransport, nextRequestId, transport, }: SimpleClientOptions): SimpleClient;
138
+ export declare function isSpaceContext(value: unknown): value is SpaceContext;
@@ -0,0 +1,226 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _ProtocolRecordHandle_nextRequestId, _ProtocolRecordHandle_transport, _ProtocolRecordHandle_snapshot;
13
+ export const PROTOCOL_VERSION = 1;
14
+ export class SpaceProtocolError extends Error {
15
+ constructor({ code, details, message }) {
16
+ super(message);
17
+ this.name = 'SpaceProtocolError';
18
+ this.code = code;
19
+ this.details = details;
20
+ }
21
+ }
22
+ export class SpaceDataError extends Error {
23
+ constructor({ code, details, message }) {
24
+ super(message);
25
+ this.name = 'SpaceDataError';
26
+ this.code = code;
27
+ this.details = details;
28
+ }
29
+ }
30
+ /**
31
+ * Creates a framework-neutral Space client around a protocol transport.
32
+ *
33
+ * MessagePort setup is intentionally outside this factory so the same public
34
+ * contract can be exercised in a first-party direct adapter and in any UI
35
+ * framework without importing browser-specific code.
36
+ */
37
+ export function createSimpleClient({ context = { kind: 'standalone' }, dataTransport, nextRequestId = createRequestId, transport, }) {
38
+ const immutableContext = immutableSpaceContext(context);
39
+ return {
40
+ context: immutableContext,
41
+ data: {
42
+ mutate: (document, variables) => executeData(dataTransport, document, variables),
43
+ query: (document, variables) => executeData(dataTransport, document, variables),
44
+ },
45
+ records: {
46
+ async current() {
47
+ if (immutableContext.kind !== 'record') {
48
+ throw new SpaceProtocolError({
49
+ code: 'unavailable',
50
+ message: 'The current record is available only when this Space is configured as a record view.',
51
+ });
52
+ }
53
+ if (!transport) {
54
+ throw new SpaceProtocolError({
55
+ code: 'unavailable',
56
+ message: 'The record protocol is unavailable for this record Space.',
57
+ });
58
+ }
59
+ const request = {
60
+ operation: 'record.current',
61
+ payload: {},
62
+ protocol: PROTOCOL_VERSION,
63
+ requestId: nextRequestId(),
64
+ };
65
+ const response = await transport.request(request);
66
+ const result = readResponse(response, request);
67
+ if (!isCurrentRecordResult(result)) {
68
+ throw invalidResponse('The primary-record response is malformed.');
69
+ }
70
+ return new ProtocolRecordHandle(result, nextRequestId, transport);
71
+ },
72
+ },
73
+ };
74
+ }
75
+ export function isSpaceContext(value) {
76
+ if (!value || typeof value !== 'object') {
77
+ return false;
78
+ }
79
+ const context = value;
80
+ if (context.kind === 'standalone') {
81
+ return true;
82
+ }
83
+ return context.kind === 'record'
84
+ && typeof context.applicationId === 'string'
85
+ && context.applicationId.length > 0
86
+ && typeof context.tableName === 'string'
87
+ && context.tableName.length > 0
88
+ && typeof context.recordId === 'string'
89
+ && context.recordId.length > 0;
90
+ }
91
+ function executeData(dataTransport, document, variables) {
92
+ if (!dataTransport) {
93
+ return Promise.reject(new SpaceDataError({
94
+ code: 'unavailable',
95
+ message: 'The Space data transport is unavailable.',
96
+ }));
97
+ }
98
+ return dataTransport.execute(document, variables);
99
+ }
100
+ class ProtocolRecordHandle {
101
+ constructor({ sessionId, snapshot }, nextRequestId, transport) {
102
+ _ProtocolRecordHandle_nextRequestId.set(this, void 0);
103
+ _ProtocolRecordHandle_transport.set(this, void 0);
104
+ _ProtocolRecordHandle_snapshot.set(this, void 0);
105
+ this.id = sessionId;
106
+ __classPrivateFieldSet(this, _ProtocolRecordHandle_nextRequestId, nextRequestId, "f");
107
+ __classPrivateFieldSet(this, _ProtocolRecordHandle_snapshot, immutableSnapshot(snapshot), "f");
108
+ __classPrivateFieldSet(this, _ProtocolRecordHandle_transport, transport, "f");
109
+ }
110
+ snapshot() {
111
+ return __classPrivateFieldGet(this, _ProtocolRecordHandle_snapshot, "f");
112
+ }
113
+ async update(values) {
114
+ const request = {
115
+ operation: 'record.update',
116
+ payload: {
117
+ sessionId: this.id,
118
+ values,
119
+ },
120
+ protocol: PROTOCOL_VERSION,
121
+ requestId: __classPrivateFieldGet(this, _ProtocolRecordHandle_nextRequestId, "f").call(this),
122
+ };
123
+ const response = await __classPrivateFieldGet(this, _ProtocolRecordHandle_transport, "f").request(request);
124
+ const result = readResponse(response, request);
125
+ if (!isRecordUpdateResult(result))
126
+ throw invalidResponse('The record-update response is malformed.');
127
+ const snapshot = immutableSnapshot(result.snapshot);
128
+ __classPrivateFieldSet(this, _ProtocolRecordHandle_snapshot, snapshot, "f");
129
+ return { ok: result.ok, snapshot };
130
+ }
131
+ async submit() {
132
+ const request = {
133
+ operation: 'record.submit',
134
+ payload: { sessionId: this.id },
135
+ protocol: PROTOCOL_VERSION,
136
+ requestId: __classPrivateFieldGet(this, _ProtocolRecordHandle_nextRequestId, "f").call(this),
137
+ };
138
+ const response = await __classPrivateFieldGet(this, _ProtocolRecordHandle_transport, "f").request(request);
139
+ const result = readResponse(response, request);
140
+ if (!isRecordSubmitResult(result))
141
+ throw invalidResponse('The record-submit response is malformed.');
142
+ const snapshot = immutableSnapshot(result.snapshot);
143
+ __classPrivateFieldSet(this, _ProtocolRecordHandle_snapshot, snapshot, "f");
144
+ return { ok: result.ok, snapshot };
145
+ }
146
+ }
147
+ _ProtocolRecordHandle_nextRequestId = new WeakMap(), _ProtocolRecordHandle_transport = new WeakMap(), _ProtocolRecordHandle_snapshot = new WeakMap();
148
+ function createRequestId() {
149
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
150
+ return crypto.randomUUID();
151
+ return `space-request-${Math.random().toString(36).slice(2)}`;
152
+ }
153
+ function immutableSnapshot(snapshot) {
154
+ if (!isRecordSnapshot(snapshot))
155
+ throw invalidResponse('The record snapshot is malformed.');
156
+ return deepFreeze(structuredClone(snapshot));
157
+ }
158
+ function immutableSpaceContext(context) {
159
+ return deepFreeze(structuredClone(context));
160
+ }
161
+ function deepFreeze(value) {
162
+ if (!value || typeof value !== 'object' || Object.isFrozen(value))
163
+ return value;
164
+ for (const child of Object.values(value))
165
+ deepFreeze(child);
166
+ return Object.freeze(value);
167
+ }
168
+ function invalidResponse(message) {
169
+ return new SpaceProtocolError({ code: 'invalid_response', message });
170
+ }
171
+ function isCurrentRecordResult(value) {
172
+ if (!value || typeof value !== 'object')
173
+ return false;
174
+ const result = value;
175
+ return typeof result.sessionId === 'string' && result.sessionId.length > 0 && isRecordSnapshot(result.snapshot);
176
+ }
177
+ function isRecordSnapshot(value) {
178
+ if (!value || typeof value !== 'object')
179
+ return false;
180
+ const snapshot = value;
181
+ return Number.isSafeInteger(snapshot.revision)
182
+ && (snapshot.revision ?? -1) >= 0
183
+ && isRecordErrorSnapshot(snapshot.errors)
184
+ && isRecordFieldSnapshotMap(snapshot.fields)
185
+ && isObjectRecord(snapshot.values);
186
+ }
187
+ function isRecordUpdateResult(value) {
188
+ if (!value || typeof value !== 'object')
189
+ return false;
190
+ const result = value;
191
+ return typeof result.ok === 'boolean' && isRecordSnapshot(result.snapshot);
192
+ }
193
+ function isRecordSubmitResult(value) {
194
+ if (!value || typeof value !== 'object')
195
+ return false;
196
+ const result = value;
197
+ return typeof result.ok === 'boolean' && isRecordSnapshot(result.snapshot);
198
+ }
199
+ function isObjectRecord(value) {
200
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
201
+ }
202
+ function isRecordErrorSnapshot(value) {
203
+ if (!isObjectRecord(value) || !isObjectRecord(value.fields) || !Array.isArray(value.form))
204
+ return false;
205
+ return Object.values(value.fields).every(errors => Array.isArray(errors) && errors.every(error => typeof error === 'string'))
206
+ && value.form.every(error => isObjectRecord(error) && typeof error.code === 'string' && typeof error.message === 'string');
207
+ }
208
+ function isRecordFieldSnapshotMap(value) {
209
+ return isObjectRecord(value) && Object.values(value).every((field) => {
210
+ if (!isObjectRecord(field))
211
+ return false;
212
+ return (field.error === null || typeof field.error === 'string')
213
+ && (field.info === null || typeof field.info === 'string')
214
+ && typeof field.readOnly === 'boolean'
215
+ && typeof field.required === 'boolean'
216
+ && typeof field.visible === 'boolean';
217
+ });
218
+ }
219
+ function readResponse(response, request) {
220
+ if (response.protocol !== PROTOCOL_VERSION || response.requestId !== request.requestId) {
221
+ throw invalidResponse('The response does not match the request envelope.');
222
+ }
223
+ if (!response.ok)
224
+ throw new SpaceProtocolError(response.error);
225
+ return response.result;
226
+ }
@@ -0,0 +1,33 @@
1
+ import type { SimpleClient } from './core.js';
2
+ import { SpaceDataError, SpaceProtocolError } from './core.js';
3
+ export { SpaceDataError, SpaceProtocolError, };
4
+ export type { GraphQLVariables, RecordErrorSnapshot, RecordFieldSnapshot, RecordFormError, RecordHandle, RecordSnapshot, RecordSubmitResult, RecordUpdateResult, SimpleClient, SimpleDataClient, SpaceContext, SpaceDataErrorPayload, SpaceProtocolErrorPayload, } from './core.js';
5
+ interface MessagePortLike {
6
+ onmessage: null | ((event: {
7
+ data: unknown;
8
+ }) => void);
9
+ postMessage: (message: unknown) => void;
10
+ start?: () => void;
11
+ }
12
+ export interface SpaceWindowLike {
13
+ addEventListener: (type: 'message', listener: (event: SpaceMessageEvent) => void) => void;
14
+ parent: {
15
+ postMessage: (message: unknown, targetOrigin: string) => void;
16
+ };
17
+ removeEventListener: (type: 'message', listener: (event: SpaceMessageEvent) => void) => void;
18
+ }
19
+ export interface SpaceMessageEvent {
20
+ data: unknown;
21
+ origin: string;
22
+ ports: MessagePortLike[];
23
+ }
24
+ export interface ConnectSpaceOptions {
25
+ targetOrigin: string;
26
+ window?: SpaceWindowLike;
27
+ }
28
+ /**
29
+ * Connects any embedded Space to its parent through the dedicated MessagePort
30
+ * handshake. Record operations become available only when the host negotiates
31
+ * record protocol v1 for a configured record view.
32
+ */
33
+ export declare function connectSpace({ targetOrigin, window }: ConnectSpaceOptions): Promise<SimpleClient>;
@@ -0,0 +1,135 @@
1
+ import { createSimpleClient, isSpaceContext, PROTOCOL_VERSION, SpaceDataError, SpaceProtocolError, } from './core.js';
2
+ export { SpaceDataError, SpaceProtocolError, };
3
+ /**
4
+ * Connects any embedded Space to its parent through the dedicated MessagePort
5
+ * handshake. Record operations become available only when the host negotiates
6
+ * record protocol v1 for a configured record view.
7
+ */
8
+ export function connectSpace({ targetOrigin, window = globalThis.window }) {
9
+ if (!window) {
10
+ return Promise.reject(new SpaceProtocolError({
11
+ code: 'unavailable',
12
+ message: 'The Space SDK requires a browser window.',
13
+ }));
14
+ }
15
+ return new Promise((resolve, reject) => {
16
+ const onMessage = (event) => {
17
+ if (event.origin !== targetOrigin || !isInitializationMessage(event.data))
18
+ return;
19
+ window.removeEventListener('message', onMessage);
20
+ const port = event.ports[0];
21
+ if (!port) {
22
+ reject(new SpaceProtocolError({
23
+ code: 'unavailable',
24
+ message: 'The Space host did not provide a MessagePort.',
25
+ }));
26
+ return;
27
+ }
28
+ if (!isSpaceContext(event.data.context)) {
29
+ reject(new SpaceProtocolError({
30
+ code: 'invalid_response',
31
+ message: 'The Space host did not provide valid context.',
32
+ }));
33
+ return;
34
+ }
35
+ const transport = createMessagePortTransport(port);
36
+ const recordTransport = event.data.protocols?.record === PROTOCOL_VERSION
37
+ ? transport
38
+ : undefined;
39
+ resolve(createSimpleClient({
40
+ context: event.data.context,
41
+ dataTransport: transport,
42
+ transport: recordTransport,
43
+ }));
44
+ };
45
+ window.addEventListener('message', onMessage);
46
+ window.parent.postMessage({
47
+ protocols: { record: [PROTOCOL_VERSION] },
48
+ type: 'SPACE_READY',
49
+ }, targetOrigin);
50
+ });
51
+ }
52
+ function createMessagePortTransport(port) {
53
+ const pendingData = new Map();
54
+ const pending = new Map();
55
+ port.onmessage = (event) => {
56
+ const message = event.data;
57
+ if (!message || typeof message !== 'object')
58
+ return;
59
+ const envelope = message;
60
+ if (envelope.type === 'SPACE_PROTOCOL_RESPONSE' && envelope.response) {
61
+ const requestId = envelope.response.requestId;
62
+ const request = pending.get(requestId);
63
+ if (!request)
64
+ return;
65
+ pending.delete(requestId);
66
+ request.resolve(envelope.response);
67
+ return;
68
+ }
69
+ if (envelope.type !== 'GRAPHQL_RESPONSE' || typeof envelope.id !== 'string')
70
+ return;
71
+ const request = pendingData.get(envelope.id);
72
+ if (!request)
73
+ return;
74
+ pendingData.delete(envelope.id);
75
+ if (envelope.error || envelope.errors) {
76
+ request.reject(new SpaceDataError({
77
+ code: 'request_failed',
78
+ details: envelope.errors,
79
+ message: readGraphQLErrorMessage(envelope.error, envelope.errors),
80
+ }));
81
+ return;
82
+ }
83
+ request.resolve(envelope.data);
84
+ };
85
+ port.start?.();
86
+ return {
87
+ execute: (document, variables) => {
88
+ return new Promise((resolve, reject) => {
89
+ const id = createDataRequestId();
90
+ pendingData.set(id, { reject, resolve: result => resolve(result) });
91
+ port.postMessage({
92
+ payload: { id, query: document, variables },
93
+ type: 'GRAPHQL_REQUEST',
94
+ });
95
+ });
96
+ },
97
+ request: (request) => {
98
+ return new Promise((resolve) => {
99
+ pending.set(request.requestId, {
100
+ resolve: response => resolve(response),
101
+ });
102
+ port.postMessage({ request, type: 'SPACE_PROTOCOL_REQUEST' });
103
+ });
104
+ },
105
+ };
106
+ }
107
+ function createDataRequestId() {
108
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
109
+ return crypto.randomUUID();
110
+ return `space-data-${Math.random().toString(36).slice(2)}`;
111
+ }
112
+ function readGraphQLErrorMessage(error, errors) {
113
+ if (typeof error === 'string' && error)
114
+ return error;
115
+ if (Array.isArray(errors)) {
116
+ const firstError = errors[0];
117
+ if (firstError && typeof firstError === 'object') {
118
+ const details = firstError;
119
+ const issue = details.extensions?.issues?.[0]?.message;
120
+ if (typeof issue === 'string' && issue)
121
+ return issue;
122
+ const message = details.extensions?.details?.message;
123
+ if (typeof message === 'string' && message)
124
+ return message;
125
+ if (typeof details.message === 'string' && details.message)
126
+ return details.message;
127
+ }
128
+ }
129
+ return 'GraphQL request failed.';
130
+ }
131
+ function isInitializationMessage(value) {
132
+ if (!value || typeof value !== 'object')
133
+ return false;
134
+ return value.type === 'INIT_RPC';
135
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simpleplatform/sdk",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "Simple Platform Typescript SDK",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://docs.simple.dev",
@@ -49,6 +49,10 @@
49
49
  "types": "./dist/host.d.ts",
50
50
  "default": "./dist/host.js"
51
51
  },
52
+ "./space": {
53
+ "types": "./dist/space/index.d.ts",
54
+ "default": "./dist/space/index.js"
55
+ },
52
56
  "./worker": "./dist/worker-override.js",
53
57
  "./package.json": "./package.json"
54
58
  },
@@ -68,6 +72,8 @@
68
72
  "typescript": "5.9.3"
69
73
  },
70
74
  "scripts": {
71
- "build": "rm -rf dist && tsc && cp src/worker-override.js dist/ && cp cli/build.js dist/"
75
+ "build": "rm -rf dist && tsc && cp src/worker-override.js dist/ && cp cli/build.js dist/ && cp src/space/package.json dist/space/",
76
+ "test": "pnpm run build && node --test test/*.test.mjs",
77
+ "typecheck": "tsc --noEmit"
72
78
  }
73
79
  }
@@ -1,103 +0,0 @@
1
- /**
2
- * @fileoverview WebAssembly Memory Management Module
3
- *
4
- * Provides a high-level interface for managing WebAssembly linear memory operations.
5
- * This module handles string-based data exchange between JavaScript and the WASM
6
- * runtime through direct UTF-8 operations, avoiding unnecessary serialization overhead.
7
- *
8
- * ## Architecture
9
- *
10
- * The memory bridge operates on two main principles:
11
- * - **Direct String Operations**: Strings are passed directly between JS and WASM
12
- * - **UTF-8 Encoding**: All string data is handled as UTF-8 encoded bytes
13
- *
14
- * ## Usage
15
- *
16
- * ```typescript
17
- * // Allocate memory for a string
18
- * const [ptr, len] = stringToPtr("Hello, World!")
19
- *
20
- * // Read data back from memory
21
- * const data = readBufferSlice(ptr, len)
22
- * ```
23
- */
24
- /** Reusable TextDecoder instance for UTF-8 string decoding. */
25
- export declare const decoder: TextDecoder;
26
- /**
27
- * Allocates a block of memory within the WebAssembly linear memory space.
28
- *
29
- * This function provides the primary interface for requesting memory from
30
- * the WASM module's allocator. The returned pointer can be used for
31
- * subsequent read/write operations.
32
- *
33
- * @param size - Number of bytes to allocate
34
- * @returns Memory pointer (offset) to the allocated block
35
- *
36
- * @example
37
- * ```typescript
38
- * const ptr = allocate(1024) // Allocate 1KB
39
- * ```
40
- */
41
- export declare function allocate(size: number): number;
42
- /**
43
- *
44
- * @param ptr - Memory pointer to deallocate
45
- * @param size - Number of bytes to deallocate
46
- *
47
- * @example
48
- * ```typescript
49
- * deallocate(ptr, 1024) // Deallocate 1KB
50
- * ```
51
- */
52
- export declare function deallocate(ptr: number, size: number): void;
53
- /**
54
- * Reads string data from WebAssembly memory and returns it as UTF-8 bytes.
55
- *
56
- * This function provides a safe interface for reading string data from
57
- * WASM memory, with proper error handling and fallback behavior.
58
- *
59
- * @param ptr - Memory pointer to read from
60
- * @param len - Number of bytes to read
61
- * @returns UTF-8 encoded bytes of the string data
62
- *
63
- * @example
64
- * ```typescript
65
- * const data = readBufferSlice(ptr, len)
66
- * const text = decoder.decode(data) // Convert back to string
67
- * ```
68
- */
69
- export declare function readBufferSlice(ptr: number, len: number): Uint8Array;
70
- /**
71
- * Reads a string directly from WebAssembly memory.
72
- *
73
- * This is a convenience function that combines memory reading and UTF-8
74
- * decoding in a single operation.
75
- *
76
- * @param ptr - Memory pointer to read from
77
- * @param len - Number of bytes to read
78
- * @returns The decoded UTF-8 string
79
- *
80
- * @example
81
- * ```typescript
82
- * const text = readString(ptr, len)
83
- * ```
84
- */
85
- export declare function readString(ptr: number, len: number): string;
86
- /**
87
- * Writes a JavaScript string to WebAssembly memory and returns its location.
88
- *
89
- * This function handles the complete process of:
90
- * 1. Calculating the UTF-8 byte length of the string
91
- * 2. Allocating sufficient memory in the WASM linear memory
92
- * 3. Writing the string data to the allocated location
93
- *
94
- * @param str - The string to write to memory
95
- * @returns A tuple containing [pointer, byte_length]
96
- *
97
- * @example
98
- * ```typescript
99
- * const [ptr, len] = stringToPtr("Hello, World!")
100
- * // ptr points to the string data, len is the byte length
101
- * ```
102
- */
103
- export declare function stringToPtr(str: string): [number, number];
@@ -1,152 +0,0 @@
1
- /**
2
- * @fileoverview WebAssembly Memory Management Module
3
- *
4
- * Provides a high-level interface for managing WebAssembly linear memory operations.
5
- * This module handles string-based data exchange between JavaScript and the WASM
6
- * runtime through direct UTF-8 operations, avoiding unnecessary serialization overhead.
7
- *
8
- * ## Architecture
9
- *
10
- * The memory bridge operates on two main principles:
11
- * - **Direct String Operations**: Strings are passed directly between JS and WASM
12
- * - **UTF-8 Encoding**: All string data is handled as UTF-8 encoded bytes
13
- *
14
- * ## Usage
15
- *
16
- * ```typescript
17
- * // Allocate memory for a string
18
- * const [ptr, len] = stringToPtr("Hello, World!")
19
- *
20
- * // Read data back from memory
21
- * const data = readBufferSlice(ptr, len)
22
- * ```
23
- */
24
- // =============================================================================
25
- // TEXT ENCODING UTILITIES
26
- // =============================================================================
27
- /** Reusable TextEncoder instance for UTF-8 string encoding. */
28
- const encoder = new TextEncoder();
29
- /** Reusable TextDecoder instance for UTF-8 string decoding. */
30
- export const decoder = new TextDecoder('utf-8');
31
- // =============================================================================
32
- // MEMORY ALLOCATION
33
- // =============================================================================
34
- /**
35
- * Allocates a block of memory within the WebAssembly linear memory space.
36
- *
37
- * This function provides the primary interface for requesting memory from
38
- * the WASM module's allocator. The returned pointer can be used for
39
- * subsequent read/write operations.
40
- *
41
- * @param size - Number of bytes to allocate
42
- * @returns Memory pointer (offset) to the allocated block
43
- *
44
- * @example
45
- * ```typescript
46
- * const ptr = allocate(1024) // Allocate 1KB
47
- * ```
48
- */
49
- export function allocate(size) {
50
- return __wasm.alloc(size);
51
- }
52
- // =============================================================================
53
- // STRING MEMORY OPERATIONS
54
- // =============================================================================
55
- /**
56
- *
57
- * @param ptr - Memory pointer to deallocate
58
- * @param size - Number of bytes to deallocate
59
- *
60
- * @example
61
- * ```typescript
62
- * deallocate(ptr, 1024) // Deallocate 1KB
63
- * ```
64
- */
65
- export function deallocate(ptr, size) {
66
- __wasm.dealloc(ptr, size);
67
- }
68
- /**
69
- * Reads string data from WebAssembly memory and returns it as UTF-8 bytes.
70
- *
71
- * This function provides a safe interface for reading string data from
72
- * WASM memory, with proper error handling and fallback behavior.
73
- *
74
- * @param ptr - Memory pointer to read from
75
- * @param len - Number of bytes to read
76
- * @returns UTF-8 encoded bytes of the string data
77
- *
78
- * @example
79
- * ```typescript
80
- * const data = readBufferSlice(ptr, len)
81
- * const text = decoder.decode(data) // Convert back to string
82
- * ```
83
- */
84
- export function readBufferSlice(ptr, len) {
85
- if (len === 0) {
86
- return new Uint8Array(0);
87
- }
88
- try {
89
- const str = __wasm.read_string(ptr, len);
90
- return encoder.encode(str);
91
- }
92
- catch (error) {
93
- console.error(`Memory read failed at ptr=${ptr}, len=${len}:`, error);
94
- return new Uint8Array(0);
95
- }
96
- }
97
- // =============================================================================
98
- // UTILITY FUNCTIONS
99
- // =============================================================================
100
- /**
101
- * Reads a string directly from WebAssembly memory.
102
- *
103
- * This is a convenience function that combines memory reading and UTF-8
104
- * decoding in a single operation.
105
- *
106
- * @param ptr - Memory pointer to read from
107
- * @param len - Number of bytes to read
108
- * @returns The decoded UTF-8 string
109
- *
110
- * @example
111
- * ```typescript
112
- * const text = readString(ptr, len)
113
- * ```
114
- */
115
- export function readString(ptr, len) {
116
- if (len === 0) {
117
- return '';
118
- }
119
- try {
120
- return __wasm.read_string(ptr, len);
121
- }
122
- catch (error) {
123
- console.error(`String read failed at ptr=${ptr}, len=${len}:`, error);
124
- return '';
125
- }
126
- }
127
- /**
128
- * Writes a JavaScript string to WebAssembly memory and returns its location.
129
- *
130
- * This function handles the complete process of:
131
- * 1. Calculating the UTF-8 byte length of the string
132
- * 2. Allocating sufficient memory in the WASM linear memory
133
- * 3. Writing the string data to the allocated location
134
- *
135
- * @param str - The string to write to memory
136
- * @returns A tuple containing [pointer, byte_length]
137
- *
138
- * @example
139
- * ```typescript
140
- * const [ptr, len] = stringToPtr("Hello, World!")
141
- * // ptr points to the string data, len is the byte length
142
- * ```
143
- */
144
- export function stringToPtr(str) {
145
- if (str.length === 0) {
146
- return [allocate(0), 0];
147
- }
148
- const bytes = encoder.encode(str);
149
- const ptr = allocate(bytes.length);
150
- __wasm.write_string(ptr, str);
151
- return [ptr, bytes.length];
152
- }