@simpleplatform/sdk 1.2.1 → 2.0.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.
- package/README.md +90 -0
- package/dist/graphql.js +0 -2
- package/dist/host.d.ts +7 -18
- package/dist/host.js +27 -135
- package/dist/index.js +4 -2
- package/dist/internal/polyfills.js +19 -11
- package/dist/space/core.d.ts +138 -0
- package/dist/space/core.js +226 -0
- package/dist/space/index.d.ts +33 -0
- package/dist/space/index.js +135 -0
- package/dist/space/package.json +3 -0
- package/package.json +8 -2
- package/dist/internal/memory.d.ts +0 -103
- package/dist/internal/memory.js +0 -152
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/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
|
-
*
|
|
3
|
+
* Calls an action on the host and returns its response.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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():
|
|
17
|
+
export declare function getContext(): any;
|
package/dist/host.js
CHANGED
|
@@ -1,148 +1,40 @@
|
|
|
1
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
*
|
|
27
|
+
* Calls an action on the host without waiting for it to answer.
|
|
108
28
|
*/
|
|
109
29
|
export function executeAsync(actionName, params, context) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
130
|
-
|
|
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
|
}
|
package/dist/index.js
CHANGED
|
@@ -141,8 +141,10 @@ function readInputFromHost() {
|
|
|
141
141
|
return JSON.stringify(globalThis.__SIMPLE_INITIAL_PAYLOAD__.request);
|
|
142
142
|
}
|
|
143
143
|
// Otherwise, we are in the main WASM module and must read from the host ABI.
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
// The runtime parses the context on its own side now, so what comes back is a
|
|
145
|
+
// value rather than bytes. Both branches of this function still answer with
|
|
146
|
+
// text because that is what its callers parse.
|
|
147
|
+
return JSON.stringify(host.getContext());
|
|
146
148
|
}
|
|
147
149
|
function returnError(message, context) {
|
|
148
150
|
const response = { data: null, errors: [message], ok: false };
|
|
@@ -37,30 +37,38 @@ export class TextDecoder {
|
|
|
37
37
|
let string = '';
|
|
38
38
|
let i = 0;
|
|
39
39
|
while (i < octets.length) {
|
|
40
|
-
|
|
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 (
|
|
47
|
+
if (leadingOctet <= 0x7F) {
|
|
44
48
|
bytesNeeded = 0;
|
|
45
|
-
codePoint =
|
|
49
|
+
codePoint = leadingOctet & 0xFF;
|
|
46
50
|
}
|
|
47
|
-
else if (
|
|
51
|
+
else if (leadingOctet <= 0xDF) {
|
|
48
52
|
bytesNeeded = 1;
|
|
49
|
-
codePoint =
|
|
53
|
+
codePoint = leadingOctet & 0x1F;
|
|
50
54
|
}
|
|
51
|
-
else if (
|
|
55
|
+
else if (leadingOctet <= 0xEF) {
|
|
52
56
|
bytesNeeded = 2;
|
|
53
|
-
codePoint =
|
|
57
|
+
codePoint = leadingOctet & 0x0F;
|
|
54
58
|
}
|
|
55
|
-
else if (
|
|
59
|
+
else if (leadingOctet <= 0xF4) {
|
|
56
60
|
bytesNeeded = 3;
|
|
57
|
-
codePoint =
|
|
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
|
-
|
|
63
|
-
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simpleplatform/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
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];
|
package/dist/internal/memory.js
DELETED
|
@@ -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
|
-
}
|