@simpleplatform/sdk 2.2.0 → 2.4.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 +228 -1
- package/dist/ai.d.ts +138 -2
- package/dist/ai.js +88 -0
- package/dist/space/core.d.ts +99 -3
- package/dist/space/core.js +211 -1
- package/dist/space/index.d.ts +5 -4
- package/dist/space/index.js +14 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ 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 reading a stored file's bytes |
|
|
43
|
-
| **Space** | `@simpleplatform/sdk/space` |
|
|
43
|
+
| **Space** | `@simpleplatform/sdk/space` | Records, data, tasks, and documents in a Space |
|
|
44
44
|
|
|
45
45
|
## Embedded Spaces
|
|
46
46
|
|
|
@@ -100,6 +100,10 @@ and tools. In a non-record Space, `simple.data` remains available while
|
|
|
100
100
|
`simple.records.current()` rejects with `SpaceProtocolError` code `unavailable`
|
|
101
101
|
and explains that the Space must be configured as a record view.
|
|
102
102
|
|
|
103
|
+
Each capability beyond `simple.data` is negotiated with the host when the Space
|
|
104
|
+
connects. A capability the host did not negotiate rejects with
|
|
105
|
+
`SpaceProtocolError` code `unavailable` when it is called, and sends nothing.
|
|
106
|
+
|
|
103
107
|
### Space data access
|
|
104
108
|
|
|
105
109
|
Use `simple.data` for application data that is not the record form currently
|
|
@@ -131,6 +135,175 @@ Those commands preserve Record Behaviors, validation, documents, and the shared
|
|
|
131
135
|
record state used by the platform header. `simple.data.mutate()` is for other
|
|
132
136
|
authorized application data; it must not be used to bypass a record workflow.
|
|
133
137
|
|
|
138
|
+
### Space tasks
|
|
139
|
+
|
|
140
|
+
`simple.tasks` creates a task from a task type and replies on a task. Tasks are
|
|
141
|
+
not the page's record, so they work in standalone and record Spaces alike.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const { task } = await simple.tasks.create({
|
|
145
|
+
assignedToId: 'USR000005', // Optional: defaults to the user creating the task.
|
|
146
|
+
input: { packet: 'DOC000001' }, // The typed input the task type declares.
|
|
147
|
+
taskTypeId: 'TTY000003',
|
|
148
|
+
title: 'Review the contract packet',
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const { messageId, taskRevision } = await simple.tasks.reply({
|
|
152
|
+
content: 'The revised drawing is attached.',
|
|
153
|
+
inReplyToMessageId: 'MSG000006', // Optional: the message this reply answers.
|
|
154
|
+
taskId: task.id,
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`create()` returns `{ task: { id, status, revision } }`, where `status` is one
|
|
159
|
+
of `queued`, `in_progress`, `waiting`, `completed`, `cancelled`, or `failed`.
|
|
160
|
+
`reply()` returns the new message's ID and the task's revision after the reply.
|
|
161
|
+
|
|
162
|
+
`input` is always a JSON object; pass `{}` when the task type needs no input.
|
|
163
|
+
Everything inside it must be a JSON value (plain objects, arrays, strings,
|
|
164
|
+
finite numbers, booleans, and `null`) so it means the same thing on every
|
|
165
|
+
transport. `null`, an array, or a single value as the input itself is refused.
|
|
166
|
+
The host holds a task type's typed input to 32,768 encoded bytes and 64 levels
|
|
167
|
+
of nesting.
|
|
168
|
+
|
|
169
|
+
`assignedToId`, when given, must name an existing user in the tenant; left out,
|
|
170
|
+
the task is assigned to the user creating it.
|
|
171
|
+
|
|
172
|
+
A request the SDK can tell is incomplete is refused before it is sent, with
|
|
173
|
+
`SpaceProtocolError` code `invalid_request`.
|
|
174
|
+
|
|
175
|
+
When the task service refuses a create or a reply, the call rejects with
|
|
176
|
+
`SpaceProtocolError` code `task_rejected`, whatever the reason. Its `message` is
|
|
177
|
+
the service's message, and its `details` is the service's error exactly as sent:
|
|
178
|
+
`{ code, category, message, pointers, details }`. Neither the host nor the SDK
|
|
179
|
+
adds, drops, or renames anything in it. The reason is `details.code`, not
|
|
180
|
+
`error.code`:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { SpaceProtocolError } from '@simpleplatform/sdk/space'
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await simple.tasks.create({ input: { amount: 700 }, taskTypeId: 'TTY000003', title: 'Open the job' })
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (!(error instanceof SpaceProtocolError) || error.code !== 'task_rejected')
|
|
190
|
+
throw error
|
|
191
|
+
|
|
192
|
+
const refusal = error.details as { category: string, code: string, details: unknown, pointers: string[] }
|
|
193
|
+
// refusal.category says whether to correct the request or send it again,
|
|
194
|
+
// refusal.code says why, and refusal.pointers says where.
|
|
195
|
+
console.warn(refusal.category, refusal.code, refusal.pointers)
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`details.category` says whether to correct the request or send it again:
|
|
200
|
+
|
|
201
|
+
- `validation` is final. The request is wrong, and the host keeps nothing of
|
|
202
|
+
the refused create, so correct the request before sending it again.
|
|
203
|
+
- `runtime` means the task service did not answer, not that the request is
|
|
204
|
+
wrong. Its codes are `TASK_RUNTIME_UNAVAILABLE`, `TASK_RUNTIME_TIMEOUT`,
|
|
205
|
+
`TASK_RUNTIME_BUSY`, and `TASK_RUNTIME_REMOTE_FAILURE`. The host keeps the
|
|
206
|
+
pending create, so calling `create()` again with the same arguments resends
|
|
207
|
+
that create under the same task id, and a create that did land is not made
|
|
208
|
+
twice.
|
|
209
|
+
|
|
210
|
+
A create's title, input, and assignee are refused with these `validation`
|
|
211
|
+
codes:
|
|
212
|
+
|
|
213
|
+
| `details.code` | When | `details.pointers` | `details.details` |
|
|
214
|
+
| ----------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------- |
|
|
215
|
+
| `TASK_INPUT_INVALID` | The input fails its task type's input schema | Each issue's `instance_pointer` under `/input`, once each | `{ errors, truncated }` |
|
|
216
|
+
| `TASK_INPUT_INVALID` | The title is longer than 255 characters, or the input is nested deeper than 64 levels | `/title` or `/input` | `{}` |
|
|
217
|
+
| `TASK_INPUT_TOO_LARGE` | The input encodes to more than 32,768 bytes | `/input` | `{ measured_bytes, allowed_bytes }` |
|
|
218
|
+
| `TASK_ASSIGNEE_INVALID` | `assignedToId` is not a user in the tenant | `/assigned_to_id` | `{}` |
|
|
219
|
+
|
|
220
|
+
For a schema failure, `errors` holds at most 50 issues sorted by
|
|
221
|
+
`instance_pointer`, and `truncated` is `true` when there were more. Each issue
|
|
222
|
+
is exactly `{ code, instance_pointer, schema_pointer }` and never carries the
|
|
223
|
+
value that failed. `instance_pointer` is relative to the input, while
|
|
224
|
+
`pointers` carries the `/input` prefix. `schema_pointer` is the location in the
|
|
225
|
+
task type's input schema of the object holding the keyword that failed.
|
|
226
|
+
|
|
227
|
+
For example, a task type whose input schema is
|
|
228
|
+
`{ type: 'object', properties: { amount: { type: 'integer' } }, required: ['job_id'], additionalProperties: false }`
|
|
229
|
+
refuses the input `{ amount: 'seven hundred', note: 'a private note' }` with
|
|
230
|
+
this `error.details`:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"code": "TASK_INPUT_INVALID",
|
|
235
|
+
"category": "validation",
|
|
236
|
+
"message": "The task input is invalid.",
|
|
237
|
+
"pointers": ["/input", "/input/amount", "/input/note"],
|
|
238
|
+
"details": {
|
|
239
|
+
"errors": [
|
|
240
|
+
{ "code": "required", "instance_pointer": "", "schema_pointer": "" },
|
|
241
|
+
{ "code": "type", "instance_pointer": "/amount", "schema_pointer": "/properties/amount" },
|
|
242
|
+
{ "code": "boolean_schema", "instance_pointer": "/note", "schema_pointer": "/additionalProperties" }
|
|
243
|
+
],
|
|
244
|
+
"truncated": false
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
A `required` issue points at the object that lacks the member, not at the
|
|
250
|
+
member: above it sits at `/input` and does not name `job_id`.
|
|
251
|
+
|
|
252
|
+
The input `{ notes }`, where `notes` holds 32,769 characters and the input
|
|
253
|
+
encodes to 32,781 bytes, is refused with:
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"code": "TASK_INPUT_TOO_LARGE",
|
|
258
|
+
"category": "validation",
|
|
259
|
+
"message": "The task input is too large. Shorten the request or split the work into more than one task.",
|
|
260
|
+
"pointers": ["/input"],
|
|
261
|
+
"details": { "measured_bytes": 32781, "allowed_bytes": 32768 }
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
An assignee who is not a user in the tenant is refused with:
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"code": "TASK_ASSIGNEE_INVALID",
|
|
270
|
+
"category": "validation",
|
|
271
|
+
"message": "The task assignee is invalid.",
|
|
272
|
+
"pointers": ["/assigned_to_id"],
|
|
273
|
+
"details": {}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Pointers name the members of the task service's command, not the SDK's
|
|
278
|
+
arguments, so the assignee is `/assigned_to_id` rather than `assignedToId`. A
|
|
279
|
+
refused `reply()` arrives the same way, with the service's own code in
|
|
280
|
+
`details.code` and the same categories: after a `runtime` refusal, calling
|
|
281
|
+
`reply()` again with the same arguments resends the kept message rather than
|
|
282
|
+
posting it twice.
|
|
283
|
+
|
|
284
|
+
### Space documents
|
|
285
|
+
|
|
286
|
+
`simple.documents.stage()` stores a file without attaching it to any record and
|
|
287
|
+
returns its handle. The host uploads it, so a large file never travels inside
|
|
288
|
+
an action request. The file's bytes are transferred to the host, not copied.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const picker = document.querySelector<HTMLInputElement>('#packet')!
|
|
292
|
+
const { handle } = await simple.documents.stage({ file: picker.files![0] })
|
|
293
|
+
|
|
294
|
+
// A plain Blob has no name of its own, so it needs one.
|
|
295
|
+
await simple.documents.stage({
|
|
296
|
+
file: new Blob([csv]),
|
|
297
|
+
mimeType: 'text/csv',
|
|
298
|
+
name: 'quantities.csv',
|
|
299
|
+
})
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
The handle is `{ file_hash, filename, mime_type, size, storage_path, scope? }`.
|
|
303
|
+
`name` defaults to a `File`'s own name, and `mimeType` to the file's type, then
|
|
304
|
+
to `application/octet-stream`. A staged document is not attached to anything
|
|
305
|
+
yet; attach its handle through the workflow that owns the record.
|
|
306
|
+
|
|
134
307
|
---
|
|
135
308
|
|
|
136
309
|
## API Documentation
|
|
@@ -209,6 +382,60 @@ console.log(result.data.summary) // "Customer called regarding..."
|
|
|
209
382
|
console.log(result.data.participants) // ["Customer", "Support Agent"]
|
|
210
383
|
```
|
|
211
384
|
|
|
385
|
+
#### Transcribe PDF Pages
|
|
386
|
+
|
|
387
|
+
Read the pages of a PDF that have no usable text of their own — scans,
|
|
388
|
+
image-only exhibits — from their images. Each such page is transcribed once,
|
|
389
|
+
and it is the same transcription an `extract` or `summarize` that asks for text
|
|
390
|
+
is given in the page's place, so a quote on an image page can be checked
|
|
391
|
+
against the text the answer was built on. Pages with a readable text layer are
|
|
392
|
+
not returned.
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
import { transcribePages } from '@simpleplatform/sdk/ai'
|
|
396
|
+
|
|
397
|
+
const { data } = await transcribePages(
|
|
398
|
+
{ ...contract, first_page: 40, last_page: 52 },
|
|
399
|
+
{},
|
|
400
|
+
request.context
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
for (const page of data.pages) {
|
|
404
|
+
if ('error' in page) {
|
|
405
|
+
console.log(`page ${page.page} could not be read: ${page.error}`)
|
|
406
|
+
continue
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
console.log(page.page, page.text.includes(quote))
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Pages are numbered in the original document. Transcriptions are kept per
|
|
414
|
+
version of the file and page, so a page already read for an `extract` or
|
|
415
|
+
`summarize` that asked for text (`deliver_as: 'text'`) is not read again.
|
|
416
|
+
|
|
417
|
+
A page is not transcribed twice to check itself: that would be the same model
|
|
418
|
+
reading the same image again, doubling the cost of every scanned page without
|
|
419
|
+
adding independence. To check an answer independently, read the pages a second
|
|
420
|
+
way — as the document itself (`deliver_as: 'document'`) — and compare.
|
|
421
|
+
|
|
422
|
+
#### How Files Travelled
|
|
423
|
+
|
|
424
|
+
Every AI result says how each file it carried reached the model, in
|
|
425
|
+
`metadata.delivery`: `deliveredAs` (`'document'`, `'text'` or `'image'`), the
|
|
426
|
+
range it was cut to, the pages transcribed from their images, and — when text
|
|
427
|
+
was asked for and the document was sent instead — a `fallback` naming the pages
|
|
428
|
+
that could not be read and why.
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
const result = await extract({ ...contract, deliver_as: 'text' }, { prompt, schema }, request.context)
|
|
432
|
+
|
|
433
|
+
for (const file of result.metadata.delivery ?? []) {
|
|
434
|
+
if (file.fallback)
|
|
435
|
+
console.log(`${file.filename} was read as a PDF: ${file.fallback.message}`)
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
212
439
|
### GraphQL Module
|
|
213
440
|
|
|
214
441
|
Execute type-safe database operations with GraphQL:
|
package/dist/ai.d.ts
CHANGED
|
@@ -88,8 +88,12 @@ export type JSONSchema = JSONSchemaArray | JSONSchemaBoolean | JSONSchemaNumber
|
|
|
88
88
|
* file,
|
|
89
89
|
* which only ever travels as text.
|
|
90
90
|
*
|
|
91
|
-
* A page
|
|
92
|
-
*
|
|
91
|
+
* A page with no usable text of its own — a scan, or text that reads as noise
|
|
92
|
+
* — is read from its image instead, and that text stands in its place, marked
|
|
93
|
+
* `[Transcribed from the page image.]` under its page label. Only if that
|
|
94
|
+
* reading fails are the pages asked for sent as a PDF, so an answer is never
|
|
95
|
+
* built on text with a hole in it. The answer's `metadata.delivery` says which
|
|
96
|
+
* happened.
|
|
93
97
|
*
|
|
94
98
|
* Pages asked for as text arrive numbered from 1, because by then they are a
|
|
95
99
|
* document of their own. A line above them says which pages of which document
|
|
@@ -203,6 +207,62 @@ export interface AITranscribeOptions extends Omit<AICommonOptions, 'prompt'> {
|
|
|
203
207
|
*/
|
|
204
208
|
summarize?: boolean;
|
|
205
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* A PDF whose pages are to be transcribed, and optionally which of them.
|
|
212
|
+
*
|
|
213
|
+
* `first_page` and `last_page` work as they do for any AI operation: counted
|
|
214
|
+
* from 1, both ends included, named together or not at all, and cut out first.
|
|
215
|
+
* `deliver_as` has no meaning here, because the pages are answered as text.
|
|
216
|
+
*/
|
|
217
|
+
export type AITranscribePagesInput = DocumentHandle & Omit<AIDocumentInput, 'deliver_as' | keyof DocumentHandle>;
|
|
218
|
+
/**
|
|
219
|
+
* Configuration options for `transcribePages`. There is no prompt and no
|
|
220
|
+
* reasoning switch: the instructions are the platform's, and every page is
|
|
221
|
+
* read at the platform's own accuracy-first effort.
|
|
222
|
+
*/
|
|
223
|
+
export interface AITranscribePagesOptions {
|
|
224
|
+
/**
|
|
225
|
+
* If true, runs the operation again rather than serving its kept answer.
|
|
226
|
+
* A page already read is still served from its kept read, which is tied to
|
|
227
|
+
* this version of the file and to the platform's instructions.
|
|
228
|
+
*/
|
|
229
|
+
regenerate?: boolean;
|
|
230
|
+
/** (Optional) The maximum time in milliseconds to wait for the operation. */
|
|
231
|
+
timeout?: number;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* One page's transcription, or why it has none. Pages are numbered in the
|
|
235
|
+
* original document.
|
|
236
|
+
*/
|
|
237
|
+
export type AIPageTranscription = {
|
|
238
|
+
/** Why the page could not be read. No partial text is given. */
|
|
239
|
+
error: string;
|
|
240
|
+
/** The page's number in the original document. */
|
|
241
|
+
page: number;
|
|
242
|
+
} | {
|
|
243
|
+
/** The page's number in the original document. */
|
|
244
|
+
page: number;
|
|
245
|
+
/**
|
|
246
|
+
* The page's text as read from its image: verbatim markdown, in reading
|
|
247
|
+
* order, tables as markdown tables, `[illegible]` where a word could not
|
|
248
|
+
* be read. An empty string is a read that found the page blank.
|
|
249
|
+
*/
|
|
250
|
+
text: string;
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* The response of `transcribePages`.
|
|
254
|
+
*/
|
|
255
|
+
export interface AITranscriptionResult {
|
|
256
|
+
/** Every page of the document, or of the range, that has no usable text layer. */
|
|
257
|
+
data: {
|
|
258
|
+
pages: AIPageTranscription[];
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* The operation's metadata. Its token counts are zero: each page read is
|
|
262
|
+
* charged where it was made, once, whichever operation asked for it.
|
|
263
|
+
*/
|
|
264
|
+
metadata: AIExecutionResult['metadata'];
|
|
265
|
+
}
|
|
206
266
|
/**
|
|
207
267
|
* Options for searching faces within the Face Engine.
|
|
208
268
|
*/
|
|
@@ -218,6 +278,34 @@ export interface AIFaceSearchOptions {
|
|
|
218
278
|
*/
|
|
219
279
|
similarityThreshold?: number;
|
|
220
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* How one file an AI operation carried reached the model. Page numbers are
|
|
283
|
+
* the original document's, whatever range was cut from it.
|
|
284
|
+
*/
|
|
285
|
+
export interface AIFileDelivery {
|
|
286
|
+
/** How the file travelled: as the document itself, as its text, or as an image. */
|
|
287
|
+
deliveredAs: 'document' | 'image' | 'text';
|
|
288
|
+
/**
|
|
289
|
+
* Present only when text was asked for and the document was sent instead:
|
|
290
|
+
* the pages whose images could not be read, and why.
|
|
291
|
+
*/
|
|
292
|
+
fallback?: {
|
|
293
|
+
/** What went wrong, page by page, in the platform's words. */
|
|
294
|
+
message: string;
|
|
295
|
+
/** The pages that could not be transcribed. */
|
|
296
|
+
pages: number[];
|
|
297
|
+
/** Why the document travelled instead of its text. */
|
|
298
|
+
reason: 'transcription_failed';
|
|
299
|
+
};
|
|
300
|
+
/** The file's name. */
|
|
301
|
+
filename: string;
|
|
302
|
+
/** The first page of the range the file was cut to, when one was named. */
|
|
303
|
+
firstPage?: number;
|
|
304
|
+
/** The last page of the range the file was cut to, when one was named. */
|
|
305
|
+
lastPage?: number;
|
|
306
|
+
/** The pages whose text was read from their images rather than their text layer. */
|
|
307
|
+
transcribedPages: number[];
|
|
308
|
+
}
|
|
221
309
|
/**
|
|
222
310
|
* The response structure from a successful AI `extract` or `summarize` operation.
|
|
223
311
|
*/
|
|
@@ -232,6 +320,12 @@ export interface AIExecutionResult {
|
|
|
232
320
|
* providing context to the user.
|
|
233
321
|
*/
|
|
234
322
|
metadata: {
|
|
323
|
+
/**
|
|
324
|
+
* How each file the operation carried reached the model, in input order.
|
|
325
|
+
* Absent when the operation carried no file, and on an answer kept from
|
|
326
|
+
* before the platform reported it.
|
|
327
|
+
*/
|
|
328
|
+
delivery?: AIFileDelivery[];
|
|
235
329
|
/** The number of tokens in the input prompt. */
|
|
236
330
|
inputTokens: number;
|
|
237
331
|
/** The number of tokens in the generated output. */
|
|
@@ -301,6 +395,48 @@ export declare function summarize(input: AIDocumentInput | DocumentHandle | obje
|
|
|
301
395
|
* // Returns: { language: "en", summary: "...", participants: ["Participant 1", "Participant 2"] }
|
|
302
396
|
*/
|
|
303
397
|
export declare function transcribe(input: DocumentHandle, options: AITranscribeOptions, context: Context): Promise<AIExecutionResult>;
|
|
398
|
+
/**
|
|
399
|
+
* Transcribes the pages of a PDF that have no usable text of their own — scans,
|
|
400
|
+
* image-only exhibits, text that reads as noise — from their images.
|
|
401
|
+
*
|
|
402
|
+
* Every such page is read once, and it is the same transcription an `extract`
|
|
403
|
+
* or `summarize` that asked for text is given in the page's place, so a caller
|
|
404
|
+
* can check that a quote on an image page is really there by looking for it in
|
|
405
|
+
* the text the answer was built on. Pages the platform reads as text are
|
|
406
|
+
* neither read nor returned.
|
|
407
|
+
*
|
|
408
|
+
* A page is not transcribed a second time to check the first: that would be
|
|
409
|
+
* the same model reading the same image again, doubling the cost of every
|
|
410
|
+
* scanned page without being independent of the first read. An independent
|
|
411
|
+
* check reads the pages a second way — as the document itself
|
|
412
|
+
* (`deliver_as: 'document'`) — and compares the answers.
|
|
413
|
+
*
|
|
414
|
+
* Transcriptions are kept per version of the file and page, and shared with
|
|
415
|
+
* every other AI operation: a page already read for an `extract` or
|
|
416
|
+
* `summarize` that asked for text is not read again.
|
|
417
|
+
*
|
|
418
|
+
* @param document The PDF, as a DocumentHandle. Add `first_page` and
|
|
419
|
+
* `last_page` to transcribe only those pages; they are counted from 1 and
|
|
420
|
+
* the answer numbers pages in the original document.
|
|
421
|
+
* @param options Optional `regenerate` and `timeout`.
|
|
422
|
+
* @param context The execution context provided by the host.
|
|
423
|
+
* @returns A promise that resolves to each page's transcription, or why it has none.
|
|
424
|
+
* @throws Will throw an error if the operation fails or the input is not a PDF.
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* const { data } = await transcribePages(
|
|
428
|
+
* { ...contract, first_page: 40, last_page: 52 },
|
|
429
|
+
* {},
|
|
430
|
+
* context,
|
|
431
|
+
* )
|
|
432
|
+
*
|
|
433
|
+
* for (const page of data.pages) {
|
|
434
|
+
* if ('error' in page)
|
|
435
|
+
* continue
|
|
436
|
+
* const quoted = page.text.includes(quote)
|
|
437
|
+
* }
|
|
438
|
+
*/
|
|
439
|
+
export declare function transcribePages(document: AITranscribePagesInput, options: AITranscribePagesOptions, context: Context): Promise<AITranscriptionResult>;
|
|
304
440
|
/**
|
|
305
441
|
* Enrolls a face for a subject. The face is associated with the given `subjectId`
|
|
306
442
|
* within the tenant's secure collection.
|
package/dist/ai.js
CHANGED
|
@@ -85,6 +85,7 @@ async function _executeAIOperation(operation, input, options, context) {
|
|
|
85
85
|
return {
|
|
86
86
|
data,
|
|
87
87
|
metadata: {
|
|
88
|
+
...(Array.isArray(metadata.delivery) && { delivery: metadata.delivery.map(_fileDelivery) }),
|
|
88
89
|
inputTokens: metadata.input_tokens,
|
|
89
90
|
outputTokens: metadata.output_tokens,
|
|
90
91
|
reasoning: metadata.reasoning,
|
|
@@ -92,6 +93,38 @@ async function _executeAIOperation(operation, input, options, context) {
|
|
|
92
93
|
},
|
|
93
94
|
};
|
|
94
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* One file's delivery as the platform reports it, in the SDK's own casing.
|
|
98
|
+
* A key the platform sends as `null` is left out rather than carried as one.
|
|
99
|
+
*
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
function _fileDelivery(file) {
|
|
103
|
+
return {
|
|
104
|
+
deliveredAs: file.delivered_as,
|
|
105
|
+
...(file.fallback && { fallback: file.fallback }),
|
|
106
|
+
filename: file.filename,
|
|
107
|
+
...(typeof file.first_page === 'number' && { firstPage: file.first_page }),
|
|
108
|
+
...(typeof file.last_page === 'number' && { lastPage: file.last_page }),
|
|
109
|
+
transcribedPages: Array.isArray(file.transcribed_pages) ? file.transcribed_pages : [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* One page's transcription as the platform reports it: its text or its error,
|
|
114
|
+
* never both. An error wins, so no partial text is handed on, and a page that
|
|
115
|
+
* carries no text is reported as unread rather than as text that is not there.
|
|
116
|
+
*
|
|
117
|
+
* @internal
|
|
118
|
+
*/
|
|
119
|
+
function _pageTranscription(page) {
|
|
120
|
+
if (typeof page.text === 'string' && typeof page.error !== 'string') {
|
|
121
|
+
return { page: page.page, text: page.text };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
error: typeof page.error === 'string' ? page.error : 'no transcription was returned for this page',
|
|
125
|
+
page: page.page,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
95
128
|
// ============================================================================
|
|
96
129
|
// Public SDK Functions
|
|
97
130
|
// ============================================================================
|
|
@@ -284,6 +317,61 @@ export async function transcribe(input, options, context) {
|
|
|
284
317
|
schema,
|
|
285
318
|
}, context);
|
|
286
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Transcribes the pages of a PDF that have no usable text of their own — scans,
|
|
322
|
+
* image-only exhibits, text that reads as noise — from their images.
|
|
323
|
+
*
|
|
324
|
+
* Every such page is read once, and it is the same transcription an `extract`
|
|
325
|
+
* or `summarize` that asked for text is given in the page's place, so a caller
|
|
326
|
+
* can check that a quote on an image page is really there by looking for it in
|
|
327
|
+
* the text the answer was built on. Pages the platform reads as text are
|
|
328
|
+
* neither read nor returned.
|
|
329
|
+
*
|
|
330
|
+
* A page is not transcribed a second time to check the first: that would be
|
|
331
|
+
* the same model reading the same image again, doubling the cost of every
|
|
332
|
+
* scanned page without being independent of the first read. An independent
|
|
333
|
+
* check reads the pages a second way — as the document itself
|
|
334
|
+
* (`deliver_as: 'document'`) — and compares the answers.
|
|
335
|
+
*
|
|
336
|
+
* Transcriptions are kept per version of the file and page, and shared with
|
|
337
|
+
* every other AI operation: a page already read for an `extract` or
|
|
338
|
+
* `summarize` that asked for text is not read again.
|
|
339
|
+
*
|
|
340
|
+
* @param document The PDF, as a DocumentHandle. Add `first_page` and
|
|
341
|
+
* `last_page` to transcribe only those pages; they are counted from 1 and
|
|
342
|
+
* the answer numbers pages in the original document.
|
|
343
|
+
* @param options Optional `regenerate` and `timeout`.
|
|
344
|
+
* @param context The execution context provided by the host.
|
|
345
|
+
* @returns A promise that resolves to each page's transcription, or why it has none.
|
|
346
|
+
* @throws Will throw an error if the operation fails or the input is not a PDF.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* const { data } = await transcribePages(
|
|
350
|
+
* { ...contract, first_page: 40, last_page: 52 },
|
|
351
|
+
* {},
|
|
352
|
+
* context,
|
|
353
|
+
* )
|
|
354
|
+
*
|
|
355
|
+
* for (const page of data.pages) {
|
|
356
|
+
* if ('error' in page)
|
|
357
|
+
* continue
|
|
358
|
+
* const quoted = page.text.includes(quote)
|
|
359
|
+
* }
|
|
360
|
+
*/
|
|
361
|
+
export async function transcribePages(document, options, context) {
|
|
362
|
+
if (!document || typeof document !== 'object' || !document.file_hash) {
|
|
363
|
+
throw new Error('The `document` parameter must be a valid DocumentHandle for `transcribePages`.');
|
|
364
|
+
}
|
|
365
|
+
const mimeType = (document.mime_type || '').split(';')[0].trim().toLowerCase();
|
|
366
|
+
if (mimeType !== 'application/pdf') {
|
|
367
|
+
throw new Error('`transcribePages` reads the pages of a PDF.');
|
|
368
|
+
}
|
|
369
|
+
const result = await _executeAIOperation('transcribe', document, options, context);
|
|
370
|
+
return {
|
|
371
|
+
data: { pages: Array.isArray(result.data?.pages) ? result.data.pages.map(_pageTranscription) : [] },
|
|
372
|
+
metadata: result.metadata,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
287
375
|
// ============================================================================
|
|
288
376
|
// Face Recognition API
|
|
289
377
|
// ============================================================================
|
package/dist/space/core.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DocumentHandle } from '../types.js';
|
|
1
2
|
export declare const PROTOCOL_VERSION: 1;
|
|
2
3
|
export type SpaceContext = {
|
|
3
4
|
applicationId: string;
|
|
@@ -29,6 +30,66 @@ export interface RecordSnapshot {
|
|
|
29
30
|
values: Readonly<Record<string, unknown>>;
|
|
30
31
|
}
|
|
31
32
|
export type GraphQLVariables = Readonly<Record<string, unknown>>;
|
|
33
|
+
/** A value that survives a JSON round trip unchanged. */
|
|
34
|
+
export type JsonValue = boolean | JsonObject | null | number | string | readonly JsonValue[];
|
|
35
|
+
/** A JSON object: not `null`, not an array, and not a single value. */
|
|
36
|
+
export interface JsonObject {
|
|
37
|
+
readonly [key: string]: JsonValue;
|
|
38
|
+
}
|
|
39
|
+
/** The platform-standard status of a task. */
|
|
40
|
+
export type TaskStatus = 'cancelled' | 'completed' | 'failed' | 'in_progress' | 'queued' | 'waiting';
|
|
41
|
+
export interface TaskCreateInput {
|
|
42
|
+
/** The user the task is assigned to. Omit it to assign the task to its creator. */
|
|
43
|
+
assignedToId?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The typed input the task type declares for its tasks. It is always a JSON
|
|
46
|
+
* object, even when the task type needs nothing: then it is `{}`.
|
|
47
|
+
*/
|
|
48
|
+
input: JsonObject;
|
|
49
|
+
/** The ID of the task type the task is created from. */
|
|
50
|
+
taskTypeId: string;
|
|
51
|
+
title: string;
|
|
52
|
+
}
|
|
53
|
+
export interface TaskCreateResult {
|
|
54
|
+
task: {
|
|
55
|
+
id: string;
|
|
56
|
+
revision: number;
|
|
57
|
+
status: TaskStatus;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export interface TaskReplyInput {
|
|
61
|
+
content: string;
|
|
62
|
+
/** The message this reply answers, when it answers one. */
|
|
63
|
+
inReplyToMessageId?: string;
|
|
64
|
+
taskId: string;
|
|
65
|
+
}
|
|
66
|
+
export interface TaskReplyResult {
|
|
67
|
+
messageId: string;
|
|
68
|
+
/** The task's revision after the reply was recorded. */
|
|
69
|
+
taskRevision: number;
|
|
70
|
+
}
|
|
71
|
+
export interface SimpleTasksClient {
|
|
72
|
+
create: (task: TaskCreateInput) => Promise<TaskCreateResult>;
|
|
73
|
+
reply: (reply: TaskReplyInput) => Promise<TaskReplyResult>;
|
|
74
|
+
}
|
|
75
|
+
/** A stored file that is not yet attached to any record. */
|
|
76
|
+
export interface StagedDocumentHandle extends DocumentHandle {
|
|
77
|
+
scope?: 'ephemeral' | 'record' | 'staged';
|
|
78
|
+
}
|
|
79
|
+
export interface DocumentStageInput {
|
|
80
|
+
/** The file to stage. Its bytes are transferred to the host, not copied. */
|
|
81
|
+
file: Blob | File;
|
|
82
|
+
/** Defaults to the file's own type, then to `application/octet-stream`. */
|
|
83
|
+
mimeType?: string;
|
|
84
|
+
/** Defaults to the name of a `File`. A plain `Blob` has none, so it needs one. */
|
|
85
|
+
name?: string;
|
|
86
|
+
}
|
|
87
|
+
export interface DocumentStageResult {
|
|
88
|
+
handle: StagedDocumentHandle;
|
|
89
|
+
}
|
|
90
|
+
export interface SimpleDocumentsClient {
|
|
91
|
+
stage: (document: DocumentStageInput) => Promise<DocumentStageResult>;
|
|
92
|
+
}
|
|
32
93
|
export interface SpaceDataTransport {
|
|
33
94
|
execute: <TResult = unknown>(document: string, variables?: GraphQLVariables) => Promise<TResult>;
|
|
34
95
|
}
|
|
@@ -45,9 +106,11 @@ export interface RecordHandle {
|
|
|
45
106
|
export interface SimpleClient {
|
|
46
107
|
context: SpaceContext;
|
|
47
108
|
data: SimpleDataClient;
|
|
109
|
+
documents: SimpleDocumentsClient;
|
|
48
110
|
records: {
|
|
49
111
|
current: () => Promise<RecordHandle>;
|
|
50
112
|
};
|
|
113
|
+
tasks: SimpleTasksClient;
|
|
51
114
|
}
|
|
52
115
|
export interface SpaceProtocolErrorPayload {
|
|
53
116
|
code: string;
|
|
@@ -104,7 +167,30 @@ export interface RecordSubmitResult {
|
|
|
104
167
|
ok: boolean;
|
|
105
168
|
snapshot: RecordSnapshot;
|
|
106
169
|
}
|
|
107
|
-
export
|
|
170
|
+
export interface TaskCreateRequest {
|
|
171
|
+
operation: 'task.create';
|
|
172
|
+
payload: TaskCreateInput;
|
|
173
|
+
protocol: typeof PROTOCOL_VERSION;
|
|
174
|
+
requestId: string;
|
|
175
|
+
}
|
|
176
|
+
export interface TaskReplyRequest {
|
|
177
|
+
operation: 'task.reply';
|
|
178
|
+
payload: TaskReplyInput;
|
|
179
|
+
protocol: typeof PROTOCOL_VERSION;
|
|
180
|
+
requestId: string;
|
|
181
|
+
}
|
|
182
|
+
export interface DocumentStageRequest {
|
|
183
|
+
operation: 'document.stage';
|
|
184
|
+
payload: {
|
|
185
|
+
/** Transferred with the request, so it is detached in the Space afterwards. */
|
|
186
|
+
bytes: ArrayBuffer;
|
|
187
|
+
mimeType: string;
|
|
188
|
+
name: string;
|
|
189
|
+
};
|
|
190
|
+
protocol: typeof PROTOCOL_VERSION;
|
|
191
|
+
requestId: string;
|
|
192
|
+
}
|
|
193
|
+
export type ProtocolRequest = CurrentRecordRequest | DocumentStageRequest | RecordSubmitRequest | RecordUpdateRequest | TaskCreateRequest | TaskReplyRequest;
|
|
108
194
|
export interface ProtocolSuccessResponse<TResult> {
|
|
109
195
|
ok: true;
|
|
110
196
|
protocol: typeof PROTOCOL_VERSION;
|
|
@@ -119,12 +205,22 @@ export interface ProtocolErrorResponse {
|
|
|
119
205
|
}
|
|
120
206
|
export type ProtocolResponse<TResult> = ProtocolErrorResponse | ProtocolSuccessResponse<TResult>;
|
|
121
207
|
export interface SpaceTransport {
|
|
122
|
-
|
|
208
|
+
/**
|
|
209
|
+
* `transfer` names buffers inside the request whose ownership moves to the
|
|
210
|
+
* host with it instead of being copied. A transport that cannot transfer
|
|
211
|
+
* sends them by value.
|
|
212
|
+
*/
|
|
213
|
+
request: <TResult>(request: ProtocolRequest, transfer?: ArrayBuffer[]) => Promise<ProtocolResponse<TResult>>;
|
|
123
214
|
}
|
|
124
215
|
export interface SimpleClientOptions {
|
|
125
216
|
context?: SpaceContext;
|
|
126
217
|
dataTransport?: SpaceDataTransport;
|
|
218
|
+
/** Present only when the host negotiated the document protocol. */
|
|
219
|
+
documentTransport?: SpaceTransport;
|
|
127
220
|
nextRequestId?: () => string;
|
|
221
|
+
/** Present only when the host negotiated the task protocol. */
|
|
222
|
+
taskTransport?: SpaceTransport;
|
|
223
|
+
/** Present only when the host negotiated the record protocol. */
|
|
128
224
|
transport?: SpaceTransport;
|
|
129
225
|
}
|
|
130
226
|
/**
|
|
@@ -134,5 +230,5 @@ export interface SimpleClientOptions {
|
|
|
134
230
|
* contract can be exercised in a first-party direct adapter and in any UI
|
|
135
231
|
* framework without importing browser-specific code.
|
|
136
232
|
*/
|
|
137
|
-
export declare function createSimpleClient({ context, dataTransport, nextRequestId, transport, }: SimpleClientOptions): SimpleClient;
|
|
233
|
+
export declare function createSimpleClient({ context, dataTransport, documentTransport, nextRequestId, taskTransport, transport, }: SimpleClientOptions): SimpleClient;
|
|
138
234
|
export declare function isSpaceContext(value: unknown): value is SpaceContext;
|
package/dist/space/core.js
CHANGED
|
@@ -34,7 +34,7 @@ export class SpaceDataError extends Error {
|
|
|
34
34
|
* contract can be exercised in a first-party direct adapter and in any UI
|
|
35
35
|
* framework without importing browser-specific code.
|
|
36
36
|
*/
|
|
37
|
-
export function createSimpleClient({ context = { kind: 'standalone' }, dataTransport, nextRequestId = createRequestId, transport, }) {
|
|
37
|
+
export function createSimpleClient({ context = { kind: 'standalone' }, dataTransport, documentTransport, nextRequestId = createRequestId, taskTransport, transport, }) {
|
|
38
38
|
const immutableContext = immutableSpaceContext(context);
|
|
39
39
|
return {
|
|
40
40
|
context: immutableContext,
|
|
@@ -42,6 +42,7 @@ export function createSimpleClient({ context = { kind: 'standalone' }, dataTrans
|
|
|
42
42
|
mutate: (document, variables) => executeData(dataTransport, document, variables),
|
|
43
43
|
query: (document, variables) => executeData(dataTransport, document, variables),
|
|
44
44
|
},
|
|
45
|
+
documents: createDocumentsClient(documentTransport, nextRequestId),
|
|
45
46
|
records: {
|
|
46
47
|
async current() {
|
|
47
48
|
if (immutableContext.kind !== 'record') {
|
|
@@ -70,6 +71,7 @@ export function createSimpleClient({ context = { kind: 'standalone' }, dataTrans
|
|
|
70
71
|
return new ProtocolRecordHandle(result, nextRequestId, transport);
|
|
71
72
|
},
|
|
72
73
|
},
|
|
74
|
+
tasks: createTasksClient(taskTransport, nextRequestId),
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
export function isSpaceContext(value) {
|
|
@@ -97,6 +99,83 @@ function executeData(dataTransport, document, variables) {
|
|
|
97
99
|
}
|
|
98
100
|
return dataTransport.execute(document, variables);
|
|
99
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* A staged document is stored without being attached to a record, so staging
|
|
104
|
+
* is available in any Space whose host negotiated the document protocol.
|
|
105
|
+
*/
|
|
106
|
+
function createDocumentsClient(transport, nextRequestId) {
|
|
107
|
+
return {
|
|
108
|
+
async stage(document) {
|
|
109
|
+
if (!transport) {
|
|
110
|
+
throw new SpaceProtocolError({
|
|
111
|
+
code: 'unavailable',
|
|
112
|
+
message: 'Documents are unavailable because the Space host did not negotiate the document protocol.',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const { file, mimeType, name } = readDocumentStageInput(document);
|
|
116
|
+
// A Blob cannot itself be transferred. Its bytes are read into a buffer
|
|
117
|
+
// once, and that buffer is handed to the host rather than copied again.
|
|
118
|
+
const bytes = await file.arrayBuffer();
|
|
119
|
+
const request = {
|
|
120
|
+
operation: 'document.stage',
|
|
121
|
+
payload: { bytes, mimeType, name },
|
|
122
|
+
protocol: PROTOCOL_VERSION,
|
|
123
|
+
requestId: nextRequestId(),
|
|
124
|
+
};
|
|
125
|
+
const response = await transport.request(request, [bytes]);
|
|
126
|
+
const result = readResponse(response, request);
|
|
127
|
+
if (!isDocumentStageResult(result))
|
|
128
|
+
throw invalidResponse('The document-stage response is malformed.');
|
|
129
|
+
return { handle: { ...result.handle } };
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Tasks are not tied to a page record, so they are available in any Space
|
|
135
|
+
* whose host negotiated the task protocol, standalone or record.
|
|
136
|
+
*/
|
|
137
|
+
function createTasksClient(transport, nextRequestId) {
|
|
138
|
+
const requireTransport = () => {
|
|
139
|
+
if (!transport) {
|
|
140
|
+
throw new SpaceProtocolError({
|
|
141
|
+
code: 'unavailable',
|
|
142
|
+
message: 'Tasks are unavailable because the Space host did not negotiate the task protocol.',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return transport;
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
async create(task) {
|
|
149
|
+
const taskTransport = requireTransport();
|
|
150
|
+
const request = {
|
|
151
|
+
operation: 'task.create',
|
|
152
|
+
payload: readTaskCreatePayload(task),
|
|
153
|
+
protocol: PROTOCOL_VERSION,
|
|
154
|
+
requestId: nextRequestId(),
|
|
155
|
+
};
|
|
156
|
+
const response = await taskTransport.request(request);
|
|
157
|
+
const result = readResponse(response, request);
|
|
158
|
+
if (!isTaskCreateResult(result))
|
|
159
|
+
throw invalidResponse('The task-create response is malformed.');
|
|
160
|
+
const { id, revision, status } = result.task;
|
|
161
|
+
return { task: { id, revision, status } };
|
|
162
|
+
},
|
|
163
|
+
async reply(reply) {
|
|
164
|
+
const taskTransport = requireTransport();
|
|
165
|
+
const request = {
|
|
166
|
+
operation: 'task.reply',
|
|
167
|
+
payload: readTaskReplyPayload(reply),
|
|
168
|
+
protocol: PROTOCOL_VERSION,
|
|
169
|
+
requestId: nextRequestId(),
|
|
170
|
+
};
|
|
171
|
+
const response = await taskTransport.request(request);
|
|
172
|
+
const result = readResponse(response, request);
|
|
173
|
+
if (!isTaskReplyResult(result))
|
|
174
|
+
throw invalidResponse('The task-reply response is malformed.');
|
|
175
|
+
return { messageId: result.messageId, taskRevision: result.taskRevision };
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
100
179
|
class ProtocolRecordHandle {
|
|
101
180
|
constructor({ sessionId, snapshot }, nextRequestId, transport) {
|
|
102
181
|
_ProtocolRecordHandle_nextRequestId.set(this, void 0);
|
|
@@ -168,6 +247,137 @@ function deepFreeze(value) {
|
|
|
168
247
|
function invalidResponse(message) {
|
|
169
248
|
return new SpaceProtocolError({ code: 'invalid_response', message });
|
|
170
249
|
}
|
|
250
|
+
function invalidRequest(message) {
|
|
251
|
+
return new SpaceProtocolError({ code: 'invalid_request', message });
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Checks a task before it is sent, so a plain-JavaScript caller learns what is
|
|
255
|
+
* wrong without a host round trip. The payload names only the members the
|
|
256
|
+
* operation defines; an optional member left out is not sent at all.
|
|
257
|
+
*/
|
|
258
|
+
function readTaskCreatePayload(task) {
|
|
259
|
+
if (!isObjectRecord(task))
|
|
260
|
+
throw invalidRequest('A task needs a title, a task type, and its input.');
|
|
261
|
+
if (!isNonBlankString(task.title))
|
|
262
|
+
throw invalidRequest('A task needs a title.');
|
|
263
|
+
if (!isNonBlankString(task.taskTypeId))
|
|
264
|
+
throw invalidRequest('A task needs the ID of its task type.');
|
|
265
|
+
if (!isJsonObject(task.input))
|
|
266
|
+
throw invalidRequest('A task input must be a JSON object, and everything in it a JSON value.');
|
|
267
|
+
if (task.assignedToId !== undefined && !isNonBlankString(task.assignedToId))
|
|
268
|
+
throw invalidRequest('A task assignee must be a user ID.');
|
|
269
|
+
return {
|
|
270
|
+
...(task.assignedToId === undefined ? {} : { assignedToId: task.assignedToId }),
|
|
271
|
+
input: task.input,
|
|
272
|
+
taskTypeId: task.taskTypeId,
|
|
273
|
+
title: task.title,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function readTaskReplyPayload(reply) {
|
|
277
|
+
if (!isObjectRecord(reply))
|
|
278
|
+
throw invalidRequest('A task reply needs a task ID and its content.');
|
|
279
|
+
if (!isNonBlankString(reply.taskId))
|
|
280
|
+
throw invalidRequest('A task reply needs the ID of its task.');
|
|
281
|
+
if (!isNonBlankString(reply.content))
|
|
282
|
+
throw invalidRequest('A task reply needs content.');
|
|
283
|
+
if (reply.inReplyToMessageId !== undefined && !isNonBlankString(reply.inReplyToMessageId))
|
|
284
|
+
throw invalidRequest('A task reply can answer only a message ID.');
|
|
285
|
+
return {
|
|
286
|
+
content: reply.content,
|
|
287
|
+
...(reply.inReplyToMessageId === undefined ? {} : { inReplyToMessageId: reply.inReplyToMessageId }),
|
|
288
|
+
taskId: reply.taskId,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* The task.create contract takes an object for input, never null, an array, or
|
|
293
|
+
* a single value, so a caller learns that here instead of from the host.
|
|
294
|
+
*/
|
|
295
|
+
function isJsonObject(value) {
|
|
296
|
+
return isObjectRecord(value) && isJsonValue(value);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Accepts only what JSON carries unchanged. A structured clone would pass a
|
|
300
|
+
* Date, Map, or class instance through a MessagePort and a JSON transport
|
|
301
|
+
* would not, so the task input is held to JSON on every transport.
|
|
302
|
+
*/
|
|
303
|
+
function isJsonValue(value, ancestors = new Set()) {
|
|
304
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string')
|
|
305
|
+
return true;
|
|
306
|
+
if (typeof value === 'number')
|
|
307
|
+
return Number.isFinite(value);
|
|
308
|
+
if (!value || typeof value !== 'object' || ancestors.has(value))
|
|
309
|
+
return false;
|
|
310
|
+
const prototype = Object.getPrototypeOf(value);
|
|
311
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
|
|
312
|
+
return false;
|
|
313
|
+
ancestors.add(value);
|
|
314
|
+
const valid = Object.values(value).every(child => isJsonValue(child, ancestors));
|
|
315
|
+
ancestors.delete(value);
|
|
316
|
+
return valid;
|
|
317
|
+
}
|
|
318
|
+
function readDocumentStageInput(document) {
|
|
319
|
+
if (!isObjectRecord(document) || !isBlob(document.file))
|
|
320
|
+
throw invalidRequest('A staged document needs a File or Blob.');
|
|
321
|
+
const name = document.name ?? readFileName(document.file);
|
|
322
|
+
if (!isNonBlankString(name))
|
|
323
|
+
throw invalidRequest('A staged document needs a name, and a Blob has none of its own.');
|
|
324
|
+
if (document.mimeType !== undefined && typeof document.mimeType !== 'string')
|
|
325
|
+
throw invalidRequest('A staged document type must be a MIME type.');
|
|
326
|
+
return {
|
|
327
|
+
file: document.file,
|
|
328
|
+
mimeType: document.mimeType || document.file.type || 'application/octet-stream',
|
|
329
|
+
name,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/** Duck-typed so a Blob from another realm, or a test double, is accepted. */
|
|
333
|
+
function isBlob(value) {
|
|
334
|
+
if (!isObjectRecord(value))
|
|
335
|
+
return false;
|
|
336
|
+
return typeof value.arrayBuffer === 'function' && typeof value.size === 'number' && typeof value.type === 'string';
|
|
337
|
+
}
|
|
338
|
+
function readFileName(file) {
|
|
339
|
+
const { name } = file;
|
|
340
|
+
return typeof name === 'string' ? name : undefined;
|
|
341
|
+
}
|
|
342
|
+
function isNonBlankString(value) {
|
|
343
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
344
|
+
}
|
|
345
|
+
function isNonNegativeInteger(value) {
|
|
346
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
347
|
+
}
|
|
348
|
+
const DOCUMENT_SCOPES = new Set([
|
|
349
|
+
'ephemeral',
|
|
350
|
+
'record',
|
|
351
|
+
'staged',
|
|
352
|
+
]);
|
|
353
|
+
function isDocumentStageResult(value) {
|
|
354
|
+
if (!isObjectRecord(value) || !isObjectRecord(value.handle))
|
|
355
|
+
return false;
|
|
356
|
+
const handle = value.handle;
|
|
357
|
+
return isNonBlankString(handle.file_hash)
|
|
358
|
+
&& isNonBlankString(handle.filename)
|
|
359
|
+
&& typeof handle.mime_type === 'string'
|
|
360
|
+
&& isNonNegativeInteger(handle.size)
|
|
361
|
+
&& isNonBlankString(handle.storage_path)
|
|
362
|
+
&& (handle.scope === undefined || DOCUMENT_SCOPES.has(handle.scope));
|
|
363
|
+
}
|
|
364
|
+
const TASK_STATUSES = new Set([
|
|
365
|
+
'cancelled',
|
|
366
|
+
'completed',
|
|
367
|
+
'failed',
|
|
368
|
+
'in_progress',
|
|
369
|
+
'queued',
|
|
370
|
+
'waiting',
|
|
371
|
+
]);
|
|
372
|
+
function isTaskCreateResult(value) {
|
|
373
|
+
if (!isObjectRecord(value) || !isObjectRecord(value.task))
|
|
374
|
+
return false;
|
|
375
|
+
const { id, revision, status } = value.task;
|
|
376
|
+
return isNonBlankString(id) && isNonNegativeInteger(revision) && TASK_STATUSES.has(status);
|
|
377
|
+
}
|
|
378
|
+
function isTaskReplyResult(value) {
|
|
379
|
+
return isObjectRecord(value) && isNonBlankString(value.messageId) && isNonNegativeInteger(value.taskRevision);
|
|
380
|
+
}
|
|
171
381
|
function isCurrentRecordResult(value) {
|
|
172
382
|
if (!value || typeof value !== 'object')
|
|
173
383
|
return false;
|
package/dist/space/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { SimpleClient } from './core.js';
|
|
2
2
|
import { SpaceDataError, SpaceProtocolError } from './core.js';
|
|
3
3
|
export { SpaceDataError, SpaceProtocolError, };
|
|
4
|
-
export type { GraphQLVariables, RecordErrorSnapshot, RecordFieldSnapshot, RecordFormError, RecordHandle, RecordSnapshot, RecordSubmitResult, RecordUpdateResult, SimpleClient, SimpleDataClient, SpaceContext, SpaceDataErrorPayload, SpaceProtocolErrorPayload, } from './core.js';
|
|
4
|
+
export type { DocumentStageInput, DocumentStageResult, GraphQLVariables, JsonObject, JsonValue, RecordErrorSnapshot, RecordFieldSnapshot, RecordFormError, RecordHandle, RecordSnapshot, RecordSubmitResult, RecordUpdateResult, SimpleClient, SimpleDataClient, SimpleDocumentsClient, SimpleTasksClient, SpaceContext, SpaceDataErrorPayload, SpaceProtocolErrorPayload, StagedDocumentHandle, TaskCreateInput, TaskCreateResult, TaskReplyInput, TaskReplyResult, TaskStatus, } from './core.js';
|
|
5
5
|
interface MessagePortLike {
|
|
6
6
|
onmessage: null | ((event: {
|
|
7
7
|
data: unknown;
|
|
8
8
|
}) => void);
|
|
9
|
-
postMessage: (message: unknown) => void;
|
|
9
|
+
postMessage: (message: unknown, transfer?: ArrayBuffer[]) => void;
|
|
10
10
|
start?: () => void;
|
|
11
11
|
}
|
|
12
12
|
export interface SpaceWindowLike {
|
|
@@ -27,7 +27,8 @@ export interface ConnectSpaceOptions {
|
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Connects any embedded Space to its parent through the dedicated MessagePort
|
|
30
|
-
* handshake.
|
|
31
|
-
*
|
|
30
|
+
* handshake. Each protocol capability is offered in `SPACE_READY` and becomes
|
|
31
|
+
* available only when the host negotiates it in `INIT_RPC`: record operations
|
|
32
|
+
* for a configured record view, and task and document operations in any Space.
|
|
32
33
|
*/
|
|
33
34
|
export declare function connectSpace({ targetOrigin, window }: ConnectSpaceOptions): Promise<SimpleClient>;
|
package/dist/space/index.js
CHANGED
|
@@ -2,8 +2,9 @@ import { createSimpleClient, isSpaceContext, PROTOCOL_VERSION, SpaceDataError, S
|
|
|
2
2
|
export { SpaceDataError, SpaceProtocolError, };
|
|
3
3
|
/**
|
|
4
4
|
* Connects any embedded Space to its parent through the dedicated MessagePort
|
|
5
|
-
* handshake.
|
|
6
|
-
*
|
|
5
|
+
* handshake. Each protocol capability is offered in `SPACE_READY` and becomes
|
|
6
|
+
* available only when the host negotiates it in `INIT_RPC`: record operations
|
|
7
|
+
* for a configured record view, and task and document operations in any Space.
|
|
7
8
|
*/
|
|
8
9
|
export function connectSpace({ targetOrigin, window = globalThis.window }) {
|
|
9
10
|
if (!window) {
|
|
@@ -33,18 +34,22 @@ export function connectSpace({ targetOrigin, window = globalThis.window }) {
|
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
const transport = createMessagePortTransport(port);
|
|
36
|
-
const
|
|
37
|
-
? transport
|
|
38
|
-
: undefined;
|
|
37
|
+
const protocols = event.data.protocols;
|
|
39
38
|
resolve(createSimpleClient({
|
|
40
39
|
context: event.data.context,
|
|
41
40
|
dataTransport: transport,
|
|
42
|
-
transport:
|
|
41
|
+
documentTransport: protocols?.document === PROTOCOL_VERSION ? transport : undefined,
|
|
42
|
+
taskTransport: protocols?.task === PROTOCOL_VERSION ? transport : undefined,
|
|
43
|
+
transport: protocols?.record === PROTOCOL_VERSION ? transport : undefined,
|
|
43
44
|
}));
|
|
44
45
|
};
|
|
45
46
|
window.addEventListener('message', onMessage);
|
|
46
47
|
window.parent.postMessage({
|
|
47
|
-
protocols: {
|
|
48
|
+
protocols: {
|
|
49
|
+
document: [PROTOCOL_VERSION],
|
|
50
|
+
record: [PROTOCOL_VERSION],
|
|
51
|
+
task: [PROTOCOL_VERSION],
|
|
52
|
+
},
|
|
48
53
|
type: 'SPACE_READY',
|
|
49
54
|
}, targetOrigin);
|
|
50
55
|
});
|
|
@@ -94,12 +99,12 @@ function createMessagePortTransport(port) {
|
|
|
94
99
|
});
|
|
95
100
|
});
|
|
96
101
|
},
|
|
97
|
-
request: (request) => {
|
|
102
|
+
request: (request, transfer = []) => {
|
|
98
103
|
return new Promise((resolve) => {
|
|
99
104
|
pending.set(request.requestId, {
|
|
100
105
|
resolve: response => resolve(response),
|
|
101
106
|
});
|
|
102
|
-
port.postMessage({ request, type: 'SPACE_PROTOCOL_REQUEST' });
|
|
107
|
+
port.postMessage({ request, type: 'SPACE_PROTOCOL_REQUEST' }, transfer);
|
|
103
108
|
});
|
|
104
109
|
},
|
|
105
110
|
};
|