@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
package/dist/inputs.js
ADDED
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
// The shape of a document that references other documents: the references a
|
|
2
|
+
// key holds, the reserved CRDT roots they live in, the status the platform
|
|
3
|
+
// reports for a key, and the frames and bodies that carry it. This is the one
|
|
4
|
+
// definition; the platform's `@brass-build/contracts` re-exports it.
|
|
5
|
+
//
|
|
6
|
+
// The document's CRDT is the source of truth for each key's references and
|
|
7
|
+
// read marks. Nothing read out of the roots is authority: a reference's
|
|
8
|
+
// `organization_id` is a hint for resolving it and grants nothing.
|
|
9
|
+
import { sha256Hex } from './sha256.js';
|
|
10
|
+
const DEFAULT_STREAM_NAME = 'default';
|
|
11
|
+
const NAME_TOKEN_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
12
|
+
function isNameToken(value) {
|
|
13
|
+
return NAME_TOKEN_PATTERN.test(value);
|
|
14
|
+
}
|
|
15
|
+
// A stored reference may carry its families as `types`, which reads the same.
|
|
16
|
+
function parseSelectionAccept(value) {
|
|
17
|
+
if (!value || typeof value !== 'object')
|
|
18
|
+
return null;
|
|
19
|
+
const raw = value;
|
|
20
|
+
const list = (v) => {
|
|
21
|
+
if (!Array.isArray(v))
|
|
22
|
+
return null;
|
|
23
|
+
const items = v.filter((x) => typeof x === 'string' && x !== '');
|
|
24
|
+
return items.length > 0 ? items : null;
|
|
25
|
+
};
|
|
26
|
+
const names = list(raw['names']);
|
|
27
|
+
const extensions = list(raw['extensions']);
|
|
28
|
+
const families = list(raw['families']) ?? list(raw['types']);
|
|
29
|
+
if (!names && !extensions && !families)
|
|
30
|
+
return null;
|
|
31
|
+
return {
|
|
32
|
+
...(names ? { names } : {}),
|
|
33
|
+
...(extensions ? { extensions } : {}),
|
|
34
|
+
...(families ? { families } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function documentTopic(docId, branchId) {
|
|
38
|
+
return `doc:${docId}:${branchId}`;
|
|
39
|
+
}
|
|
40
|
+
function parseDocTopic(topic) {
|
|
41
|
+
const m = /^doc:(doc_[A-Za-z0-9_-]{32}):(b_[A-Za-z0-9_-]{32})$/.exec(topic);
|
|
42
|
+
return m && m[1] && m[2] ? { docId: m[1], branchId: m[2] } : null;
|
|
43
|
+
}
|
|
44
|
+
// --- Identifiers -----------------------------------------------------------
|
|
45
|
+
const DOC_ID_PATTERN = /^doc_[A-Za-z0-9_-]{32}$/;
|
|
46
|
+
const LINE_ID_PATTERN = /^b_[A-Za-z0-9_-]{32}$/;
|
|
47
|
+
// Organization and project ids are opaque here; the shape only has to keep
|
|
48
|
+
// them safe inside a key or an identity string.
|
|
49
|
+
const ENTITY_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
50
|
+
/** @internal */
|
|
51
|
+
export function isInputDocId(value) {
|
|
52
|
+
return typeof value === 'string' && DOC_ID_PATTERN.test(value);
|
|
53
|
+
}
|
|
54
|
+
/** @internal */
|
|
55
|
+
export function isInputLineId(value) {
|
|
56
|
+
return typeof value === 'string' && LINE_ID_PATTERN.test(value);
|
|
57
|
+
}
|
|
58
|
+
// --- Keys ------------------------------------------------------------------
|
|
59
|
+
/** @internal The longest key an app may record references under. */
|
|
60
|
+
export const INPUT_KEY_MAX_CHARS = 128;
|
|
61
|
+
/**
|
|
62
|
+
* @internal
|
|
63
|
+
* An app key: a letter or digit, then letters, digits and `._:@/+=~-`. `#`
|
|
64
|
+
* is excluded because the key is the tail of a row's sort key, and so is
|
|
65
|
+
* whitespace, which an app writing `stamp:${id}` never produces on purpose.
|
|
66
|
+
*/
|
|
67
|
+
export const INPUT_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/+=~-]{0,127}$/;
|
|
68
|
+
/** @internal Keys under this prefix are the platform's own, refused from an app. */
|
|
69
|
+
export const INPUT_KEY_RESERVED_PREFIX = 'brass:';
|
|
70
|
+
/** @internal */
|
|
71
|
+
export function isInputKey(value) {
|
|
72
|
+
return (typeof value === 'string' &&
|
|
73
|
+
INPUT_KEY_PATTERN.test(value) &&
|
|
74
|
+
!value.toLowerCase().startsWith(INPUT_KEY_RESERVED_PREFIX));
|
|
75
|
+
}
|
|
76
|
+
/** @internal */
|
|
77
|
+
export const INPUT_REFERENCE_LABEL_MAX_CHARS = 256;
|
|
78
|
+
/**
|
|
79
|
+
* @internal
|
|
80
|
+
* Reads a reference off an untrusted value: the wire, or an entry a peer
|
|
81
|
+
* wrote into the reserved roots. Null when it is not one.
|
|
82
|
+
*/
|
|
83
|
+
export function parseInputReference(raw) {
|
|
84
|
+
if (!isRecord(raw))
|
|
85
|
+
return null;
|
|
86
|
+
const extent = raw['extent'];
|
|
87
|
+
const organizationId = raw['organization_id'];
|
|
88
|
+
if (typeof organizationId !== 'string' || !ENTITY_ID_PATTERN.test(organizationId))
|
|
89
|
+
return null;
|
|
90
|
+
const ref = { organization_id: organizationId, extent: 'organization' };
|
|
91
|
+
if (extent === 'document') {
|
|
92
|
+
if (!isInputDocId(raw['doc_id']))
|
|
93
|
+
return null;
|
|
94
|
+
ref.extent = 'document';
|
|
95
|
+
ref.doc_id = raw['doc_id'];
|
|
96
|
+
const stream = raw['stream'];
|
|
97
|
+
if (stream !== undefined) {
|
|
98
|
+
if (typeof stream !== 'string' || !isNameToken(stream))
|
|
99
|
+
return null;
|
|
100
|
+
if (stream !== DEFAULT_STREAM_NAME)
|
|
101
|
+
ref.stream = stream;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (extent === 'project') {
|
|
105
|
+
const projectId = raw['project_id'];
|
|
106
|
+
if (typeof projectId !== 'string' || !ENTITY_ID_PATTERN.test(projectId))
|
|
107
|
+
return null;
|
|
108
|
+
if (raw['stream'] !== undefined)
|
|
109
|
+
return null;
|
|
110
|
+
ref.extent = 'project';
|
|
111
|
+
ref.project_id = projectId;
|
|
112
|
+
}
|
|
113
|
+
else if (extent === 'organization') {
|
|
114
|
+
if (raw['stream'] !== undefined)
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
// A project reference may also say which project a document sits in; the
|
|
121
|
+
// identity reads only the field its extent names.
|
|
122
|
+
if (ref.extent === 'document' && typeof raw['project_id'] === 'string') {
|
|
123
|
+
if (!ENTITY_ID_PATTERN.test(raw['project_id']))
|
|
124
|
+
return null;
|
|
125
|
+
ref.project_id = raw['project_id'];
|
|
126
|
+
}
|
|
127
|
+
const label = raw['label'];
|
|
128
|
+
if (label !== undefined) {
|
|
129
|
+
if (typeof label !== 'string' || label.length > INPUT_REFERENCE_LABEL_MAX_CHARS)
|
|
130
|
+
return null;
|
|
131
|
+
ref.label = label;
|
|
132
|
+
}
|
|
133
|
+
if (raw['accept'] !== undefined) {
|
|
134
|
+
if (!isRecord(raw['accept']))
|
|
135
|
+
return null;
|
|
136
|
+
// A narrowing with no clause narrows nothing, so it is the same
|
|
137
|
+
// reference as one carrying none.
|
|
138
|
+
const accept = parseSelectionAccept(raw['accept']);
|
|
139
|
+
if (accept !== null)
|
|
140
|
+
ref.accept = accept;
|
|
141
|
+
}
|
|
142
|
+
return ref;
|
|
143
|
+
}
|
|
144
|
+
function isEmptyAccept(accept) {
|
|
145
|
+
return !accept.families?.length && !accept.names?.length && !accept.extensions?.length;
|
|
146
|
+
}
|
|
147
|
+
// The narrowing as one canonical string: families and extensions compared
|
|
148
|
+
// case-insensitively, extensions without their dot, names case-insensitively
|
|
149
|
+
// (the match is), each list sorted and de-duplicated.
|
|
150
|
+
function canonicalAccept(accept) {
|
|
151
|
+
const norm = (xs, strip = false) => [...new Set((xs ?? []).map((x) => (strip ? x.replace(/^\./, '') : x).toLowerCase()))].sort();
|
|
152
|
+
return JSON.stringify({
|
|
153
|
+
t: norm(accept.families),
|
|
154
|
+
e: norm(accept.extensions, true),
|
|
155
|
+
n: norm(accept.names),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* @internal
|
|
160
|
+
* The reference's stable identity: the key it is stored under in the
|
|
161
|
+
* references root, and what the platform's rows name it by.
|
|
162
|
+
*
|
|
163
|
+
* `d:<doc_id>`, `d:<doc_id>/<stream>`, `p:<project_id>` or
|
|
164
|
+
* `o:<organization_id>`, followed by `~<16 hex>` when the reference carries a
|
|
165
|
+
* narrowing. The label is not part of it, so renaming a choice does not make
|
|
166
|
+
* it a different reference.
|
|
167
|
+
*/
|
|
168
|
+
export function inputReferenceId(ref) {
|
|
169
|
+
let id;
|
|
170
|
+
if (ref.extent === 'document') {
|
|
171
|
+
id = `d:${ref.doc_id ?? ''}`;
|
|
172
|
+
if (ref.stream !== undefined && ref.stream !== DEFAULT_STREAM_NAME)
|
|
173
|
+
id += `/${ref.stream}`;
|
|
174
|
+
}
|
|
175
|
+
else if (ref.extent === 'project') {
|
|
176
|
+
id = `p:${ref.project_id ?? ''}`;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
id = `o:${ref.organization_id}`;
|
|
180
|
+
}
|
|
181
|
+
if (ref.accept && !isEmptyAccept(ref.accept)) {
|
|
182
|
+
id += `~${sha256Hex(canonicalAccept(ref.accept)).slice(0, 16)}`;
|
|
183
|
+
}
|
|
184
|
+
return id;
|
|
185
|
+
}
|
|
186
|
+
const INPUT_REFERENCE_ID_PATTERN = /^(?:d:doc_[A-Za-z0-9_-]{32}(?:\/[a-z0-9][a-z0-9._-]{0,63})?|[po]:[A-Za-z0-9_-]{1,64})(?:~[0-9a-f]{16})?$/;
|
|
187
|
+
/** @internal */
|
|
188
|
+
export function isInputReferenceId(value) {
|
|
189
|
+
return typeof value === 'string' && INPUT_REFERENCE_ID_PATTERN.test(value);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* @internal
|
|
193
|
+
* The hash of a reference set: what keys with identical references share one
|
|
194
|
+
* resolution under. Order and duplicates do not move it, and neither does a
|
|
195
|
+
* label.
|
|
196
|
+
*/
|
|
197
|
+
export function inputReferenceSetHash(refs) {
|
|
198
|
+
const ids = [...new Set(refs.map(inputReferenceId))].sort();
|
|
199
|
+
return sha256Hex(`inputs-v1\n${ids.join('\n')}`);
|
|
200
|
+
}
|
|
201
|
+
const SET_HASH_PATTERN = /^[0-9a-f]{64}$/;
|
|
202
|
+
/** @internal */
|
|
203
|
+
export function isInputReferenceSetHash(value) {
|
|
204
|
+
return typeof value === 'string' && SET_HASH_PATTERN.test(value);
|
|
205
|
+
}
|
|
206
|
+
// --- Limits ----------------------------------------------------------------
|
|
207
|
+
/**
|
|
208
|
+
* @internal
|
|
209
|
+
* The most distinct documents a document's keys report across all of them.
|
|
210
|
+
* Past it the status says `truncated`; it is never a refusal.
|
|
211
|
+
*/
|
|
212
|
+
export const INPUT_DOCS_MAX = 2000;
|
|
213
|
+
/** @internal The most references one key may hold in a provisional body. */
|
|
214
|
+
export const INPUT_REFS_MAX_PER_KEY = 2000;
|
|
215
|
+
/** @internal The most read marks one key may carry in a provisional body. */
|
|
216
|
+
export const INPUT_READ_MARKS_MAX_PER_KEY = INPUT_DOCS_MAX;
|
|
217
|
+
/** @internal The longest version stamp a read mark or a file may carry. */
|
|
218
|
+
export const INPUT_STAMP_MAX_CHARS = 64;
|
|
219
|
+
// --- The reserved CRDT roots -------------------------------------------------
|
|
220
|
+
/**
|
|
221
|
+
* @internal
|
|
222
|
+
* The document roots holding every key's references and read marks. Both are
|
|
223
|
+
* flat maps at the root: a root container's id is fixed by its name, so every
|
|
224
|
+
* peer writes into the same container. A container created under a map entry
|
|
225
|
+
* is not: two peers creating one concurrently make two, and the entry keeps
|
|
226
|
+
* one of them, dropping the other's writes. So nothing here nests.
|
|
227
|
+
*
|
|
228
|
+
* The names sit outside both the `brass:` record-store key space and any name
|
|
229
|
+
* an app passes to `doc.map`, so record paging and the frozen-to-CRDT
|
|
230
|
+
* conversion, which walk app roots, never reach them.
|
|
231
|
+
*
|
|
232
|
+
* `INPUTS_REFS_ROOT`: `<key>␟<referenceId>` -> the reference.
|
|
233
|
+
* `INPUTS_READ_ROOT`: `<key>␟<docId>␟<stamp>␟<kind>` -> true.
|
|
234
|
+
*/
|
|
235
|
+
export const INPUTS_REFS_ROOT = '__brass_input_refs';
|
|
236
|
+
/** @internal */
|
|
237
|
+
export const INPUTS_READ_ROOT = '__brass_input_read';
|
|
238
|
+
/**
|
|
239
|
+
* @internal
|
|
240
|
+
* Joins the parts of an entry key: U+241F SYMBOL FOR UNIT SEPARATOR, which no
|
|
241
|
+
* app key, reference id, doc id or stamp can contain.
|
|
242
|
+
*/
|
|
243
|
+
export const INPUTS_ENTRY_SEPARATOR = '␟';
|
|
244
|
+
const SEP = INPUTS_ENTRY_SEPARATOR;
|
|
245
|
+
/** @internal */
|
|
246
|
+
export function inputRefEntryKey(key, refId) {
|
|
247
|
+
return `${key}${SEP}${refId}`;
|
|
248
|
+
}
|
|
249
|
+
/** @internal */
|
|
250
|
+
export function parseInputRefEntryKey(entryKey) {
|
|
251
|
+
const parts = entryKey.split(SEP);
|
|
252
|
+
if (parts.length !== 2)
|
|
253
|
+
return null;
|
|
254
|
+
const [key, refId] = parts;
|
|
255
|
+
return isInputKey(key) && isInputReferenceId(refId) ? { key, refId } : null;
|
|
256
|
+
}
|
|
257
|
+
/** @internal Digits a stamp is zero-padded to in an entry key, so the keys sort by stamp. */
|
|
258
|
+
export const INPUT_STAMP_DIGITS = 16;
|
|
259
|
+
/**
|
|
260
|
+
* @internal
|
|
261
|
+
* A version of a document, as a read mark records it: the milliseconds since
|
|
262
|
+
* the epoch of the `updated_at` the reader read the document at.
|
|
263
|
+
*/
|
|
264
|
+
export function inputStampFromUpdatedAt(updatedAt) {
|
|
265
|
+
const ms = Date.parse(updatedAt);
|
|
266
|
+
return Number.isSafeInteger(ms) && ms >= 0 && ms < 10 ** INPUT_STAMP_DIGITS ? ms : null;
|
|
267
|
+
}
|
|
268
|
+
function isStamp(value) {
|
|
269
|
+
return (typeof value === 'number' &&
|
|
270
|
+
Number.isSafeInteger(value) &&
|
|
271
|
+
value >= 0 &&
|
|
272
|
+
value < 10 ** INPUT_STAMP_DIGITS);
|
|
273
|
+
}
|
|
274
|
+
const READ_KIND = { removed: '0', read: '1' };
|
|
275
|
+
/** @internal */
|
|
276
|
+
export function inputReadEntryKey(key, docId, mark) {
|
|
277
|
+
const stamp = String(mark.stamp).padStart(INPUT_STAMP_DIGITS, '0');
|
|
278
|
+
const kind = mark.removed ? READ_KIND.removed : READ_KIND.read;
|
|
279
|
+
return `${key}${SEP}${docId}${SEP}${stamp}${SEP}${kind}`;
|
|
280
|
+
}
|
|
281
|
+
/** @internal The prefix every read mark of one key shares, or of one document under it. */
|
|
282
|
+
export function inputReadEntryPrefix(key, docId) {
|
|
283
|
+
return docId === undefined ? `${key}${SEP}` : `${key}${SEP}${docId}${SEP}`;
|
|
284
|
+
}
|
|
285
|
+
/** @internal */
|
|
286
|
+
export function parseInputReadEntryKey(entryKey) {
|
|
287
|
+
const parts = entryKey.split(SEP);
|
|
288
|
+
if (parts.length !== 4)
|
|
289
|
+
return null;
|
|
290
|
+
const [key, docId, stamp, kind] = parts;
|
|
291
|
+
if (!isInputKey(key) || !isInputDocId(docId))
|
|
292
|
+
return null;
|
|
293
|
+
if (!new RegExp(`^[0-9]{${INPUT_STAMP_DIGITS}}$`).test(stamp))
|
|
294
|
+
return null;
|
|
295
|
+
if (kind !== READ_KIND.removed && kind !== READ_KIND.read)
|
|
296
|
+
return null;
|
|
297
|
+
return { key, docId, mark: { stamp: Number(stamp), removed: kind === READ_KIND.removed } };
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* @internal
|
|
301
|
+
* The mark that acknowledges a file leaving a key, given the latest mark the
|
|
302
|
+
* reader holds for it and the version it left at (`updatedAt` on the removed
|
|
303
|
+
* entry; null when the entry carries none). The mark sits at that version, so
|
|
304
|
+
* a read of the file returning at it follows the removal. When the version
|
|
305
|
+
* did not move past the reader's last read, the removal has to sit one stamp
|
|
306
|
+
* past that read to follow it, and `inputReadMarkFor` lifts a later read of
|
|
307
|
+
* the returning file to the removal's stamp. A removal already acknowledged
|
|
308
|
+
* keeps its mark.
|
|
309
|
+
*/
|
|
310
|
+
export function inputRemovalMark(latest, updatedAt) {
|
|
311
|
+
if (latest?.removed === true)
|
|
312
|
+
return latest;
|
|
313
|
+
const leftAt = updatedAt === null ? null : inputStampFromUpdatedAt(updatedAt);
|
|
314
|
+
const floor = latest === null ? 0 : latest.stamp + 1;
|
|
315
|
+
return { stamp: Math.max(leftAt ?? 0, floor), removed: true };
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* @internal
|
|
319
|
+
* The mark that records reading a file at `stamp`, given the latest mark the
|
|
320
|
+
* reader holds for it: at `stamp`, or at a removal's stamp when that removal
|
|
321
|
+
* sits past it, which is the one stamp a read can take to follow it.
|
|
322
|
+
*/
|
|
323
|
+
export function inputReadMarkFor(latest, stamp) {
|
|
324
|
+
if (latest?.removed === true && latest.stamp > stamp)
|
|
325
|
+
return { stamp: latest.stamp, removed: false };
|
|
326
|
+
return { stamp, removed: false };
|
|
327
|
+
}
|
|
328
|
+
/** @internal Whether `a` is a later acknowledgement than `b`. */
|
|
329
|
+
export function inputReadMarkAfter(a, b) {
|
|
330
|
+
if (a.stamp !== b.stamp)
|
|
331
|
+
return a.stamp > b.stamp;
|
|
332
|
+
return !a.removed && b.removed;
|
|
333
|
+
}
|
|
334
|
+
/** @internal */
|
|
335
|
+
export function parseInputReadMark(raw) {
|
|
336
|
+
if (!isRecord(raw) || !isStamp(raw['stamp']) || typeof raw['removed'] !== 'boolean')
|
|
337
|
+
return null;
|
|
338
|
+
return { stamp: raw['stamp'], removed: raw['removed'] };
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* @internal
|
|
342
|
+
* Every key holding references, from the two roots' plain-object forms.
|
|
343
|
+
* Tolerant, since peers write the roots: an entry whose key does not parse,
|
|
344
|
+
* whose value is not a reference, or whose reference id does not match the
|
|
345
|
+
* one re-derived from its value is dropped rather than failing the read. A
|
|
346
|
+
* read mark is kept only when present (`true`), and only the latest per
|
|
347
|
+
* document counts. Keys with no references are absent, along with their
|
|
348
|
+
* marks.
|
|
349
|
+
*/
|
|
350
|
+
export function decodeInputsRoots(refsRoot, readRoot) {
|
|
351
|
+
const refsByKey = new Map();
|
|
352
|
+
if (isRecord(refsRoot)) {
|
|
353
|
+
for (const [entryKey, value] of Object.entries(refsRoot)) {
|
|
354
|
+
const parsed = parseInputRefEntryKey(entryKey);
|
|
355
|
+
const ref = parseInputReference(value);
|
|
356
|
+
if (parsed === null || ref === null || inputReferenceId(ref) !== parsed.refId)
|
|
357
|
+
continue;
|
|
358
|
+
const held = refsByKey.get(parsed.key) ?? new Map();
|
|
359
|
+
held.set(parsed.refId, ref);
|
|
360
|
+
refsByKey.set(parsed.key, held);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const out = new Map();
|
|
364
|
+
for (const key of [...refsByKey.keys()].sort()) {
|
|
365
|
+
const held = refsByKey.get(key);
|
|
366
|
+
const refs = [...held.keys()].sort().map((id) => held.get(id));
|
|
367
|
+
out.set(key, { refs, read: {} });
|
|
368
|
+
}
|
|
369
|
+
if (isRecord(readRoot)) {
|
|
370
|
+
for (const [entryKey, value] of Object.entries(readRoot)) {
|
|
371
|
+
if (value !== true)
|
|
372
|
+
continue;
|
|
373
|
+
const parsed = parseInputReadEntryKey(entryKey);
|
|
374
|
+
const state = parsed ? out.get(parsed.key) : undefined;
|
|
375
|
+
if (!parsed || !state)
|
|
376
|
+
continue;
|
|
377
|
+
const prior = state.read[parsed.docId];
|
|
378
|
+
if (!prior || inputReadMarkAfter(parsed.mark, prior))
|
|
379
|
+
state.read[parsed.docId] = parsed.mark;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* @internal
|
|
386
|
+
* The writes that make `key`'s references `next` (null clears the key), given
|
|
387
|
+
* the refs root as this writer sees it. Only the difference is written:
|
|
388
|
+
* entries this writer did not see are not touched, so a reference another
|
|
389
|
+
* peer adds concurrently survives. Throws on a reference that would not
|
|
390
|
+
* decode.
|
|
391
|
+
*/
|
|
392
|
+
export function inputRefsWrites(refsRoot, key, next) {
|
|
393
|
+
if (!isInputKey(key))
|
|
394
|
+
throw new TypeError('Not a valid input key');
|
|
395
|
+
const wanted = new Map();
|
|
396
|
+
for (const ref of next ?? []) {
|
|
397
|
+
const parsed = parseInputReference(ref);
|
|
398
|
+
if (parsed === null)
|
|
399
|
+
throw new TypeError('Not a valid input reference');
|
|
400
|
+
wanted.set(inputRefEntryKey(key, inputReferenceId(parsed)), parsed);
|
|
401
|
+
}
|
|
402
|
+
const writes = { set: {}, delete: [] };
|
|
403
|
+
for (const entryKey of Object.keys(refsRoot).sort()) {
|
|
404
|
+
if (parseInputRefEntryKey(entryKey)?.key === key && !wanted.has(entryKey)) {
|
|
405
|
+
writes.delete.push(entryKey);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const entryKey of [...wanted.keys()].sort()) {
|
|
409
|
+
const ref = wanted.get(entryKey);
|
|
410
|
+
if (!sameJson(refsRoot[entryKey], ref))
|
|
411
|
+
writes.set[entryKey] = ref;
|
|
412
|
+
}
|
|
413
|
+
return writes;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* @internal
|
|
417
|
+
* The writes that record `mark` for `docId` under `key`, given the read root
|
|
418
|
+
* as this writer sees it: the mark's entry, and the deletion of every earlier
|
|
419
|
+
* mark for that document. A later mark already present is left in place, and
|
|
420
|
+
* still wins.
|
|
421
|
+
*/
|
|
422
|
+
export function inputReadMarkWrites(readRoot, key, docId, mark) {
|
|
423
|
+
if (!isInputKey(key) || !isInputDocId(docId) || parseInputReadMark(mark) === null) {
|
|
424
|
+
throw new TypeError('Not a valid read mark');
|
|
425
|
+
}
|
|
426
|
+
const own = inputReadEntryKey(key, docId, mark);
|
|
427
|
+
const writes = { set: {}, delete: [] };
|
|
428
|
+
for (const entryKey of Object.keys(readRoot).sort()) {
|
|
429
|
+
const parsed = parseInputReadEntryKey(entryKey);
|
|
430
|
+
if (parsed?.key === key && parsed.docId === docId && inputReadMarkAfter(mark, parsed.mark)) {
|
|
431
|
+
writes.delete.push(entryKey);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (readRoot[own] !== true)
|
|
435
|
+
writes.set[own] = true;
|
|
436
|
+
return writes;
|
|
437
|
+
}
|
|
438
|
+
/** @internal The writes that clear every read mark under `key`, as when the key is cleared. */
|
|
439
|
+
export function inputReadClearWrites(readRoot, key) {
|
|
440
|
+
const prefix = inputReadEntryPrefix(key);
|
|
441
|
+
return { set: {}, delete: Object.keys(readRoot).filter((k) => k.startsWith(prefix)).sort() };
|
|
442
|
+
}
|
|
443
|
+
function sameJson(a, b) {
|
|
444
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
445
|
+
}
|
|
446
|
+
/** @internal */
|
|
447
|
+
export const INPUT_FILE_CHANGES = ['added', 'modified', 'removed'];
|
|
448
|
+
/** @internal The most keys one page of the keyless status carries. */
|
|
449
|
+
export const INPUT_KEYS_PAGE_SIZE = 50;
|
|
450
|
+
/** @internal The most changed entries one page of the keyless status carries. */
|
|
451
|
+
export const INPUT_KEYS_CHANGED_MAX = INPUT_DOCS_MAX;
|
|
452
|
+
/** @internal Query parameters on `GET` of a key's status. */
|
|
453
|
+
export const INPUT_STATUS_SINCE_PARAM = 'since_rev';
|
|
454
|
+
/** @internal */
|
|
455
|
+
export const INPUT_STATUS_CURSOR_PARAM = 'cursor';
|
|
456
|
+
/**
|
|
457
|
+
* @internal
|
|
458
|
+
* The line whose keys a `GET` reads: a branch or a stream of the document,
|
|
459
|
+
* named as a document join names it. Absent reads the document's default
|
|
460
|
+
* branch.
|
|
461
|
+
*/
|
|
462
|
+
export const INPUT_STATUS_LINE_PARAM = 'branch';
|
|
463
|
+
/** @internal The most documents one page or one delta carries. */
|
|
464
|
+
export const INPUT_STATUS_PAGE_SIZE = 500;
|
|
465
|
+
/**
|
|
466
|
+
* @internal
|
|
467
|
+
* A page cursor: the revision the first page answered at and the last doc id
|
|
468
|
+
* the previous page held. Pinning the revision is what keeps a paged read
|
|
469
|
+
* consistent; a server whose status moved since answers a resync, and the
|
|
470
|
+
* reader starts again.
|
|
471
|
+
*/
|
|
472
|
+
export function encodeInputStatusCursor(cursor) {
|
|
473
|
+
return `${cursor.rev}.${cursor.after}`;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* @internal
|
|
477
|
+
* A keyless status cursor: `encodeInputStatusCursor` over the revision the
|
|
478
|
+
* first page answered at and the last key the previous page held.
|
|
479
|
+
*/
|
|
480
|
+
export function decodeInputKeysCursor(raw) {
|
|
481
|
+
if (typeof raw !== 'string')
|
|
482
|
+
return null;
|
|
483
|
+
const m = /^(0|[1-9][0-9]{0,14})\.(.+)$/s.exec(raw);
|
|
484
|
+
if (!m || !m[1] || !m[2] || !isInputKey(m[2]))
|
|
485
|
+
return null;
|
|
486
|
+
return { rev: Number(m[1]), after: m[2] };
|
|
487
|
+
}
|
|
488
|
+
/** @internal */
|
|
489
|
+
export function decodeInputStatusCursor(raw) {
|
|
490
|
+
if (typeof raw !== 'string')
|
|
491
|
+
return null;
|
|
492
|
+
const m = /^(0|[1-9][0-9]{0,14})\.(doc_[A-Za-z0-9_-]{32})$/.exec(raw);
|
|
493
|
+
if (!m || !m[1] || !m[2])
|
|
494
|
+
return null;
|
|
495
|
+
return { rev: Number(m[1]), after: m[2] };
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* @internal
|
|
499
|
+
* The status a delta moves `status` to, or null when the delta does not start
|
|
500
|
+
* at `status.rev` (a gap, which the reader closes by fetching again).
|
|
501
|
+
*/
|
|
502
|
+
export function applyInputStatusDelta(status, delta) {
|
|
503
|
+
if (delta.key !== status.key || delta.since_rev !== status.rev)
|
|
504
|
+
return null;
|
|
505
|
+
return {
|
|
506
|
+
key: status.key,
|
|
507
|
+
refs: delta.refs ?? status.refs,
|
|
508
|
+
files: applyUpserts(status.files, delta.files),
|
|
509
|
+
changed: applyUpserts(status.changed, delta.changed),
|
|
510
|
+
hidden_count: delta.hidden_count,
|
|
511
|
+
truncated: delta.truncated,
|
|
512
|
+
rev: delta.rev,
|
|
513
|
+
...(delta.stale === true ? { stale: true } : {}),
|
|
514
|
+
...(delta.arriving === true ? { arriving: true } : {}),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function applyUpserts(current, diff) {
|
|
518
|
+
const byId = new Map(current.map((e) => [e.doc_id, e]));
|
|
519
|
+
for (const id of diff.remove)
|
|
520
|
+
byId.delete(id);
|
|
521
|
+
for (const e of diff.upsert)
|
|
522
|
+
byId.set(e.doc_id, e);
|
|
523
|
+
return [...byId.values()].sort((a, b) => (a.doc_id < b.doc_id ? -1 : a.doc_id > b.doc_id ? 1 : 0));
|
|
524
|
+
}
|
|
525
|
+
/** @internal Reads a key's status (or a page of one) off the wire. Null when malformed. */
|
|
526
|
+
export function parseInputStatus(raw) {
|
|
527
|
+
if (!isRecord(raw) || !isInputKey(raw['key']))
|
|
528
|
+
return null;
|
|
529
|
+
const refs = parseList(raw['refs'], parseInputReference);
|
|
530
|
+
const files = parseList(raw['files'], parseInputStatusFile);
|
|
531
|
+
const changed = parseList(raw['changed'], parseInputStatusChange);
|
|
532
|
+
const counts = parseCounts(raw);
|
|
533
|
+
const stale = parseStale(raw);
|
|
534
|
+
const arriving = parseArriving(raw);
|
|
535
|
+
if (refs === null || files === null || changed === null || counts === null || stale === null || arriving === null) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
return { key: raw['key'], refs, files, changed, ...counts, ...stale, ...arriving };
|
|
539
|
+
}
|
|
540
|
+
/** @internal */
|
|
541
|
+
export function parseInputStatusPage(raw) {
|
|
542
|
+
const status = parseInputStatus(raw);
|
|
543
|
+
if (status === null || !isRecord(raw))
|
|
544
|
+
return null;
|
|
545
|
+
const cursor = raw['cursor'];
|
|
546
|
+
if (cursor !== null && decodeInputStatusCursor(cursor) === null)
|
|
547
|
+
return null;
|
|
548
|
+
return { ...status, cursor: cursor };
|
|
549
|
+
}
|
|
550
|
+
/** @internal Reads a keyless status page, or the resync a moved cursor draws. Null when malformed. */
|
|
551
|
+
export function parseInputStatusAll(raw) {
|
|
552
|
+
if (!isRecord(raw))
|
|
553
|
+
return null;
|
|
554
|
+
if (raw['resync'] === true)
|
|
555
|
+
return isRev(raw['rev']) ? { rev: raw['rev'], resync: true } : null;
|
|
556
|
+
const cursor = raw['cursor'] ?? null;
|
|
557
|
+
if (cursor !== null && decodeInputKeysCursor(cursor) === null)
|
|
558
|
+
return null;
|
|
559
|
+
const keys = raw['keys'];
|
|
560
|
+
if (!Array.isArray(keys) || !keys.every(isInputKey))
|
|
561
|
+
return null;
|
|
562
|
+
const changed = parseList(raw['changed'], parseInputStatusChange);
|
|
563
|
+
const counts = parseCounts(raw);
|
|
564
|
+
const stale = parseStale(raw);
|
|
565
|
+
if (changed === null || counts === null || stale === null)
|
|
566
|
+
return null;
|
|
567
|
+
const keyed = [];
|
|
568
|
+
for (const c of changed) {
|
|
569
|
+
if (c.key === undefined)
|
|
570
|
+
return null;
|
|
571
|
+
keyed.push({ ...c, key: c.key });
|
|
572
|
+
}
|
|
573
|
+
return { keys: [...keys], changed: keyed, ...counts, ...stale, cursor: cursor };
|
|
574
|
+
}
|
|
575
|
+
/** @internal Reads a `since_rev` answer: a delta, a resync, or null when malformed. */
|
|
576
|
+
export function parseInputStatusDelta(raw) {
|
|
577
|
+
if (!isRecord(raw) || !isInputKey(raw['key']))
|
|
578
|
+
return null;
|
|
579
|
+
const rev = raw['rev'];
|
|
580
|
+
if (!isRev(rev))
|
|
581
|
+
return null;
|
|
582
|
+
if (raw['resync'] === true)
|
|
583
|
+
return { key: raw['key'], rev, resync: true };
|
|
584
|
+
const since = raw['since_rev'];
|
|
585
|
+
if (!isRev(since) || since > rev)
|
|
586
|
+
return null;
|
|
587
|
+
const files = parseDiff(raw['files'], parseInputStatusFile);
|
|
588
|
+
const changed = parseDiff(raw['changed'], parseInputStatusChange);
|
|
589
|
+
const counts = parseCounts(raw);
|
|
590
|
+
const stale = parseStale(raw);
|
|
591
|
+
const arriving = parseArriving(raw);
|
|
592
|
+
if (files === null || changed === null || counts === null || stale === null || arriving === null)
|
|
593
|
+
return null;
|
|
594
|
+
const delta = {
|
|
595
|
+
key: raw['key'],
|
|
596
|
+
since_rev: since,
|
|
597
|
+
rev,
|
|
598
|
+
files,
|
|
599
|
+
changed,
|
|
600
|
+
hidden_count: counts.hidden_count,
|
|
601
|
+
truncated: counts.truncated,
|
|
602
|
+
...stale,
|
|
603
|
+
...arriving,
|
|
604
|
+
};
|
|
605
|
+
if (raw['refs'] !== undefined) {
|
|
606
|
+
const refs = parseList(raw['refs'], parseInputReference);
|
|
607
|
+
if (refs === null)
|
|
608
|
+
return null;
|
|
609
|
+
delta.refs = refs;
|
|
610
|
+
}
|
|
611
|
+
return delta;
|
|
612
|
+
}
|
|
613
|
+
function parseInputStatusFile(raw) {
|
|
614
|
+
if (!isRecord(raw) || !isInputDocId(raw['doc_id']))
|
|
615
|
+
return null;
|
|
616
|
+
const updatedAt = raw['updated_at'];
|
|
617
|
+
if (typeof updatedAt !== 'string' || updatedAt.length > INPUT_STAMP_MAX_CHARS)
|
|
618
|
+
return null;
|
|
619
|
+
const name = raw['name'];
|
|
620
|
+
if (typeof name !== 'string')
|
|
621
|
+
return null;
|
|
622
|
+
const from = parseFrom(raw['from']);
|
|
623
|
+
const ref = parseInputReference(raw['ref']);
|
|
624
|
+
if (from === null || ref === null)
|
|
625
|
+
return null;
|
|
626
|
+
const schemaType = raw['schema_type'];
|
|
627
|
+
if (schemaType !== undefined && typeof schemaType !== 'string')
|
|
628
|
+
return null;
|
|
629
|
+
const awaitsImport = raw['awaits_import'];
|
|
630
|
+
if (awaitsImport !== undefined && awaitsImport !== true)
|
|
631
|
+
return null;
|
|
632
|
+
return {
|
|
633
|
+
doc_id: raw['doc_id'],
|
|
634
|
+
name,
|
|
635
|
+
...(schemaType !== undefined ? { schema_type: schemaType } : {}),
|
|
636
|
+
updated_at: updatedAt,
|
|
637
|
+
from,
|
|
638
|
+
ref,
|
|
639
|
+
...(awaitsImport === true ? { awaits_import: true } : {}),
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function parseInputStatusChange(raw) {
|
|
643
|
+
if (!isRecord(raw) || !isInputDocId(raw['doc_id']))
|
|
644
|
+
return null;
|
|
645
|
+
const change = raw['change'];
|
|
646
|
+
if (!INPUT_FILE_CHANGES.includes(change))
|
|
647
|
+
return null;
|
|
648
|
+
const from = parseFrom(raw['from']);
|
|
649
|
+
if (from === null)
|
|
650
|
+
return null;
|
|
651
|
+
const out = { doc_id: raw['doc_id'], change: change, from };
|
|
652
|
+
if (raw['key'] !== undefined) {
|
|
653
|
+
if (!isInputKey(raw['key']))
|
|
654
|
+
return null;
|
|
655
|
+
out.key = raw['key'];
|
|
656
|
+
}
|
|
657
|
+
if (raw['updated_at'] !== undefined) {
|
|
658
|
+
const at = raw['updated_at'];
|
|
659
|
+
if (typeof at !== 'string' || at.length > INPUT_STAMP_MAX_CHARS)
|
|
660
|
+
return null;
|
|
661
|
+
out.updated_at = at;
|
|
662
|
+
}
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
665
|
+
function parseFrom(raw) {
|
|
666
|
+
return Array.isArray(raw) && raw.every(isInputReferenceId) ? [...raw] : null;
|
|
667
|
+
}
|
|
668
|
+
function parseCounts(raw) {
|
|
669
|
+
const hidden = raw['hidden_count'];
|
|
670
|
+
const truncated = raw['truncated'];
|
|
671
|
+
const rev = raw['rev'];
|
|
672
|
+
if (!isRev(hidden) || typeof truncated !== 'boolean' || !isRev(rev))
|
|
673
|
+
return null;
|
|
674
|
+
return { hidden_count: hidden, truncated, rev };
|
|
675
|
+
}
|
|
676
|
+
// `stale` is optional on the wire and carried only when true.
|
|
677
|
+
function parseStale(raw) {
|
|
678
|
+
const stale = raw['stale'];
|
|
679
|
+
if (stale === undefined || stale === false)
|
|
680
|
+
return {};
|
|
681
|
+
return stale === true ? { stale: true } : null;
|
|
682
|
+
}
|
|
683
|
+
// `arriving` is optional on the wire and carried only when true.
|
|
684
|
+
function parseArriving(raw) {
|
|
685
|
+
const arriving = raw['arriving'];
|
|
686
|
+
if (arriving === undefined || arriving === false)
|
|
687
|
+
return {};
|
|
688
|
+
return arriving === true ? { arriving: true } : null;
|
|
689
|
+
}
|
|
690
|
+
function parseDiff(raw, parse) {
|
|
691
|
+
if (!isRecord(raw))
|
|
692
|
+
return null;
|
|
693
|
+
const upsert = parseList(raw['upsert'], parse);
|
|
694
|
+
const remove = raw['remove'];
|
|
695
|
+
if (upsert === null || !Array.isArray(remove) || !remove.every(isInputDocId))
|
|
696
|
+
return null;
|
|
697
|
+
return { upsert, remove: [...remove] };
|
|
698
|
+
}
|
|
699
|
+
function parseList(raw, parse) {
|
|
700
|
+
if (!Array.isArray(raw))
|
|
701
|
+
return null;
|
|
702
|
+
const out = [];
|
|
703
|
+
for (const entry of raw) {
|
|
704
|
+
const parsed = parse(entry);
|
|
705
|
+
if (parsed === null)
|
|
706
|
+
return null;
|
|
707
|
+
out.push(parsed);
|
|
708
|
+
}
|
|
709
|
+
return out;
|
|
710
|
+
}
|
|
711
|
+
function isRev(value) {
|
|
712
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
713
|
+
}
|
|
714
|
+
// --- Live nudge (WS) --------------------------------------------------------
|
|
715
|
+
/** @internal */
|
|
716
|
+
export const INPUT_STATUS_FRAME_TYPE = 'doc_input_status';
|
|
717
|
+
/** @internal */
|
|
718
|
+
export function inputStatusNudgeFrame(docId, lineId, key, rev) {
|
|
719
|
+
return { type: INPUT_STATUS_FRAME_TYPE, topic: documentTopic(docId, lineId), key, rev };
|
|
720
|
+
}
|
|
721
|
+
/** @internal Reads a nudge frame. Null for any other frame or a malformed one. */
|
|
722
|
+
export function parseInputStatusNudge(raw) {
|
|
723
|
+
if (!isRecord(raw) || raw['type'] !== INPUT_STATUS_FRAME_TYPE)
|
|
724
|
+
return null;
|
|
725
|
+
const topic = typeof raw['topic'] === 'string' ? parseDocTopic(raw['topic']) : null;
|
|
726
|
+
if (topic === null || !isInputKey(raw['key']) || !isRev(raw['rev']))
|
|
727
|
+
return null;
|
|
728
|
+
return { docId: topic.docId, lineId: topic.branchId, key: raw['key'], rev: raw['rev'] };
|
|
729
|
+
}
|
|
730
|
+
/** @internal */
|
|
731
|
+
export function parseProvisionalInputsAnswer(raw) {
|
|
732
|
+
if (!isRecord(raw) || !isInputKey(raw['key']) || !isRev(raw['rev']))
|
|
733
|
+
return null;
|
|
734
|
+
if (typeof raw['written'] !== 'boolean')
|
|
735
|
+
return null;
|
|
736
|
+
return { key: raw['key'], rev: raw['rev'], written: raw['written'] };
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* @internal
|
|
740
|
+
* Reads a provisional body for document `docId`. Refuses a reference to any
|
|
741
|
+
* stream of `docId` itself, and references that repeat one identity.
|
|
742
|
+
*/
|
|
743
|
+
export function parseProvisionalInputsBody(raw, opts) {
|
|
744
|
+
if (!isRecord(raw))
|
|
745
|
+
return fail('Body must be an object');
|
|
746
|
+
const { branch_id: lineId, op_seq: opSeq } = raw;
|
|
747
|
+
if (!isInputLineId(lineId))
|
|
748
|
+
return fail('branch_id is not a valid line id');
|
|
749
|
+
if (typeof opSeq !== 'number' || !Number.isSafeInteger(opSeq) || opSeq < 1) {
|
|
750
|
+
return fail('op_seq must be a positive integer');
|
|
751
|
+
}
|
|
752
|
+
const refsRaw = raw['refs'];
|
|
753
|
+
if (!Array.isArray(refsRaw))
|
|
754
|
+
return fail('refs must be an array');
|
|
755
|
+
if (refsRaw.length > INPUT_REFS_MAX_PER_KEY)
|
|
756
|
+
return fail('Too many references');
|
|
757
|
+
const refs = [];
|
|
758
|
+
const seen = new Set();
|
|
759
|
+
for (const entry of refsRaw) {
|
|
760
|
+
const ref = parseInputReference(entry);
|
|
761
|
+
if (ref === null)
|
|
762
|
+
return fail('refs holds an invalid reference');
|
|
763
|
+
if (ref.extent === 'document' && ref.doc_id === opts.docId) {
|
|
764
|
+
return fail('A document cannot reference itself');
|
|
765
|
+
}
|
|
766
|
+
const id = inputReferenceId(ref);
|
|
767
|
+
if (seen.has(id))
|
|
768
|
+
return fail('refs repeats a reference');
|
|
769
|
+
seen.add(id);
|
|
770
|
+
refs.push(ref);
|
|
771
|
+
}
|
|
772
|
+
const readRaw = raw['read'];
|
|
773
|
+
if (!isRecord(readRaw))
|
|
774
|
+
return fail('read must be an object');
|
|
775
|
+
const docIds = Object.keys(readRaw);
|
|
776
|
+
if (docIds.length > INPUT_READ_MARKS_MAX_PER_KEY)
|
|
777
|
+
return fail('Too many read marks');
|
|
778
|
+
const read = {};
|
|
779
|
+
for (const docId of docIds) {
|
|
780
|
+
const mark = parseInputReadMark(readRaw[docId]);
|
|
781
|
+
if (!isInputDocId(docId) || mark === null)
|
|
782
|
+
return fail('read holds an invalid mark');
|
|
783
|
+
read[docId] = mark;
|
|
784
|
+
}
|
|
785
|
+
return { ok: true, value: { branch_id: lineId, op_seq: opSeq, refs, read } };
|
|
786
|
+
}
|
|
787
|
+
function fail(error) {
|
|
788
|
+
return { ok: false, error };
|
|
789
|
+
}
|
|
790
|
+
function isRecord(value) {
|
|
791
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
792
|
+
}
|
|
793
|
+
//# sourceMappingURL=inputs.js.map
|