@brass-build/client 0.4.0 → 0.5.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/AGENTS.md +142 -41
- package/CHANGELOG.md +93 -4
- package/README.md +666 -179
- package/dist/acting-organization.d.ts.map +1 -1
- package/dist/acting-organization.js +6 -11
- package/dist/acting-organization.js.map +1 -1
- package/dist/api-fetch.d.ts +8 -0
- package/dist/api-fetch.d.ts.map +1 -1
- package/dist/api-fetch.js +35 -8
- package/dist/api-fetch.js.map +1 -1
- package/dist/batched-get.d.ts +11 -0
- package/dist/batched-get.d.ts.map +1 -0
- package/dist/batched-get.js +92 -0
- package/dist/batched-get.js.map +1 -0
- package/dist/call-service.d.ts +4 -0
- package/dist/call-service.d.ts.map +1 -1
- package/dist/call-service.js +66 -3
- package/dist/call-service.js.map +1 -1
- package/dist/client.d.ts +51 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +982 -124
- package/dist/client.js.map +1 -1
- package/dist/document-checkpoints.d.ts +25 -2
- package/dist/document-checkpoints.d.ts.map +1 -1
- package/dist/document-checkpoints.js +258 -61
- package/dist/document-checkpoints.js.map +1 -1
- package/dist/document-commands.d.ts.map +1 -1
- package/dist/document-commands.js +5 -3
- package/dist/document-commands.js.map +1 -1
- package/dist/document-contents.d.ts +10 -0
- package/dist/document-contents.d.ts.map +1 -1
- package/dist/document-contents.js +23 -1
- package/dist/document-contents.js.map +1 -1
- package/dist/document-set-subscription.d.ts +56 -0
- package/dist/document-set-subscription.d.ts.map +1 -0
- package/dist/document-set-subscription.js +240 -0
- package/dist/document-set-subscription.js.map +1 -0
- package/dist/document-signals.d.ts +6 -0
- package/dist/document-signals.d.ts.map +1 -1
- package/dist/document-signals.js +31 -1
- package/dist/document-signals.js.map +1 -1
- package/dist/document-streams.d.ts +36 -2
- package/dist/document-streams.d.ts.map +1 -1
- package/dist/document-streams.js +69 -18
- package/dist/document-streams.js.map +1 -1
- package/dist/document-thumbnail-capture.d.ts +12 -2
- package/dist/document-thumbnail-capture.d.ts.map +1 -1
- package/dist/document-thumbnail-capture.js +103 -27
- package/dist/document-thumbnail-capture.js.map +1 -1
- package/dist/documents-namespace.d.ts +92 -10
- package/dist/documents-namespace.d.ts.map +1 -1
- package/dist/documents-namespace.js +285 -39
- package/dist/documents-namespace.js.map +1 -1
- package/dist/documents.d.ts +173 -8
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +1778 -310
- package/dist/documents.js.map +1 -1
- package/dist/errors.d.ts +3 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fetch-retry.d.ts +1 -0
- package/dist/fetch-retry.d.ts.map +1 -1
- package/dist/fetch-retry.js +2 -0
- package/dist/fetch-retry.js.map +1 -1
- package/dist/file-choice.d.ts +2 -0
- package/dist/file-choice.d.ts.map +1 -0
- package/dist/file-choice.js +27 -0
- package/dist/file-choice.js.map +1 -0
- package/dist/guest-link.d.ts +18 -1
- package/dist/guest-link.d.ts.map +1 -1
- package/dist/guest-link.js +329 -1
- package/dist/guest-link.js.map +1 -1
- package/dist/handle-types.d.ts +67 -5
- package/dist/handle-types.d.ts.map +1 -1
- package/dist/handles.d.ts +15 -19
- package/dist/handles.d.ts.map +1 -1
- package/dist/handles.js +231 -76
- package/dist/handles.js.map +1 -1
- package/dist/hosted-ui.d.ts +61 -0
- package/dist/hosted-ui.d.ts.map +1 -1
- package/dist/hosted-ui.js +94 -42
- package/dist/hosted-ui.js.map +1 -1
- package/dist/import-contract.d.ts +74 -0
- package/dist/import-contract.d.ts.map +1 -0
- package/dist/import-contract.js +163 -0
- package/dist/import-contract.js.map +1 -0
- package/dist/import-guest.d.ts +9 -0
- package/dist/import-guest.d.ts.map +1 -0
- package/dist/import-guest.js +155 -0
- package/dist/import-guest.js.map +1 -0
- package/dist/import-report.d.ts +17 -0
- package/dist/import-report.d.ts.map +1 -0
- package/dist/import-report.js +7 -0
- package/dist/import-report.js.map +1 -0
- package/dist/importer-wait.d.ts +2 -0
- package/dist/importer-wait.d.ts.map +1 -0
- package/dist/importer-wait.js +5 -0
- package/dist/importer-wait.js.map +1 -0
- package/dist/importer.d.ts +18 -4
- package/dist/importer.d.ts.map +1 -1
- package/dist/importer.js +322 -105
- package/dist/importer.js.map +1 -1
- package/dist/in-development.d.ts +2 -0
- package/dist/in-development.d.ts.map +1 -0
- package/dist/in-development.js +10 -0
- package/dist/in-development.js.map +1 -0
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -8
- package/dist/index.js.map +1 -1
- package/dist/init-keys.d.ts +1 -1
- package/dist/init-keys.d.ts.map +1 -1
- package/dist/init-keys.js +6 -1
- package/dist/init-keys.js.map +1 -1
- package/dist/input-documents.d.ts +67 -0
- package/dist/input-documents.d.ts.map +1 -0
- package/dist/input-documents.js +693 -0
- package/dist/input-documents.js.map +1 -0
- package/dist/inputs-status.d.ts +80 -0
- package/dist/inputs-status.d.ts.map +1 -0
- package/dist/inputs-status.js +460 -0
- package/dist/inputs-status.js.map +1 -0
- package/dist/inputs.d.ts +2 -0
- package/dist/inputs.d.ts.map +1 -0
- package/dist/inputs.js +793 -0
- package/dist/inputs.js.map +1 -0
- package/dist/internal.d.ts +6 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -1
- package/dist/internal.js.map +1 -1
- package/dist/join-content.d.ts +33 -0
- package/dist/join-content.d.ts.map +1 -0
- package/dist/join-content.js +114 -0
- package/dist/join-content.js.map +1 -0
- package/dist/join-stamp.d.ts +3 -0
- package/dist/join-stamp.d.ts.map +1 -0
- package/dist/join-stamp.js +12 -0
- package/dist/join-stamp.js.map +1 -0
- package/dist/manifest-drift.d.ts +9 -0
- package/dist/manifest-drift.d.ts.map +1 -0
- package/dist/manifest-drift.js +46 -0
- package/dist/manifest-drift.js.map +1 -0
- package/dist/open-document.d.ts +109 -2
- package/dist/open-document.d.ts.map +1 -1
- package/dist/open-document.js +1822 -811
- package/dist/open-document.js.map +1 -1
- package/dist/page-restore.d.ts +12 -0
- package/dist/page-restore.d.ts.map +1 -0
- package/dist/page-restore.js +23 -0
- package/dist/page-restore.js.map +1 -0
- package/dist/placement.d.ts +13 -0
- package/dist/placement.d.ts.map +1 -0
- package/dist/placement.js +34 -0
- package/dist/placement.js.map +1 -0
- package/dist/presence.d.ts +1 -0
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js +16 -4
- package/dist/presence.js.map +1 -1
- package/dist/read-only-document.d.ts +87 -0
- package/dist/read-only-document.d.ts.map +1 -0
- package/dist/read-only-document.js +4 -0
- package/dist/read-only-document.js.map +1 -0
- package/dist/record-paging.d.ts +63 -6
- package/dist/record-paging.d.ts.map +1 -1
- package/dist/record-paging.js +122 -29
- package/dist/record-paging.js.map +1 -1
- package/dist/references.d.ts +40 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +135 -0
- package/dist/references.js.map +1 -0
- package/dist/request-lane.d.ts +5 -0
- package/dist/request-lane.d.ts.map +1 -0
- package/dist/request-lane.js +48 -0
- package/dist/request-lane.js.map +1 -0
- package/dist/sealed-arrow.d.ts +170 -0
- package/dist/sealed-arrow.d.ts.map +1 -0
- package/dist/sealed-arrow.js +769 -0
- package/dist/sealed-arrow.js.map +1 -0
- package/dist/sealed-cache.d.ts +21 -0
- package/dist/sealed-cache.d.ts.map +1 -0
- package/dist/sealed-cache.js +247 -0
- package/dist/sealed-cache.js.map +1 -0
- package/dist/sealed-encode.d.ts +87 -0
- package/dist/sealed-encode.d.ts.map +1 -0
- package/dist/sealed-encode.js +876 -0
- package/dist/sealed-encode.js.map +1 -0
- package/dist/sealed-handles.d.ts +45 -0
- package/dist/sealed-handles.d.ts.map +1 -0
- package/dist/sealed-handles.js +408 -0
- package/dist/sealed-handles.js.map +1 -0
- package/dist/sealed-store.d.ts +242 -0
- package/dist/sealed-store.d.ts.map +1 -0
- package/dist/sealed-store.js +1155 -0
- package/dist/sealed-store.js.map +1 -0
- package/dist/serializer.d.ts +6 -15
- package/dist/serializer.d.ts.map +1 -1
- package/dist/session-toolbar.d.ts +20 -5
- package/dist/session-toolbar.d.ts.map +1 -1
- package/dist/session-toolbar.js +419 -67
- package/dist/session-toolbar.js.map +1 -1
- package/dist/session.d.ts +38 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +562 -79
- package/dist/session.js.map +1 -1
- package/dist/sha256.d.ts +3 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +74 -0
- package/dist/sha256.js.map +1 -0
- package/dist/stable-handles.d.ts +19 -0
- package/dist/stable-handles.d.ts.map +1 -0
- package/dist/stable-handles.js +193 -0
- package/dist/stable-handles.js.map +1 -0
- package/dist/staged-seal.d.ts +2 -0
- package/dist/staged-seal.d.ts.map +1 -0
- package/dist/staged-seal.js +76 -0
- package/dist/staged-seal.js.map +1 -0
- package/dist/subscriber-delivery.d.ts +8 -0
- package/dist/subscriber-delivery.d.ts.map +1 -0
- package/dist/subscriber-delivery.js +31 -0
- package/dist/subscriber-delivery.js.map +1 -0
- package/dist/tail-op-import.d.ts +10 -0
- package/dist/tail-op-import.d.ts.map +1 -0
- package/dist/tail-op-import.js +33 -0
- package/dist/tail-op-import.js.map +1 -0
- package/dist/topic-lifecycle.d.ts +6 -0
- package/dist/topic-lifecycle.d.ts.map +1 -1
- package/dist/topic-lifecycle.js +32 -2
- package/dist/topic-lifecycle.js.map +1 -1
- package/dist/trial-replace-dialog.d.ts +10 -0
- package/dist/trial-replace-dialog.d.ts.map +1 -0
- package/dist/trial-replace-dialog.js +153 -0
- package/dist/trial-replace-dialog.js.map +1 -0
- package/dist/types.d.ts +8 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/undo.d.ts +60 -5
- package/dist/undo.d.ts.map +1 -1
- package/dist/undo.js +344 -75
- package/dist/undo.js.map +1 -1
- package/dist/version-bar.d.ts +1 -0
- package/dist/version-bar.d.ts.map +1 -1
- package/dist/version-bar.js +2 -1
- package/dist/version-bar.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/dist/ws-client.d.ts +21 -0
- package/dist/ws-client.d.ts.map +1 -1
- package/dist/ws-client.js +192 -8
- package/dist/ws-client.js.map +1 -1
- package/dist/ws-frame-limit.d.ts +1 -0
- package/dist/ws-frame-limit.d.ts.map +1 -1
- package/dist/ws-frame-limit.js.map +1 -1
- package/docs/app-manifest.md +121 -17
- package/docs/custom-chrome.md +24 -16
- package/docs/hosted-ui-advanced.md +38 -6
- package/docs/imported-documents.md +104 -51
- package/docs/presence.md +16 -4
- package/docs/schemas.md +8 -5
- package/docs/services.md +6 -6
- package/docs/streams.md +139 -150
- package/docs/text-fields.md +26 -19
- package/package.json +5 -2
- package/src/acting-organization.ts +6 -12
- package/src/api-fetch.ts +53 -3
- package/src/auth-flow.ts +1 -1
- package/src/batched-get.ts +117 -0
- package/src/call-service.ts +78 -3
- package/src/client.ts +1092 -121
- package/src/document-checkpoints.ts +307 -65
- package/src/document-commands.ts +5 -5
- package/src/document-contents.ts +27 -1
- package/src/document-set-subscription.ts +274 -0
- package/src/document-signals.ts +47 -5
- package/src/document-streams.ts +85 -16
- package/src/document-thumbnail-capture.ts +118 -32
- package/src/documents-namespace.ts +539 -84
- package/src/documents.ts +2017 -348
- package/src/errors.ts +17 -6
- package/src/fetch-retry.ts +5 -0
- package/src/file-choice.ts +26 -0
- package/src/guest-link.ts +362 -1
- package/src/handle-types.ts +73 -47
- package/src/handles.ts +253 -83
- package/src/hosted-ui.ts +211 -50
- package/src/import-contract.ts +227 -0
- package/src/import-guest.ts +193 -0
- package/src/import-report.ts +24 -0
- package/src/importer-wait.ts +4 -0
- package/src/importer.ts +386 -131
- package/src/in-development.ts +10 -0
- package/src/index.ts +76 -16
- package/src/init-keys.ts +6 -1
- package/src/input-documents.ts +819 -0
- package/src/inputs-status.ts +598 -0
- package/src/inputs.ts +1090 -0
- package/src/internal.ts +7 -0
- package/src/join-content.ts +135 -0
- package/src/join-stamp.ts +12 -0
- package/src/manifest-drift.ts +54 -0
- package/src/open-document.ts +2346 -930
- package/src/page-restore.ts +38 -0
- package/src/placement.ts +54 -0
- package/src/presence.ts +21 -5
- package/src/read-only-document.ts +133 -0
- package/src/record-paging.ts +147 -25
- package/src/references.ts +207 -0
- package/src/request-lane.ts +59 -0
- package/src/sealed-arrow.ts +843 -0
- package/src/sealed-cache.ts +267 -0
- package/src/sealed-encode.ts +1015 -0
- package/src/sealed-handles.ts +486 -0
- package/src/sealed-store.ts +1330 -0
- package/src/serializer.ts +8 -20
- package/src/session-toolbar.ts +463 -72
- package/src/session.ts +740 -99
- package/src/sha256.ts +65 -0
- package/src/stable-handles.ts +235 -0
- package/src/staged-seal.ts +90 -0
- package/src/subscriber-delivery.ts +37 -0
- package/src/tail-op-import.ts +30 -0
- package/src/topic-lifecycle.ts +38 -2
- package/src/trial-replace-dialog.ts +163 -0
- package/src/types.ts +6 -6
- package/src/undo.ts +386 -73
- package/src/version-bar.ts +5 -1
- package/src/version.ts +1 -1
- package/src/ws-client.ts +208 -16
- package/src/ws-frame-limit.ts +1 -0
|
@@ -4,10 +4,30 @@
|
|
|
4
4
|
// `Brass.createDocument`. Access-granting is hosted-only (the share
|
|
5
5
|
// dialog), not a programmatic method here.
|
|
6
6
|
|
|
7
|
+
import { awaitSeal, type SealStatus } from './staged-seal.js';
|
|
8
|
+
import type { ImportReport } from './import-report.js';
|
|
9
|
+
import type { DocumentWorkStatus } from './document-signals.js';
|
|
10
|
+
import type { InputReference } from './hosted-ui.js';
|
|
7
11
|
import type { Document } from './documents.js';
|
|
8
|
-
import type {
|
|
12
|
+
import type { CheckpointAnchor } from './document-checkpoints.js';
|
|
13
|
+
import type { ImportClaimResponse, ImportFailedResponse } from './importer.js';
|
|
9
14
|
import { createApiFetch } from './api-fetch.js';
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_IMPORT_STREAM,
|
|
17
|
+
encodeImportStreams,
|
|
18
|
+
encodeStreamBundle,
|
|
19
|
+
type ImportContent,
|
|
20
|
+
} from './import-contract.js';
|
|
21
|
+
import type { PackedRecord } from './record-paging.js';
|
|
10
22
|
import { BrassError } from './errors.js';
|
|
23
|
+
import { createRequestLane } from './request-lane.js';
|
|
24
|
+
import { createBatchedGet } from './batched-get.js';
|
|
25
|
+
import { parseProvisionalInputsAnswer, type ProvisionalInputsAnswer, type ProvisionalInputsPost } from './inputs.js';
|
|
26
|
+
import {
|
|
27
|
+
createDocumentList,
|
|
28
|
+
type DocumentList,
|
|
29
|
+
type DocumentSetSocket,
|
|
30
|
+
} from './document-set-subscription.js';
|
|
11
31
|
|
|
12
32
|
export type DocumentRole = 'owner' | 'editor' | 'viewer';
|
|
13
33
|
|
|
@@ -49,37 +69,17 @@ export interface DocumentSummary {
|
|
|
49
69
|
thumbnail?: DocumentThumbnail;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
|
-
// One byte stream of the original file behind an opaque-blob document. A
|
|
53
|
-
// source file is a set of one or more streams: the primary stream (what
|
|
54
|
-
// the file's own name names) plus any sidecar streams uploaded with it (a
|
|
55
|
-
// glTF's `.bin` buffer, a shapefile's `.dbf`). Each stream carries its own
|
|
56
|
-
// short-lived download URL, so an app fetches exactly the streams it
|
|
57
|
-
// needs, in parallel.
|
|
58
|
-
export interface DocumentSourceStream {
|
|
59
|
-
// The stream's name within the file set (the primary stream's is the
|
|
60
|
-
// file's own name).
|
|
61
|
-
name: string;
|
|
62
|
-
url: string;
|
|
63
|
-
content_type: string;
|
|
64
|
-
byte_size: number;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
72
|
// The original file behind an opaque-blob document, returned by
|
|
68
73
|
// `doc.sourceBlob()`. `url` is a short-lived presigned GET the app
|
|
69
74
|
// fetches the raw bytes from (the bytes are not inlined: a real source
|
|
70
75
|
// file routinely exceeds the API response cap); the descriptor fields let
|
|
71
76
|
// the app decide whether it handles this format before downloading.
|
|
72
|
-
// `streams` lists every stream of the file set, primary first (its entry
|
|
73
|
-
// mirrors the top-level fields): a single-stream file has exactly one
|
|
74
|
-
// entry, so an app that iterates `streams` handles single- and
|
|
75
|
-
// multi-stream files with one code path.
|
|
76
77
|
export interface DocumentSourceBlob {
|
|
77
78
|
url: string;
|
|
78
79
|
filename: string;
|
|
79
80
|
content_type: string;
|
|
80
81
|
byte_size: number;
|
|
81
82
|
expires_in: number;
|
|
82
|
-
streams: DocumentSourceStream[];
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export interface DocumentRecord extends DocumentSummary {
|
|
@@ -88,16 +88,37 @@ export interface DocumentRecord extends DocumentSummary {
|
|
|
88
88
|
role: DocumentRole;
|
|
89
89
|
members: DocumentMember[];
|
|
90
90
|
app_metadata?: Record<string, unknown>;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// `organization_id`, so the hierarchy above the document can evolve
|
|
95
|
-
// (folders / teams) without a client change.
|
|
91
|
+
/** The name of the project the document is filed in, ready to show as
|
|
92
|
+
* is, or null when it is in no project. Render it verbatim rather than
|
|
93
|
+
* composing a location from `project_id`. */
|
|
96
94
|
location_label: string | null;
|
|
97
95
|
// Whether anything can produce a file from this document. The server
|
|
98
96
|
// resolves who would write it, so a newly exportable format needs no
|
|
99
97
|
// client change.
|
|
100
98
|
downloadable: boolean;
|
|
99
|
+
/** The folder or archive this document sits in. Absent when it sits
|
|
100
|
+
* directly in its project, or loose. */
|
|
101
|
+
member_of?: DocumentContainer;
|
|
102
|
+
/** The document's family. A file carries its family before it is
|
|
103
|
+
* imported. Absent on a file no importer takes. */
|
|
104
|
+
family?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The folder or archive a document sits in. `path` is the document's own
|
|
108
|
+
* path inside it. `chain` is every folder and archive above the document,
|
|
109
|
+
* outermost first, ending with the one `doc_id` names. */
|
|
110
|
+
export interface DocumentContainer {
|
|
111
|
+
doc_id: string;
|
|
112
|
+
path: string;
|
|
113
|
+
name: string;
|
|
114
|
+
kind: 'folder' | 'archive';
|
|
115
|
+
chain: DocumentContainerStep[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface DocumentContainerStep {
|
|
119
|
+
doc_id: string;
|
|
120
|
+
name: string;
|
|
121
|
+
kind: 'folder' | 'archive';
|
|
101
122
|
}
|
|
102
123
|
|
|
103
124
|
// One stream of a document: a named, independently-loadable CRDT unit
|
|
@@ -106,9 +127,9 @@ export interface DocumentRecord extends DocumentSummary {
|
|
|
106
127
|
// identifies the stream's underlying CRDT line; apps only need `name`.
|
|
107
128
|
export interface DocumentStreamInfo {
|
|
108
129
|
name: string;
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
130
|
+
// `'crdt'` for every stream the platform writes today, frozen or not.
|
|
131
|
+
// `doc.stream()` refuses any other value, so a kind of stream added
|
|
132
|
+
// later is not opened as a line by this SDK.
|
|
112
133
|
format: string;
|
|
113
134
|
line_id: string;
|
|
114
135
|
created_at: string;
|
|
@@ -195,8 +216,8 @@ export interface PushToDriveResult {
|
|
|
195
216
|
// file-roundtrip path (a doc imported from a drive file). The doc stays
|
|
196
217
|
// `ahead` until the drive's change-event webhook confirms the import,
|
|
197
218
|
// so an app should say "submitted, waiting for the importer" rather
|
|
198
|
-
// than "published", and expect `driveStatus` to stay
|
|
199
|
-
// the confirmation lands. Absent / false means the push was
|
|
219
|
+
// than "published", and expect `driveStatus.publishing` to stay true
|
|
220
|
+
// (with `canPush` false) until the confirmation lands. Absent / false means the push was
|
|
200
221
|
// synchronous and the baseline already advanced (the doc is `synced`).
|
|
201
222
|
pending?: boolean;
|
|
202
223
|
// Entity count the push uploaded.
|
|
@@ -220,16 +241,135 @@ export interface CreateBranchRequest {
|
|
|
220
241
|
name?: string;
|
|
221
242
|
}
|
|
222
243
|
|
|
244
|
+
/** @internal What `documents.setRecords` is handed: the whole content of a document,
|
|
245
|
+
* in the order it should hold it. Each record is what a document holds: a
|
|
246
|
+
* stable key, the tag the record store calls `type`, and the body it calls
|
|
247
|
+
* `params`, which is the same value an app assigns when it writes a record
|
|
248
|
+
* through a handle. */
|
|
249
|
+
export type DocumentRecordSetInput = DocumentRecordStoreInput | DocumentStreamsInput;
|
|
250
|
+
|
|
251
|
+
/** A record store: the records, in order, under a schema version. */
|
|
252
|
+
export interface DocumentRecordStoreInput {
|
|
253
|
+
records: Array<{ key: string; type: string; params: unknown }>;
|
|
254
|
+
/** The schema version stamped on the document, which is what a reader
|
|
255
|
+
* checks its own expectations against. */
|
|
256
|
+
schemaVersion: string;
|
|
257
|
+
/** A format's own preamble, for records that came out of a file that has
|
|
258
|
+
* one. Omit it for records an app computed. */
|
|
259
|
+
header?: Array<{ type: string; params: unknown }>;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Content by stream, in the shape an importer returns it: the document's
|
|
263
|
+
* own stream is `default`, and any other name is a stream an app opens with
|
|
264
|
+
* `doc.stream(name)`. */
|
|
265
|
+
export interface DocumentStreamsInput {
|
|
266
|
+
streams: Record<string, ImportContent>;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Which documents to list. The other fields narrow what `from` resolves to
|
|
271
|
+
* and are AND-ed; a query naming no references lists no documents.
|
|
272
|
+
*
|
|
273
|
+
* The query is over the document, not over what is inside it: name, type,
|
|
274
|
+
* project and date are answered from the listing itself, and a question
|
|
275
|
+
* about records is asked of each document once it is open.
|
|
276
|
+
*/
|
|
277
|
+
export interface ListDocumentsQuery {
|
|
278
|
+
// The references to resolve: what `Brass.chooseInputs()` handed you,
|
|
279
|
+
// kept by your app. A folder reference keeps answering, so the platform
|
|
280
|
+
// resolves it against what is filed there now.
|
|
281
|
+
//
|
|
282
|
+
// Name them on every call. Your app's permissions are not a question it
|
|
283
|
+
// can ask: naming none answers with none, rather than with every document
|
|
284
|
+
// anyone has ever opened with it.
|
|
285
|
+
from?: readonly InputReference[];
|
|
286
|
+
// Type tokens, matched against a document's own type or the type of the
|
|
287
|
+
// file behind it, either of which names it: a file nobody has read yet
|
|
288
|
+
// already carries the family its importer will stamp, and opening it is
|
|
289
|
+
// what reads it.
|
|
290
|
+
types?: string[];
|
|
291
|
+
/** @internal */
|
|
292
|
+
projectIds?: string[];
|
|
293
|
+
/** @internal Case-insensitive substring of the document's name. */
|
|
294
|
+
name?: string;
|
|
295
|
+
/** @internal ISO-8601 bound on when the document last changed. */
|
|
296
|
+
updatedAfter?: string;
|
|
297
|
+
/** @internal */
|
|
298
|
+
updatedBefore?: string;
|
|
299
|
+
/** @internal At most this many, most recently worked on first. */
|
|
300
|
+
limit?: number;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface DocumentListing {
|
|
304
|
+
doc_id: string;
|
|
305
|
+
// Internal fields are optional, because the published types strip them
|
|
306
|
+
// and a value an app builds against those types carries neither.
|
|
307
|
+
/** @internal */
|
|
308
|
+
organization_id?: string;
|
|
309
|
+
project_id: string | null;
|
|
310
|
+
name: string;
|
|
311
|
+
created_at: string;
|
|
312
|
+
updated_at: string;
|
|
313
|
+
// When a person last acted on the document.
|
|
314
|
+
last_activity_at: string;
|
|
315
|
+
/** @internal */
|
|
316
|
+
owner_sub?: string;
|
|
317
|
+
// The document's family, and the type of the file behind it. Both say
|
|
318
|
+
// what the document IS: a file nothing has read yet already carries the
|
|
319
|
+
// family its importer will stamp, so neither answers whether the content
|
|
320
|
+
// has arrived. `doc.status` on the open document answers that.
|
|
321
|
+
family?: string;
|
|
322
|
+
file_type?: string;
|
|
323
|
+
thumbnail?: DocumentThumbnail;
|
|
324
|
+
// The folder or archive the document sits in, and its path inside it.
|
|
325
|
+
member_of?: { doc_id: string; path: string };
|
|
326
|
+
// Present while the platform has work in flight on the document (an import
|
|
327
|
+
// it is running), and after work that failed. Absent otherwise, including
|
|
328
|
+
// on a file nothing has opened yet.
|
|
329
|
+
status?: DocumentWorkStatus;
|
|
330
|
+
// Every entry of the listing's `from` that admits this document, nearest
|
|
331
|
+
// first: the document itself where it was named, then the folders or
|
|
332
|
+
// archives above it, nearest first, then its project, then its
|
|
333
|
+
// organization. The same objects the query was given, so they compare by
|
|
334
|
+
// identity.
|
|
335
|
+
from?: InputReference[];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
type DocumentListingWire = Omit<DocumentListing, 'family' | 'from'> & {
|
|
339
|
+
schema_type?: string;
|
|
340
|
+
refs?: string[];
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
type DocumentRecordWire = Omit<DocumentRecord, 'family'> & { schema_type?: string };
|
|
344
|
+
|
|
345
|
+
function listingFromWire(byToken: Map<string, InputReference>) {
|
|
346
|
+
return ({ refs, schema_type, ...row }: DocumentListingWire): DocumentListing => ({
|
|
347
|
+
...row,
|
|
348
|
+
...(schema_type !== undefined ? { family: schema_type } : {}),
|
|
349
|
+
...(refs !== undefined ? { from: refs.flatMap((token) => byToken.get(token) ?? []) } : {}),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
223
353
|
export interface DocumentsNamespace {
|
|
224
354
|
// Metadata read. Returns organization_id, project_id (or null), role,
|
|
225
355
|
// members, name. Closes the cold-URL-restore gap (an app whose URL
|
|
226
356
|
// carries a doc_id can resolve project + organization + role from
|
|
227
357
|
// this one call before opening the editable Document via
|
|
228
|
-
// `Brass.openDocumentById`). Creating / moving
|
|
229
|
-
//
|
|
230
|
-
//
|
|
358
|
+
// `Brass.openDocumentById`). Creating / moving documents is done
|
|
359
|
+
// through the hosted pickers + document-settings dialog, not this
|
|
360
|
+
// namespace; the runtime create-and-open path is the top-level
|
|
231
361
|
// `Brass.createDocument` / `Brass.openDocumentById`.
|
|
232
362
|
get(docId: string): Promise<DocumentRecord>;
|
|
363
|
+
/**
|
|
364
|
+
* The documents the references in `query.from` resolve to, narrowed by
|
|
365
|
+
* the rest of `query` and by what the signed-in person can see. Naming no
|
|
366
|
+
* references lists no documents. `onChanged`
|
|
367
|
+
* fires once when a document joins the set, leaves it, or changes.
|
|
368
|
+
*
|
|
369
|
+
* @internal The app-facing listing of a set is a key's input status
|
|
370
|
+
* (`status.files`).
|
|
371
|
+
*/
|
|
372
|
+
list(query?: ListDocumentsQuery): Promise<DocumentList>;
|
|
233
373
|
// Upload a preview image for a document. Brass cannot render an
|
|
234
374
|
// app's document body, so an app that wants a thumbnail on its
|
|
235
375
|
// document cards renders one itself (e.g. `canvas.toBlob`) and
|
|
@@ -243,6 +383,27 @@ export interface DocumentsNamespace {
|
|
|
243
383
|
doc: Document,
|
|
244
384
|
input: SetThumbnailInput,
|
|
245
385
|
): Promise<DocumentThumbnail>;
|
|
386
|
+
/**
|
|
387
|
+
* @internal
|
|
388
|
+
* Give a document its whole content at once, as a record set.
|
|
389
|
+
*
|
|
390
|
+
* The other way to fill a document is to write its records through the
|
|
391
|
+
* handles, one commit at a time, which is what builds a collaborative
|
|
392
|
+
* document: each record arrives as an edit, and the history of those edits
|
|
393
|
+
* is what makes it mergeable, undoable and versioned. A producer that
|
|
394
|
+
* already holds the whole set, an app that parsed a file it understands or
|
|
395
|
+
* computed a result, pays for that history and never uses it, and its
|
|
396
|
+
* readers pay again to get the records back out.
|
|
397
|
+
*
|
|
398
|
+
* So this hands the set over. The document then opens faster, holds less,
|
|
399
|
+
* and reads through exactly the same `Document`: an app reading it calls
|
|
400
|
+
* what it always calls.
|
|
401
|
+
*
|
|
402
|
+
* Takes a record store, or content by stream in the shape an importer
|
|
403
|
+
* returns. Legal only while the document has no content. Rejects otherwise,
|
|
404
|
+
* because a record set carries no history and so cannot be written over one.
|
|
405
|
+
*/
|
|
406
|
+
setRecords(docId: string, input: DocumentRecordSetInput): Promise<void>;
|
|
246
407
|
// Read a document's thumbnail. With no options, returns the
|
|
247
408
|
// document's current card thumbnail; pass `version` for the
|
|
248
409
|
// thumbnail at a point in history. Resolves `null` when no
|
|
@@ -258,7 +419,17 @@ export interface DocumentsNamespace {
|
|
|
258
419
|
// which the toolbar branch command + hosted branch dialog drive), and the
|
|
259
420
|
// hosted history modal; not exposed on `Brass.documents`.
|
|
260
421
|
export interface DocumentsInternalNamespace {
|
|
261
|
-
|
|
422
|
+
// `setThumbnail` at an explicit version: the picture a checkpoint took,
|
|
423
|
+
// uploaded once the checkpoint's version is known.
|
|
424
|
+
setThumbnailAt(
|
|
425
|
+
doc: Document,
|
|
426
|
+
input: SetThumbnailInput,
|
|
427
|
+
version: number,
|
|
428
|
+
): Promise<DocumentThumbnail>;
|
|
429
|
+
createCheckpoint(
|
|
430
|
+
doc: Document,
|
|
431
|
+
anchor?: CheckpointAnchor,
|
|
432
|
+
): Promise<DocumentCheckpoint>;
|
|
262
433
|
listBranches(
|
|
263
434
|
docId: string,
|
|
264
435
|
): Promise<{ default_branch_id: string; branches: BranchSummary[] }>;
|
|
@@ -288,23 +459,104 @@ export interface DocumentsInternalNamespace {
|
|
|
288
459
|
// file or the caller cannot reach it (404). The per-doc instance method
|
|
289
460
|
// `doc.sourceBlob()` routes through this.
|
|
290
461
|
sourceBlob(doc: Document): Promise<DocumentSourceBlob>;
|
|
462
|
+
// The input status behind `subscribeInputStatus`, one key's. Answers the
|
|
463
|
+
// response body as the server sent it, or null for a 404 (a key with no row
|
|
464
|
+
// yet, or a platform without the route).
|
|
465
|
+
readInputKeyStatus(docId: string, key: string, query: Record<string, string>): Promise<unknown>;
|
|
466
|
+
// Posts a key's provisional entry after the op carrying its change is
|
|
467
|
+
// acknowledged. Never rejects: the document already holds the change, and
|
|
468
|
+
// the platform projects it from there whatever this post answers.
|
|
469
|
+
/** @internal */
|
|
470
|
+
postProvisionalInputs(
|
|
471
|
+
docId: string,
|
|
472
|
+
post: ProvisionalInputsPost,
|
|
473
|
+
opts?: { superseded?: () => boolean },
|
|
474
|
+
): Promise<ProvisionalInputsAnswer | null>;
|
|
291
475
|
// The import-claim protocol behind `Brass.registerImporter`. The SDK
|
|
292
476
|
// (never the app) claims the right to run the import of a stored
|
|
293
477
|
// source file, and reports completion so the platform stamps the
|
|
294
478
|
// document with the importing app's schema. Both reject with a
|
|
295
479
|
// `BrassError` on the ineligible / claim-lost outcomes.
|
|
296
480
|
claimImport(docId: string): Promise<ImportClaimResponse>;
|
|
481
|
+
// Stage an import's encoded streams, as one bundle, for `completeImport`
|
|
482
|
+
// to seal; answers the key the completion names it by.
|
|
483
|
+
stageImport(docId: string, bundle: Uint8Array): Promise<string>;
|
|
297
484
|
completeImport(
|
|
298
485
|
docId: string,
|
|
486
|
+
report: ImportReport | undefined,
|
|
487
|
+
uploadKey: string,
|
|
299
488
|
): Promise<{ schema_ref: { app_id: string; schema_hash: string } }>;
|
|
489
|
+
// Record that the claim holder's importer could not read the file.
|
|
490
|
+
failImport(docId: string, message: string): Promise<ImportFailedResponse>;
|
|
300
491
|
}
|
|
301
492
|
|
|
302
493
|
interface DocumentsFactoryOptions {
|
|
303
494
|
apiBaseUrl: string;
|
|
304
495
|
getAccessToken: () => Promise<string | null>;
|
|
305
496
|
getAppAssertion?: () => Promise<string | null>;
|
|
497
|
+
getSocket?: () => DocumentSetSocket | undefined;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// A chosen reference as the `refs` query names it, and as a listing row names
|
|
501
|
+
// the reference it came from.
|
|
502
|
+
function refToken(ref: InputReference): string {
|
|
503
|
+
return ref.extent === 'organization'
|
|
504
|
+
? `organization:${ref.organization_id}`
|
|
505
|
+
: ref.extent === 'project'
|
|
506
|
+
? `project:${ref.project_id}`
|
|
507
|
+
: `document:${ref.doc_id}`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// How long a provisional inputs post waits before trying again, or null to
|
|
511
|
+
// give up. A 409 names an op that has not landed on its line yet, which it
|
|
512
|
+
// does within moments; a 429 asks for fewer requests; a 5xx is a write that
|
|
513
|
+
// applied nothing (other writers held the line), jittered so a commit's posts
|
|
514
|
+
// do not meet again. Anything else is final.
|
|
515
|
+
const PROVISIONAL_NOT_LANDED_DELAYS_MS = [500, 1000, 2000, 4000];
|
|
516
|
+
const PROVISIONAL_RATE_LIMITED_DELAYS_MS = [5000, 15000];
|
|
517
|
+
const PROVISIONAL_UNAVAILABLE_DELAYS_MS = [1000, 3000, 9000];
|
|
518
|
+
|
|
519
|
+
export function provisionalRetryDelayMs(
|
|
520
|
+
status: number | undefined,
|
|
521
|
+
attempt: number,
|
|
522
|
+
random: () => number = Math.random,
|
|
523
|
+
): number | null {
|
|
524
|
+
if (status !== undefined && status >= 500 && status <= 504) {
|
|
525
|
+
const base = PROVISIONAL_UNAVAILABLE_DELAYS_MS[attempt];
|
|
526
|
+
return base === undefined ? null : Math.round(base * (0.5 + random()));
|
|
527
|
+
}
|
|
528
|
+
const delays =
|
|
529
|
+
status === 409
|
|
530
|
+
? PROVISIONAL_NOT_LANDED_DELAYS_MS
|
|
531
|
+
: status === 429
|
|
532
|
+
? PROVISIONAL_RATE_LIMITED_DELAYS_MS
|
|
533
|
+
: [];
|
|
534
|
+
return delays[attempt] ?? null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
let inputsRouteMissingNoted = false;
|
|
538
|
+
|
|
539
|
+
// A platform without the provisional inputs route still records a key from
|
|
540
|
+
// the document when it compacts, so the post's absence delays the status
|
|
541
|
+
// rather than losing anything. Said once per page.
|
|
542
|
+
function noteInputsRouteMissing(): void {
|
|
543
|
+
if (inputsRouteMissingNoted) return;
|
|
544
|
+
inputsRouteMissingNoted = true;
|
|
545
|
+
console.info(
|
|
546
|
+
'Brass: this platform does not take provisional input posts yet, so input status updates after the document compacts.',
|
|
547
|
+
);
|
|
306
548
|
}
|
|
307
549
|
|
|
550
|
+
/** How many single `documents.get` requests one client has in flight at once. */
|
|
551
|
+
const GET_CONCURRENCY = 6;
|
|
552
|
+
/** How many batched `documents.get` requests (20 ids each) one client has in
|
|
553
|
+
* flight at once. */
|
|
554
|
+
const BATCH_CONCURRENCY = 5;
|
|
555
|
+
/** Attempts a queued `get` makes against a throttled API (about 9 s of
|
|
556
|
+
* backoff). The API's request budget is shared by every caller, so a get
|
|
557
|
+
* queued behind a burst can meet a refusal that clears within seconds. */
|
|
558
|
+
const GET_ATTEMPTS = 6;
|
|
559
|
+
|
|
308
560
|
export function createDocumentsNamespace(
|
|
309
561
|
opts: DocumentsFactoryOptions,
|
|
310
562
|
): DocumentsNamespace & DocumentsInternalNamespace {
|
|
@@ -314,11 +566,108 @@ export function createDocumentsNamespace(
|
|
|
314
566
|
// Minted once per namespace, so re-claiming from this client refreshes
|
|
315
567
|
// its own claim rather than taking a second one.
|
|
316
568
|
const importClientId = randomToken();
|
|
569
|
+
// An app that shows something about each of several hundred documents
|
|
570
|
+
// calls `get` once per document, usually all at once. Sent one request each,
|
|
571
|
+
// those calls spend the API's request budget for this app's other calls
|
|
572
|
+
// (its opens) and every other caller's, so they are batched, and reads sent
|
|
573
|
+
// on their own queue.
|
|
574
|
+
const getLane = createRequestLane(GET_CONCURRENCY);
|
|
575
|
+
const singleGet = (docId: string): Promise<DocumentRecordWire> =>
|
|
576
|
+
getLane.run(docId, () =>
|
|
577
|
+
apiFetch<DocumentRecordWire>({
|
|
578
|
+
path: `/documents/${encodeURIComponent(docId)}`,
|
|
579
|
+
maxAttempts: GET_ATTEMPTS,
|
|
580
|
+
}),
|
|
581
|
+
);
|
|
582
|
+
const batchedGet = createBatchedGet<DocumentRecordWire>({
|
|
583
|
+
apiFetch,
|
|
584
|
+
lane: createRequestLane(BATCH_CONCURRENCY),
|
|
585
|
+
single: singleGet,
|
|
586
|
+
maxAttempts: GET_ATTEMPTS,
|
|
587
|
+
});
|
|
588
|
+
const readSeal =
|
|
589
|
+
(docId: string) =>
|
|
590
|
+
(sealId: string): Promise<SealStatus> =>
|
|
591
|
+
apiFetch<SealStatus>({
|
|
592
|
+
path: `/documents/${encodeURIComponent(docId)}/seals/${encodeURIComponent(sealId)}`,
|
|
593
|
+
});
|
|
317
594
|
|
|
318
|
-
|
|
595
|
+
const putThumbnail = async (
|
|
596
|
+
doc: Document,
|
|
597
|
+
input: SetThumbnailInput,
|
|
598
|
+
version: number | null,
|
|
599
|
+
): Promise<DocumentThumbnail> => {
|
|
600
|
+
const docId = doc.id;
|
|
601
|
+
if (docId === null) {
|
|
602
|
+
throw new BrassError(
|
|
603
|
+
'setThumbnail requires a Document opened from the server',
|
|
604
|
+
{ status: 400 },
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
if (version === null) {
|
|
608
|
+
throw new BrassError(
|
|
609
|
+
'setThumbnail: the document has no resolved version yet',
|
|
610
|
+
{ status: 400 },
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const branchId = doc.branchId;
|
|
614
|
+
const { data, contentType } = await encodeThumbnailImage(input);
|
|
615
|
+
const body: {
|
|
616
|
+
content_type: string;
|
|
617
|
+
data: string;
|
|
618
|
+
version: number;
|
|
619
|
+
branch_id?: string;
|
|
620
|
+
width?: number;
|
|
621
|
+
height?: number;
|
|
622
|
+
} = { content_type: contentType, data, version };
|
|
623
|
+
if (branchId !== null) body.branch_id = branchId;
|
|
624
|
+
if (input.width !== undefined) body.width = input.width;
|
|
625
|
+
if (input.height !== undefined) body.height = input.height;
|
|
626
|
+
const res = await apiFetch<{ thumbnail: DocumentThumbnail }>({
|
|
627
|
+
method: 'POST',
|
|
628
|
+
path: `/documents/${encodeURIComponent(docId)}/thumbnail`,
|
|
629
|
+
body,
|
|
630
|
+
});
|
|
631
|
+
return res.thumbnail;
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
const namespace: DocumentsNamespace & DocumentsInternalNamespace = {
|
|
319
635
|
get(docId) {
|
|
320
|
-
return
|
|
321
|
-
|
|
636
|
+
return batchedGet(docId).then(({ schema_type, ...record }) => ({
|
|
637
|
+
...record,
|
|
638
|
+
...(schema_type !== undefined ? { family: schema_type } : {}),
|
|
639
|
+
}));
|
|
640
|
+
},
|
|
641
|
+
async list(query) {
|
|
642
|
+
const params = new URLSearchParams();
|
|
643
|
+
if (query?.from?.length) {
|
|
644
|
+
params.set(
|
|
645
|
+
'refs',
|
|
646
|
+
JSON.stringify(
|
|
647
|
+
query.from.map((ref) => ({
|
|
648
|
+
ref: refToken(ref),
|
|
649
|
+
...(ref.accept !== undefined ? { accept: ref.accept } : {}),
|
|
650
|
+
})),
|
|
651
|
+
),
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
if (query?.types?.length) params.set('type', query.types.join(','));
|
|
655
|
+
if (query?.projectIds?.length) params.set('project_id', query.projectIds.join(','));
|
|
656
|
+
if (query?.name) params.set('name', query.name);
|
|
657
|
+
if (query?.updatedAfter) params.set('updated_after', query.updatedAfter);
|
|
658
|
+
if (query?.updatedBefore) params.set('updated_before', query.updatedBefore);
|
|
659
|
+
if (query?.limit !== undefined) params.set('limit', String(query.limit));
|
|
660
|
+
const search = params.toString();
|
|
661
|
+
const result = await apiFetch<{ documents: DocumentListingWire[] }>({
|
|
662
|
+
path: search ? `/documents?${search}` : '/documents',
|
|
663
|
+
});
|
|
664
|
+
const byToken = new Map((query?.from ?? []).map((ref) => [refToken(ref), ref]));
|
|
665
|
+
const documents = result.documents.map(listingFromWire(byToken));
|
|
666
|
+
return createDocumentList({
|
|
667
|
+
documents,
|
|
668
|
+
query: query ?? {},
|
|
669
|
+
list: async (q) => (await namespace.list(q)).documents,
|
|
670
|
+
socket: opts.getSocket?.(),
|
|
322
671
|
});
|
|
323
672
|
},
|
|
324
673
|
listBranches(docId) {
|
|
@@ -331,6 +680,72 @@ export function createDocumentsNamespace(
|
|
|
331
680
|
path: `/documents/${encodeURIComponent(docId)}/streams?branch=${encodeURIComponent(branchId)}`,
|
|
332
681
|
});
|
|
333
682
|
},
|
|
683
|
+
async setRecords(docId, input) {
|
|
684
|
+
// Encoded on the client, by the encoder an importer's result goes
|
|
685
|
+
// through, and staged as one bundle, so the platform seals it exactly
|
|
686
|
+
// as it seals an import. What reaches the network is the stored form's
|
|
687
|
+
// tables rather than a JSON rendering of them, which is several times
|
|
688
|
+
// larger.
|
|
689
|
+
const { encodeSealedRecordsUpload } = await import('./sealed-encode.js');
|
|
690
|
+
const streams =
|
|
691
|
+
'streams' in input
|
|
692
|
+
? await encodeImportStreams({ streams: input.streams })
|
|
693
|
+
: [
|
|
694
|
+
{
|
|
695
|
+
name: DEFAULT_IMPORT_STREAM,
|
|
696
|
+
bytes: await encodeSealedRecordsUpload({
|
|
697
|
+
schemaVersion: input.schemaVersion,
|
|
698
|
+
header: (input.header ?? []).map((entry) => ({
|
|
699
|
+
tag: entry.type,
|
|
700
|
+
body: entry.params,
|
|
701
|
+
})),
|
|
702
|
+
records: input.records.map(
|
|
703
|
+
(record): PackedRecord<unknown> => [
|
|
704
|
+
record.key,
|
|
705
|
+
record.type,
|
|
706
|
+
record.params,
|
|
707
|
+
],
|
|
708
|
+
),
|
|
709
|
+
}),
|
|
710
|
+
},
|
|
711
|
+
];
|
|
712
|
+
if (streams.length === 0) {
|
|
713
|
+
throw new BrassError('There is nothing to set: every stream is empty', {
|
|
714
|
+
status: 400,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
const bytes = encodeStreamBundle(streams);
|
|
718
|
+
const staged = await apiFetch<{ upload_key: string; url: string }>({
|
|
719
|
+
method: 'POST',
|
|
720
|
+
path: `/documents/${encodeURIComponent(docId)}/records/upload`,
|
|
721
|
+
body: { byte_size: bytes.byteLength },
|
|
722
|
+
});
|
|
723
|
+
// A presigned PUT carries no Brass headers: the signature is the
|
|
724
|
+
// credential, and a stray header breaks it.
|
|
725
|
+
// The view's own buffer rather than the view: a `Uint8Array` over a
|
|
726
|
+
// larger buffer would send the whole of it, and `fetch` takes the
|
|
727
|
+
// buffer type directly.
|
|
728
|
+
const put = await fetch(staged.url, {
|
|
729
|
+
method: 'PUT',
|
|
730
|
+
body: bytes.buffer.slice(
|
|
731
|
+
bytes.byteOffset,
|
|
732
|
+
bytes.byteOffset + bytes.byteLength,
|
|
733
|
+
) as ArrayBuffer,
|
|
734
|
+
});
|
|
735
|
+
if (!put.ok) {
|
|
736
|
+
throw new BrassError('Uploading the record set failed', {
|
|
737
|
+
status: put.status,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
// The platform answers once the set is queued; the call resolves when
|
|
741
|
+
// it is stored, and rejects with the platform's refusal if it is not.
|
|
742
|
+
const accepted = await apiFetch<unknown>({
|
|
743
|
+
method: 'POST',
|
|
744
|
+
path: `/documents/${encodeURIComponent(docId)}/records`,
|
|
745
|
+
body: { upload_key: staged.upload_key },
|
|
746
|
+
});
|
|
747
|
+
await awaitSeal(accepted, readSeal(docId));
|
|
748
|
+
},
|
|
334
749
|
createStream(docId, branchId, name) {
|
|
335
750
|
return apiFetch<DocumentStreamInfo>({
|
|
336
751
|
method: 'POST',
|
|
@@ -357,44 +772,15 @@ export function createDocumentsNamespace(
|
|
|
357
772
|
});
|
|
358
773
|
},
|
|
359
774
|
async setThumbnail(doc, input) {
|
|
360
|
-
const docId = doc.id;
|
|
361
|
-
if (docId === null) {
|
|
362
|
-
throw new BrassError(
|
|
363
|
-
'setThumbnail requires a Document opened from the server',
|
|
364
|
-
{ status: 400 },
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
775
|
// Capture the depicted version synchronously, before the async
|
|
368
776
|
// encode, so a concurrent editor's op landing mid-upload cannot
|
|
369
777
|
// shift the anchor away from the state the app rendered.
|
|
370
|
-
|
|
371
|
-
if (version === null) {
|
|
372
|
-
throw new BrassError(
|
|
373
|
-
'setThumbnail: the document has no resolved version yet',
|
|
374
|
-
{ status: 400 },
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
const branchId = doc.branchId;
|
|
378
|
-
const { data, contentType } = await encodeThumbnailImage(input);
|
|
379
|
-
const body: {
|
|
380
|
-
content_type: string;
|
|
381
|
-
data: string;
|
|
382
|
-
version: number;
|
|
383
|
-
branch_id?: string;
|
|
384
|
-
width?: number;
|
|
385
|
-
height?: number;
|
|
386
|
-
} = { content_type: contentType, data, version };
|
|
387
|
-
if (branchId !== null) body.branch_id = branchId;
|
|
388
|
-
if (input.width !== undefined) body.width = input.width;
|
|
389
|
-
if (input.height !== undefined) body.height = input.height;
|
|
390
|
-
const res = await apiFetch<{ thumbnail: DocumentThumbnail }>({
|
|
391
|
-
method: 'POST',
|
|
392
|
-
path: `/documents/${encodeURIComponent(docId)}/thumbnail`,
|
|
393
|
-
body,
|
|
394
|
-
});
|
|
395
|
-
return res.thumbnail;
|
|
778
|
+
return putThumbnail(doc, input, doc.currentSeq);
|
|
396
779
|
},
|
|
397
|
-
async
|
|
780
|
+
async setThumbnailAt(doc, input, version) {
|
|
781
|
+
return putThumbnail(doc, input, version);
|
|
782
|
+
},
|
|
783
|
+
async createCheckpoint(doc, anchor) {
|
|
398
784
|
const docId = doc.id;
|
|
399
785
|
if (docId === null) {
|
|
400
786
|
throw new BrassError(
|
|
@@ -402,13 +788,11 @@ export function createDocumentsNamespace(
|
|
|
402
788
|
{ status: 400 },
|
|
403
789
|
);
|
|
404
790
|
}
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
// version" control.
|
|
411
|
-
const version = doc.currentSeq;
|
|
791
|
+
// The scheduler passes the seq it read once every local op was acked,
|
|
792
|
+
// so the marker holds the state of the calls it covers. Without one,
|
|
793
|
+
// the depicted version is read here, synchronously, so an op landing
|
|
794
|
+
// mid-call cannot shift the anchor.
|
|
795
|
+
const version = anchor?.version ?? doc.currentSeq;
|
|
412
796
|
if (version === null) {
|
|
413
797
|
throw new BrassError(
|
|
414
798
|
'createCheckpoint: the document has no resolved version yet',
|
|
@@ -463,18 +847,79 @@ export function createDocumentsNamespace(
|
|
|
463
847
|
path: `/documents/${encodeURIComponent(docId)}/source-blob`,
|
|
464
848
|
});
|
|
465
849
|
},
|
|
850
|
+
async postProvisionalInputs(docId, { key, body }, opts = {}) {
|
|
851
|
+
const path = `/documents/${encodeURIComponent(docId)}/inputs/${encodeURIComponent(key)}`;
|
|
852
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
853
|
+
let status: number | undefined;
|
|
854
|
+
try {
|
|
855
|
+
return parseProvisionalInputsAnswer(await apiFetch<unknown>({ method: 'POST', path, body }));
|
|
856
|
+
} catch (err) {
|
|
857
|
+
status = err instanceof BrassError ? err.status : undefined;
|
|
858
|
+
}
|
|
859
|
+
if (status === 404) {
|
|
860
|
+
noteInputsRouteMissing();
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
const delay = provisionalRetryDelayMs(status, attempt);
|
|
864
|
+
if (delay === null || opts.superseded?.() === true) return null;
|
|
865
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
866
|
+
if (opts.superseded?.() === true) return null;
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
readInputKeyStatus(docId, key, query) {
|
|
870
|
+
const search = new URLSearchParams(query).toString();
|
|
871
|
+
const path = `/documents/${encodeURIComponent(docId)}/inputs/${encodeURIComponent(key)}`;
|
|
872
|
+
return orNullOn404(apiFetch<unknown>({ path: search ? `${path}?${search}` : path }));
|
|
873
|
+
},
|
|
466
874
|
claimImport(docId) {
|
|
467
875
|
return apiFetch<ImportClaimResponse>({
|
|
468
876
|
method: 'POST',
|
|
469
877
|
path: `/documents/${encodeURIComponent(docId)}/import-claim`,
|
|
470
|
-
body: { client_id: importClientId },
|
|
878
|
+
body: { client_id: importClientId, stages: true },
|
|
471
879
|
});
|
|
472
880
|
},
|
|
473
|
-
|
|
474
|
-
|
|
881
|
+
async stageImport(docId, bundle) {
|
|
882
|
+
const staged = await apiFetch<{ upload_key: string; url: string }>({
|
|
883
|
+
method: 'POST',
|
|
884
|
+
path: `/documents/${encodeURIComponent(docId)}/import-complete/upload`,
|
|
885
|
+
body: { byte_size: bundle.byteLength },
|
|
886
|
+
});
|
|
887
|
+
// A presigned PUT carries no Brass headers: the signature is the
|
|
888
|
+
// credential, and a stray header breaks it.
|
|
889
|
+
const put = await fetch(staged.url, {
|
|
890
|
+
method: 'PUT',
|
|
891
|
+
body: bundle.buffer.slice(
|
|
892
|
+
bundle.byteOffset,
|
|
893
|
+
bundle.byteOffset + bundle.byteLength,
|
|
894
|
+
) as ArrayBuffer,
|
|
895
|
+
});
|
|
896
|
+
if (!put.ok) {
|
|
897
|
+
throw new BrassError('Uploading the import failed', {
|
|
898
|
+
status: put.status,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
return staged.upload_key;
|
|
902
|
+
},
|
|
903
|
+
async completeImport(docId, report, uploadKey) {
|
|
904
|
+
const accepted = await apiFetch<unknown>({
|
|
475
905
|
method: 'POST',
|
|
476
906
|
path: `/documents/${encodeURIComponent(docId)}/import-complete`,
|
|
477
|
-
body: {
|
|
907
|
+
body: {
|
|
908
|
+
client_id: importClientId,
|
|
909
|
+
...(report !== undefined ? { report } : {}),
|
|
910
|
+
upload_key: uploadKey,
|
|
911
|
+
},
|
|
912
|
+
});
|
|
913
|
+
return awaitSeal<{ schema_ref: { app_id: string; schema_hash: string } }>(
|
|
914
|
+
accepted,
|
|
915
|
+
readSeal(docId),
|
|
916
|
+
);
|
|
917
|
+
},
|
|
918
|
+
failImport(docId, message) {
|
|
919
|
+
return apiFetch<ImportFailedResponse>({
|
|
920
|
+
method: 'POST',
|
|
921
|
+
path: `/documents/${encodeURIComponent(docId)}/import-failed`,
|
|
922
|
+
body: { client_id: importClientId, message },
|
|
478
923
|
});
|
|
479
924
|
},
|
|
480
925
|
async getThumbnail(docId, options) {
|
|
@@ -492,9 +937,10 @@ export function createDocumentsNamespace(
|
|
|
492
937
|
return res.thumbnail;
|
|
493
938
|
},
|
|
494
939
|
};
|
|
940
|
+
return namespace;
|
|
495
941
|
}
|
|
496
942
|
|
|
497
|
-
function randomToken(): string {
|
|
943
|
+
export function randomToken(): string {
|
|
498
944
|
const g = globalThis as { crypto?: { randomUUID?: () => string } };
|
|
499
945
|
if (g.crypto?.randomUUID) return g.crypto.randomUUID();
|
|
500
946
|
let s = '';
|
|
@@ -539,3 +985,12 @@ function bytesToBase64(bytes: Uint8Array): string {
|
|
|
539
985
|
}
|
|
540
986
|
return btoa(binary);
|
|
541
987
|
}
|
|
988
|
+
|
|
989
|
+
async function orNullOn404(request: Promise<unknown>): Promise<unknown> {
|
|
990
|
+
try {
|
|
991
|
+
return await request;
|
|
992
|
+
} catch (err) {
|
|
993
|
+
if (err instanceof BrassError && err.status === 404) return null;
|
|
994
|
+
throw err;
|
|
995
|
+
}
|
|
996
|
+
}
|