@simpleplatform/sdk 2.0.0 → 2.1.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 +39 -1
- package/dist/host.d.ts +15 -0
- package/dist/host.js +35 -0
- package/dist/http.d.ts +7 -0
- package/dist/http.js +1 -0
- package/dist/index.js +39 -5
- package/dist/storage.d.ts +76 -0
- package/dist/storage.js +152 -1
- package/dist/worker-override.js +12 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ The TypeScript SDK is organized into focused modules for different capabilities:
|
|
|
39
39
|
| **HTTP** | `@simpleplatform/sdk/http` | External HTTP requests |
|
|
40
40
|
| **Security** | `@simpleplatform/sdk/security` | Security policy authoring |
|
|
41
41
|
| **Settings** | `@simpleplatform/sdk/settings` | Application settings retrieval |
|
|
42
|
-
| **Storage** | `@simpleplatform/sdk/storage` | File upload and
|
|
42
|
+
| **Storage** | `@simpleplatform/sdk/storage` | File upload, and reading a stored file's bytes |
|
|
43
43
|
| **Space** | `@simpleplatform/sdk/space` | Behavior-aware record workflows in a Space |
|
|
44
44
|
|
|
45
45
|
## Embedded Spaces
|
|
@@ -274,8 +274,23 @@ const response = await http.fetch(
|
|
|
274
274
|
},
|
|
275
275
|
request.context
|
|
276
276
|
)
|
|
277
|
+
|
|
278
|
+
// A browser request that carries the page's cookies
|
|
279
|
+
const session = await http.fetch(
|
|
280
|
+
{
|
|
281
|
+
credentials: 'include',
|
|
282
|
+
url: 'https://api.example.com/session'
|
|
283
|
+
},
|
|
284
|
+
request.context
|
|
285
|
+
)
|
|
277
286
|
```
|
|
278
287
|
|
|
288
|
+
`credentials` is the browser's credentials mode for the request: `'omit'`,
|
|
289
|
+
`'same-origin'` or `'include'`. A request that names none sends none, and the
|
|
290
|
+
browser host then omits credentials. A cross-origin request that includes them
|
|
291
|
+
still needs the endpoint to allow the page's origin and credentials through
|
|
292
|
+
CORS. The server host has no browser credentials and ignores the option.
|
|
293
|
+
|
|
279
294
|
### Security Module
|
|
280
295
|
|
|
281
296
|
Define declarative security policies with a fluent, global-style API:
|
|
@@ -370,6 +385,29 @@ console.log(documentHandle.mime_type) // "application/pdf"
|
|
|
370
385
|
console.log(documentHandle.size) // File size in bytes
|
|
371
386
|
```
|
|
372
387
|
|
|
388
|
+
The way back out takes the same handle and answers with the file's bytes:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
import { read, readRange, size } from '@simpleplatform/sdk/storage'
|
|
392
|
+
|
|
393
|
+
const bytes: Uint8Array = await read(documentHandle, request.context)
|
|
394
|
+
|
|
395
|
+
const length = await size(documentHandle, request.context) // without reading any of it
|
|
396
|
+
const head = await readRange(documentHandle, 0, 1024, request.context) // the first kilobyte
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The bytes cross from the host as they are, with no JSON and no base64. `read`
|
|
400
|
+
asks for the size first and reads the file in ranges of at most
|
|
401
|
+
`MAX_RANGE_BYTES` (16 MiB): a file that fits one range arrives as one array, and
|
|
402
|
+
a larger one is read into a single buffer allocated once at exactly its size.
|
|
403
|
+
`readRange` asks for the size first too, so a range that runs past the end of
|
|
404
|
+
the file answers with exactly the bytes up to the end, and one that starts at or
|
|
405
|
+
past the end is refused before any of it is read.
|
|
406
|
+
|
|
407
|
+
Reading is for server actions; a browser action is refused. It needs a runtime
|
|
408
|
+
plugin that provides `__host.callBytes`, and an older plugin is refused with a
|
|
409
|
+
message saying so rather than handed bytes it would try to read as JSON.
|
|
410
|
+
|
|
373
411
|
### Type Definitions
|
|
374
412
|
|
|
375
413
|
The TypeScript SDK is **fully typed** with comprehensive TypeScript definitions. Leverage IDE autocompletion and compile-time type checking:
|
package/dist/host.d.ts
CHANGED
|
@@ -7,6 +7,21 @@ import type { Context, SimpleResponse } from './types';
|
|
|
7
7
|
* parsed response as a value.
|
|
8
8
|
*/
|
|
9
9
|
export declare function execute<T = any>(actionName: string, params: any, context: Context): SimpleResponse<T>;
|
|
10
|
+
/**
|
|
11
|
+
* Calls an action whose reply is a run of bytes, such as a range of a stored
|
|
12
|
+
* file, and returns them.
|
|
13
|
+
*
|
|
14
|
+
* The runtime hands the bytes over as a `Uint8Array` that owns them: no JSON,
|
|
15
|
+
* no base64, and no address. A host that refused answers with its ordinary
|
|
16
|
+
* envelope instead, returned here as a failed response, so a caller checks
|
|
17
|
+
* `ok` exactly as it does for `execute`. Its message names the call — `<action>
|
|
18
|
+
* failed: <the host's reason>` — worded as the Rust and Go SDKs word it.
|
|
19
|
+
*
|
|
20
|
+
* A runtime plugin released before this call existed does not provide it, and
|
|
21
|
+
* is refused here, before anything is sent, rather than handed a reply it
|
|
22
|
+
* would try to read as JSON.
|
|
23
|
+
*/
|
|
24
|
+
export declare function executeBytes(actionName: string, params: any, context: Context): SimpleResponse<Uint8Array>;
|
|
10
25
|
/**
|
|
11
26
|
* Calls an action on the host without waiting for it to answer.
|
|
12
27
|
*/
|
package/dist/host.js
CHANGED
|
@@ -11,6 +11,7 @@ function assertRuntimeAbi() {
|
|
|
11
11
|
throw new Error(ABI_MISMATCH_MESSAGE);
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
const BYTES_ABI_MISSING_MESSAGE = 'Reading a reply as bytes needs __host.callBytes, which this runtime plugin does not provide. Install a runtime plugin released with the matching SDK.';
|
|
14
15
|
/**
|
|
15
16
|
* Calls an action on the host and returns its response.
|
|
16
17
|
*
|
|
@@ -23,6 +24,40 @@ export function execute(actionName, params, context) {
|
|
|
23
24
|
void context;
|
|
24
25
|
return __host.call(actionName, params ?? null);
|
|
25
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Calls an action whose reply is a run of bytes, such as a range of a stored
|
|
29
|
+
* file, and returns them.
|
|
30
|
+
*
|
|
31
|
+
* The runtime hands the bytes over as a `Uint8Array` that owns them: no JSON,
|
|
32
|
+
* no base64, and no address. A host that refused answers with its ordinary
|
|
33
|
+
* envelope instead, returned here as a failed response, so a caller checks
|
|
34
|
+
* `ok` exactly as it does for `execute`. Its message names the call — `<action>
|
|
35
|
+
* failed: <the host's reason>` — worded as the Rust and Go SDKs word it.
|
|
36
|
+
*
|
|
37
|
+
* A runtime plugin released before this call existed does not provide it, and
|
|
38
|
+
* is refused here, before anything is sent, rather than handed a reply it
|
|
39
|
+
* would try to read as JSON.
|
|
40
|
+
*/
|
|
41
|
+
export function executeBytes(actionName, params, context) {
|
|
42
|
+
assertRuntimeAbi();
|
|
43
|
+
void context;
|
|
44
|
+
if (typeof __host.callBytes !== 'function') {
|
|
45
|
+
throw new TypeError(BYTES_ABI_MISSING_MESSAGE);
|
|
46
|
+
}
|
|
47
|
+
const reply = __host.callBytes(actionName, params ?? null);
|
|
48
|
+
if (reply instanceof Uint8Array) {
|
|
49
|
+
return { data: reply, ok: true };
|
|
50
|
+
}
|
|
51
|
+
// The runtime answers with bytes or with the host's refusal, and nothing
|
|
52
|
+
// else. A refusal that says it succeeded is still a refusal: no bytes came.
|
|
53
|
+
const refusal = reply;
|
|
54
|
+
if (refusal?.ok !== false)
|
|
55
|
+
return { error: { message: `${actionName} was refused and gave no reason.` }, ok: false };
|
|
56
|
+
const reason = refusal.error?.message;
|
|
57
|
+
if (typeof reason !== 'string' || reason.trim() === '')
|
|
58
|
+
return { error: { message: `${actionName} failed: The host refused the call and gave no reason.` }, ok: false };
|
|
59
|
+
return { error: { message: `${actionName} failed: ${reason}` }, ok: false };
|
|
60
|
+
}
|
|
26
61
|
/**
|
|
27
62
|
* Calls an action on the host without waiting for it to answer.
|
|
28
63
|
*/
|
package/dist/http.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ import type { Context } from './types';
|
|
|
4
4
|
*/
|
|
5
5
|
export interface HttpRequest {
|
|
6
6
|
body?: any;
|
|
7
|
+
/**
|
|
8
|
+
* The browser's credentials mode for the request: whether it carries the
|
|
9
|
+
* page's cookies and HTTP authentication. A request that names none sends
|
|
10
|
+
* none, and the browser host then omits credentials. The server host has no
|
|
11
|
+
* browser credentials and ignores it.
|
|
12
|
+
*/
|
|
13
|
+
credentials?: 'include' | 'omit' | 'same-origin';
|
|
7
14
|
headers?: Record<string, string>;
|
|
8
15
|
method?: 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT';
|
|
9
16
|
url: string;
|
package/dist/http.js
CHANGED
|
@@ -18,6 +18,7 @@ export async function fetch(request, context) {
|
|
|
18
18
|
}
|
|
19
19
|
const hostRequest = {
|
|
20
20
|
body: request.body ? JSON.stringify(request.body) : undefined,
|
|
21
|
+
credentials: request.credentials,
|
|
21
22
|
headers: request.headers,
|
|
22
23
|
method: request.method ?? 'GET',
|
|
23
24
|
url: request.url,
|
package/dist/index.js
CHANGED
|
@@ -88,12 +88,11 @@ async function handle(handler) {
|
|
|
88
88
|
// All other contexts (WASM Loader, Elixir Backend) are handled below.
|
|
89
89
|
let context;
|
|
90
90
|
try {
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
91
|
+
const simpleReq = readRequestFromHost();
|
|
92
|
+
if (simpleReq === undefined) {
|
|
93
93
|
returnError('no input payload provided by the host environment', undefined);
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
|
-
const simpleReq = JSON.parse(inputText);
|
|
97
96
|
context = simpleReq.context;
|
|
98
97
|
// Context 1: This is the initial WASM loader running in the browser.
|
|
99
98
|
// Its only job is to start the script worker.
|
|
@@ -141,8 +140,43 @@ function readInputFromHost() {
|
|
|
141
140
|
return JSON.stringify(globalThis.__SIMPLE_INITIAL_PAYLOAD__.request);
|
|
142
141
|
}
|
|
143
142
|
// Otherwise, we are in the main WASM module and must read from the host ABI.
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
// The runtime parses the context on its own side now, so what comes back is a
|
|
144
|
+
// value rather than bytes. Both branches of this function still answer with
|
|
145
|
+
// text because that is what its callers parse.
|
|
146
|
+
return JSON.stringify(host.getContext());
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Reads the request the host sent, or `undefined` when it sent none.
|
|
150
|
+
*
|
|
151
|
+
* Inside a WASM module the request is handed over as a value the runtime
|
|
152
|
+
* parsed from the host's own JSON, so it is read as it arrived. It used to be
|
|
153
|
+
* serialised and parsed again, which cost the whole payload twice over — the
|
|
154
|
+
* data is one string holding all of it, so ten megabytes were escaped into a
|
|
155
|
+
* new string and unescaped into another — and gave back what it started with,
|
|
156
|
+
* apart from the two values JSON cannot write: a negative zero came back as
|
|
157
|
+
* zero, and an infinite number as null. An action now sees what the host sent,
|
|
158
|
+
* those two included.
|
|
159
|
+
*
|
|
160
|
+
* Nothing here writes to the value. It belongs to the host, which stays free to
|
|
161
|
+
* hand over a frozen envelope, to answer `data` from a getter, or to read its
|
|
162
|
+
* own envelope back — as an action does when it calls `getContext` itself.
|
|
163
|
+
*
|
|
164
|
+
* The script worker in the browser reads a different thing. There the host
|
|
165
|
+
* leaves the request on the global as a JavaScript object of its own making,
|
|
166
|
+
* which reached the worker as a structured clone and can hold what JSON has no
|
|
167
|
+
* form for, such as a `Date` or an `undefined`. The round trip is what turns
|
|
168
|
+
* that into the request an action expects, so that path keeps it until a sweep
|
|
169
|
+
* of its own decides otherwise.
|
|
170
|
+
*/
|
|
171
|
+
function readRequestFromHost() {
|
|
172
|
+
if (globalThis.__SIMPLE_INITIAL_PAYLOAD__) {
|
|
173
|
+
const inputText = readInputFromHost();
|
|
174
|
+
return inputText ? JSON.parse(inputText) : undefined;
|
|
175
|
+
}
|
|
176
|
+
// `undefined` is what the runtime answers when the host sent no input at all,
|
|
177
|
+
// and it is the one thing a parsed JSON document can never be, so no request
|
|
178
|
+
// is mistaken for a missing one.
|
|
179
|
+
return host.getContext();
|
|
146
180
|
}
|
|
147
181
|
function returnError(message, context) {
|
|
148
182
|
const response = { data: null, errors: [message], ok: false };
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { Context, DocumentHandle, ExternalFileSource, StorageTarget } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The longest range the host answers one read with.
|
|
4
|
+
*
|
|
5
|
+
* `read` and `readRange` cover anything longer in ranges of this size, into one
|
|
6
|
+
* buffer, so it bounds what the host holds for one call rather than what an
|
|
7
|
+
* action can read.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_RANGE_BYTES: number;
|
|
2
10
|
/**
|
|
3
11
|
* Uploads an in-memory binary buffer as a document to the platform's storage system.
|
|
4
12
|
*
|
|
@@ -58,3 +66,71 @@ export declare function uploadBuffer(buffer: ArrayBuffer | Uint8Array, filename:
|
|
|
58
66
|
* ```
|
|
59
67
|
*/
|
|
60
68
|
export declare function uploadExternal(source: ExternalFileSource, target: StorageTarget, context: Context): Promise<DocumentHandle>;
|
|
69
|
+
/**
|
|
70
|
+
* Returns how many bytes a stored file holds, from the store's own record of it
|
|
71
|
+
* rather than the handle's `size`.
|
|
72
|
+
*
|
|
73
|
+
* @param handle The document handle, exactly as a `:document` field holds it.
|
|
74
|
+
* @param context The execution context for the request.
|
|
75
|
+
* @returns A promise that resolves with the file's size in bytes.
|
|
76
|
+
* @throws If the handle does not name a stored file, or the host refuses.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* const bytes = await size(record.attachment, request.context)
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function size(handle: DocumentHandle, context: Context): Promise<number>;
|
|
84
|
+
/**
|
|
85
|
+
* Returns the whole of a stored file, as its bytes.
|
|
86
|
+
*
|
|
87
|
+
* The size is asked for first, and the file arrives in ranges of at most
|
|
88
|
+
* `MAX_RANGE_BYTES`, each handed over by the host as it is — no JSON and no
|
|
89
|
+
* base64. A file that fits one range arrives as one array holding exactly its
|
|
90
|
+
* bytes. A larger one is read into a single buffer allocated once at exactly
|
|
91
|
+
* the file's size, and is refused before any range is read when this action
|
|
92
|
+
* has no memory for it. A range answered short — a file that changed while it
|
|
93
|
+
* was read — is refused rather than handed over incomplete.
|
|
94
|
+
*
|
|
95
|
+
* Reading is for server actions; a browser action is refused.
|
|
96
|
+
*
|
|
97
|
+
* @param handle The document handle, exactly as a `:document` field holds it.
|
|
98
|
+
* @param context The execution context for the request.
|
|
99
|
+
* @returns A promise that resolves with the file's bytes.
|
|
100
|
+
* @throws If the handle does not name a stored file, the host refuses, or the
|
|
101
|
+
* file changed while it was read.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* const bytes = await read(record.attachment, request.context)
|
|
106
|
+
* const text = new TextDecoder().decode(bytes)
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export declare function read(handle: DocumentHandle, context: Context): Promise<Uint8Array>;
|
|
110
|
+
/**
|
|
111
|
+
* Returns up to `length` bytes of a stored file, starting `offset` bytes in.
|
|
112
|
+
*
|
|
113
|
+
* The size is asked for first, so the range is held at what the file has past
|
|
114
|
+
* `offset`: one that runs past the end answers with exactly the bytes up to
|
|
115
|
+
* the end, and one that starts at or past the end is refused before any range
|
|
116
|
+
* is read. What is left is read as `read` reads a whole file — in ranges of at
|
|
117
|
+
* most `MAX_RANGE_BYTES`, into one buffer, each range answered in full or
|
|
118
|
+
* refused.
|
|
119
|
+
*
|
|
120
|
+
* Asking for the size first is also what makes a host that cannot read stored
|
|
121
|
+
* files refuse in its own words, before any range is asked of it.
|
|
122
|
+
*
|
|
123
|
+
* @param handle The document handle, exactly as a `:document` field holds it.
|
|
124
|
+
* @param offset How many bytes into the file the range starts, zero or more.
|
|
125
|
+
* @param length How many bytes to read, one or more.
|
|
126
|
+
* @param context The execution context for the request.
|
|
127
|
+
* @returns A promise that resolves with the range's bytes.
|
|
128
|
+
* @throws If the handle or the range is not valid, the range starts at or past
|
|
129
|
+
* the end of the file, or the host refuses.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* const head = await readRange(record.attachment, 0, 1024, request.context)
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export declare function readRange(handle: DocumentHandle, offset: number, length: number, context: Context): Promise<Uint8Array>;
|
package/dist/storage.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import { execute as hostExecute } from './host';
|
|
1
|
+
import { execute as hostExecute, executeBytes as hostExecuteBytes } from './host';
|
|
2
|
+
/** The host action that answers a stored file's size. */
|
|
3
|
+
const STAT = 'action:storage/stat';
|
|
4
|
+
/** The host action that answers one range of a stored file as its bytes. */
|
|
5
|
+
const READ = 'action:storage/read';
|
|
6
|
+
/**
|
|
7
|
+
* The longest range the host answers one read with.
|
|
8
|
+
*
|
|
9
|
+
* `read` and `readRange` cover anything longer in ranges of this size, into one
|
|
10
|
+
* buffer, so it bounds what the host holds for one call rather than what an
|
|
11
|
+
* action can read.
|
|
12
|
+
*/
|
|
13
|
+
export const MAX_RANGE_BYTES = 16 * 1024 * 1024;
|
|
2
14
|
/**
|
|
3
15
|
* Uploads an in-memory binary buffer as a document to the platform's storage system.
|
|
4
16
|
*
|
|
@@ -115,3 +127,142 @@ export async function uploadExternal(source, target, context) {
|
|
|
115
127
|
}
|
|
116
128
|
return response.data;
|
|
117
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Returns how many bytes a stored file holds, from the store's own record of it
|
|
132
|
+
* rather than the handle's `size`.
|
|
133
|
+
*
|
|
134
|
+
* @param handle The document handle, exactly as a `:document` field holds it.
|
|
135
|
+
* @param context The execution context for the request.
|
|
136
|
+
* @returns A promise that resolves with the file's size in bytes.
|
|
137
|
+
* @throws If the handle does not name a stored file, or the host refuses.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```typescript
|
|
141
|
+
* const bytes = await size(record.attachment, request.context)
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export async function size(handle, context) {
|
|
145
|
+
checkHandle(handle);
|
|
146
|
+
const response = await hostExecute(STAT, { handle }, context);
|
|
147
|
+
if (!response.ok)
|
|
148
|
+
throw refused(STAT, response.error?.message);
|
|
149
|
+
const answered = response.data?.size;
|
|
150
|
+
if (typeof answered !== 'number' || !Number.isSafeInteger(answered) || answered < 0)
|
|
151
|
+
throw new Error(`${STAT} answered without a size: ${JSON.stringify(response.data ?? null)}`);
|
|
152
|
+
return answered;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Returns the whole of a stored file, as its bytes.
|
|
156
|
+
*
|
|
157
|
+
* The size is asked for first, and the file arrives in ranges of at most
|
|
158
|
+
* `MAX_RANGE_BYTES`, each handed over by the host as it is — no JSON and no
|
|
159
|
+
* base64. A file that fits one range arrives as one array holding exactly its
|
|
160
|
+
* bytes. A larger one is read into a single buffer allocated once at exactly
|
|
161
|
+
* the file's size, and is refused before any range is read when this action
|
|
162
|
+
* has no memory for it. A range answered short — a file that changed while it
|
|
163
|
+
* was read — is refused rather than handed over incomplete.
|
|
164
|
+
*
|
|
165
|
+
* Reading is for server actions; a browser action is refused.
|
|
166
|
+
*
|
|
167
|
+
* @param handle The document handle, exactly as a `:document` field holds it.
|
|
168
|
+
* @param context The execution context for the request.
|
|
169
|
+
* @returns A promise that resolves with the file's bytes.
|
|
170
|
+
* @throws If the handle does not name a stored file, the host refuses, or the
|
|
171
|
+
* file changed while it was read.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* const bytes = await read(record.attachment, request.context)
|
|
176
|
+
* const text = new TextDecoder().decode(bytes)
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export async function read(handle, context) {
|
|
180
|
+
const total = await size(handle, context);
|
|
181
|
+
return total === 0 ? new Uint8Array(0) : readSpan(handle, 0, total, context);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Returns up to `length` bytes of a stored file, starting `offset` bytes in.
|
|
185
|
+
*
|
|
186
|
+
* The size is asked for first, so the range is held at what the file has past
|
|
187
|
+
* `offset`: one that runs past the end answers with exactly the bytes up to
|
|
188
|
+
* the end, and one that starts at or past the end is refused before any range
|
|
189
|
+
* is read. What is left is read as `read` reads a whole file — in ranges of at
|
|
190
|
+
* most `MAX_RANGE_BYTES`, into one buffer, each range answered in full or
|
|
191
|
+
* refused.
|
|
192
|
+
*
|
|
193
|
+
* Asking for the size first is also what makes a host that cannot read stored
|
|
194
|
+
* files refuse in its own words, before any range is asked of it.
|
|
195
|
+
*
|
|
196
|
+
* @param handle The document handle, exactly as a `:document` field holds it.
|
|
197
|
+
* @param offset How many bytes into the file the range starts, zero or more.
|
|
198
|
+
* @param length How many bytes to read, one or more.
|
|
199
|
+
* @param context The execution context for the request.
|
|
200
|
+
* @returns A promise that resolves with the range's bytes.
|
|
201
|
+
* @throws If the handle or the range is not valid, the range starts at or past
|
|
202
|
+
* the end of the file, or the host refuses.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const head = await readRange(record.attachment, 0, 1024, request.context)
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export async function readRange(handle, offset, length, context) {
|
|
210
|
+
checkHandle(handle);
|
|
211
|
+
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
212
|
+
throw new Error('A range needs an offset that is a whole number of bytes, zero or more.');
|
|
213
|
+
if (!Number.isSafeInteger(length) || length < 1)
|
|
214
|
+
throw new Error('A range needs at least one byte. Pass a length of one or more, or ask size() how large the file is.');
|
|
215
|
+
const total = await size(handle, context);
|
|
216
|
+
if (offset >= total)
|
|
217
|
+
throw new Error(`The range starts at byte ${offset}, at or past the end of the file, which is ${total} bytes. Ask size() how large the file is, and start the range before its end.`);
|
|
218
|
+
return readSpan(handle, offset, Math.min(length, total - offset), context);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Exactly `length` bytes from `offset`, which the file's size says are there.
|
|
222
|
+
*
|
|
223
|
+
* One range is returned as the host handed it over. More are read into one
|
|
224
|
+
* buffer, allocated once at exactly `length` before any of them is asked for.
|
|
225
|
+
*/
|
|
226
|
+
async function readSpan(handle, offset, length, context) {
|
|
227
|
+
if (length <= MAX_RANGE_BYTES)
|
|
228
|
+
return readExactly(handle, offset, length, context);
|
|
229
|
+
const bytes = allocate(length);
|
|
230
|
+
for (let done = 0; done < length; done += MAX_RANGE_BYTES) {
|
|
231
|
+
const part = await readExactly(handle, offset + done, Math.min(MAX_RANGE_BYTES, length - done), context);
|
|
232
|
+
bytes.set(part, done);
|
|
233
|
+
}
|
|
234
|
+
return bytes;
|
|
235
|
+
}
|
|
236
|
+
/** One range, which must be answered in full. */
|
|
237
|
+
async function readExactly(handle, offset, length, context) {
|
|
238
|
+
const response = await hostExecuteBytes(READ, { handle, length, offset }, context);
|
|
239
|
+
if (!response.ok)
|
|
240
|
+
throw new Error(response.error?.message ?? `${READ} failed: The host refused the call and gave no reason.`);
|
|
241
|
+
const part = response.data;
|
|
242
|
+
if (part.length !== length)
|
|
243
|
+
throw new Error(`The file answered ${part.length} bytes for the ${length} at offset ${offset}, so it is not the size it was when the read began. Read it again.`);
|
|
244
|
+
return part;
|
|
245
|
+
}
|
|
246
|
+
/** The error for a call the host refused in its envelope, naming the call. */
|
|
247
|
+
function refused(actionName, message) {
|
|
248
|
+
if (message === undefined || message.trim() === '')
|
|
249
|
+
return new Error(`${actionName} failed: The host refused the call and gave no reason.`);
|
|
250
|
+
return new Error(`${actionName} failed: ${message}`);
|
|
251
|
+
}
|
|
252
|
+
/** A buffer of exactly `length` bytes, or a refusal saying there is no room. */
|
|
253
|
+
function allocate(length) {
|
|
254
|
+
try {
|
|
255
|
+
return new Uint8Array(length);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
throw new Error(`Reading ${length} bytes needs more memory than this action has. Raise the action's mem_limit, or read the file in parts with readRange.`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Whether a handle carries what the host locates a stored file by. */
|
|
262
|
+
function checkHandle(handle) {
|
|
263
|
+
for (const member of ['storage_path', 'filename', 'file_hash']) {
|
|
264
|
+
const value = handle?.[member];
|
|
265
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
266
|
+
throw new Error(`A document handle needs ${member}. Pass the handle exactly as the :document field holds it.`);
|
|
267
|
+
}
|
|
268
|
+
}
|
package/dist/worker-override.js
CHANGED
|
@@ -56,6 +56,18 @@ export function execute(actionName, params, context) {
|
|
|
56
56
|
})
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* A reply that is a run of bytes reaches only a server action: the browser
|
|
61
|
+
* runtime has no way to hand one over. Refused here, before anything is sent.
|
|
62
|
+
* @returns {Promise<import('./types').SimpleResponse<Uint8Array>>} A promise that rejects
|
|
63
|
+
*/
|
|
64
|
+
export function executeBytes(actionName) {
|
|
65
|
+
return Promise.reject(new Error(
|
|
66
|
+
`${actionName} answers with bytes, which only a server action can receive. `
|
|
67
|
+
+ 'Set the action\'s execution environment to server.',
|
|
68
|
+
))
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
/**
|
|
60
72
|
* Worker-compatible fire-and-forget implementation.
|
|
61
73
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simpleplatform/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Simple Platform Typescript SDK",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://docs.simple.dev",
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
"types": "./dist/security.d.ts",
|
|
46
46
|
"default": "./dist/security.js"
|
|
47
47
|
},
|
|
48
|
+
"./storage": {
|
|
49
|
+
"types": "./dist/storage.d.ts",
|
|
50
|
+
"default": "./dist/storage.js"
|
|
51
|
+
},
|
|
48
52
|
"./host": {
|
|
49
53
|
"types": "./dist/host.d.ts",
|
|
50
54
|
"default": "./dist/host.js"
|