@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
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// The handle surface over a static record store.
|
|
2
|
+
//
|
|
3
|
+
// A document's encoding is not something an app is told about, so the roots of
|
|
4
|
+
// a static document are reached through the same `MapHandle` / `ListHandle` /
|
|
5
|
+
// `OrderedMapHandle` types a CRDT document's roots are: a projection built in
|
|
6
|
+
// a subscriber, a record resolved by key, a scoped handle onto one record's
|
|
7
|
+
// body. One read type over both encodings is what makes the encoding the
|
|
8
|
+
// platform's decision rather than the app's, so these implement the published
|
|
9
|
+
// interfaces rather than a read-only cousin of them.
|
|
10
|
+
//
|
|
11
|
+
// What they do not carry is a write. A record set holds no history for an edit
|
|
12
|
+
// to become, and building the CRDT that would hold one is the platform's
|
|
13
|
+
// (`runRecordSetConversion`), so a document served as a record set is one the
|
|
14
|
+
// reader asked to read. The write methods the interfaces declare therefore
|
|
15
|
+
// refuse through the document's own read-only gate, which names the reader's
|
|
16
|
+
// actual limit rather than the encoding.
|
|
17
|
+
//
|
|
18
|
+
// Reads stay as lazy as the store is. A keyed read decodes the one page that
|
|
19
|
+
// holds the record; `keys()` costs the pages' key lists; only a subscriber,
|
|
20
|
+
// which is asking for the whole collection by definition, walks every page.
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
Change,
|
|
24
|
+
HandleUnsubscribe,
|
|
25
|
+
TextHandle,
|
|
26
|
+
TextSubscriber,
|
|
27
|
+
ListHandle,
|
|
28
|
+
ListSubscriber,
|
|
29
|
+
MapHandle,
|
|
30
|
+
MapSubscriber,
|
|
31
|
+
OrderedMapHandle,
|
|
32
|
+
Splice,
|
|
33
|
+
} from './handle-types.js';
|
|
34
|
+
import { pushBoundedSplice } from './subscriber-delivery.js';
|
|
35
|
+
import { SealedStore, type SealedContent, type SealedMapReader } from './sealed-store.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How a write through one of these handles is refused.
|
|
39
|
+
*
|
|
40
|
+
* The document's own gate, passed in rather than restated here, so the
|
|
41
|
+
* refusal names what the reader is actually up against (a snapshot they asked
|
|
42
|
+
* for, a view-only grant, a link) instead of describing how the platform
|
|
43
|
+
* happens to store the content. It always throws for a document served as a
|
|
44
|
+
* record set, because such a document is opened read-only; the throw that
|
|
45
|
+
* follows a gate that did not is the invariant stated where it is relied on.
|
|
46
|
+
*/
|
|
47
|
+
type RefuseWrite = () => void;
|
|
48
|
+
|
|
49
|
+
function refuse(assertWritable: RefuseWrite): never {
|
|
50
|
+
assertWritable();
|
|
51
|
+
throw new Error('Document is read-only: it holds a stored record set.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether a decoded body carries this entry itself. The prototype chain is
|
|
55
|
+
* not part of a record, so a key an app never wrote must not resolve through
|
|
56
|
+
* it. */
|
|
57
|
+
function ownEntry(object: Record<string, unknown>, key: string): boolean {
|
|
58
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The subscribers of one handle, re-delivered when the content it reads is
|
|
62
|
+
* swapped. */
|
|
63
|
+
class RootSubscribers<C> {
|
|
64
|
+
private readonly _subs = new Set<C>();
|
|
65
|
+
|
|
66
|
+
add(cb: C, deliver: (cb: C) => void): HandleUnsubscribe {
|
|
67
|
+
this._subs.add(cb);
|
|
68
|
+
deliver(cb);
|
|
69
|
+
return () => {
|
|
70
|
+
this._subs.delete(cb);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
each(visit: (cb: C) => void): void {
|
|
75
|
+
for (const cb of this._subs) {
|
|
76
|
+
try {
|
|
77
|
+
visit(cb);
|
|
78
|
+
} catch {
|
|
79
|
+
// A throwing subscriber cannot strand the others.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A stored map root as a map handle, and as the ordered map its stored
|
|
86
|
+
* order makes it. */
|
|
87
|
+
class SealedRecordMap<T> implements MapHandle<T>, OrderedMapHandle<T> {
|
|
88
|
+
private readonly _subs = new RootSubscribers<MapSubscriber<T>>();
|
|
89
|
+
private readonly _settled = new RootSubscribers<() => void>();
|
|
90
|
+
// The keys each subscriber was last handed, so a swap reports the ones the
|
|
91
|
+
// new content no longer holds as deleted.
|
|
92
|
+
private readonly _delivered = new Map<MapSubscriber<T>, string[]>();
|
|
93
|
+
|
|
94
|
+
/** `content` resolves the content this document shows now. A root that
|
|
95
|
+
* content does not hold reads as the empty collection a CRDT document
|
|
96
|
+
* reports for a container nothing has written.
|
|
97
|
+
*
|
|
98
|
+
* `_ordered` is which of the two interfaces this instance was taken as. A
|
|
99
|
+
* plain map's change carries no `order`, because that field is how an
|
|
100
|
+
* ordered map tells a subscriber to re-sequence; carrying it on a plain
|
|
101
|
+
* map's fire would ask for work the app never asked to do. */
|
|
102
|
+
constructor(
|
|
103
|
+
private readonly _content: () => SealedContent,
|
|
104
|
+
private readonly _name: string,
|
|
105
|
+
private readonly _refuse: RefuseWrite,
|
|
106
|
+
private readonly _ordered: boolean,
|
|
107
|
+
) {}
|
|
108
|
+
|
|
109
|
+
private _map(): SealedMapReader | null {
|
|
110
|
+
return this._content().mapRoot(this._name);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get(entryKey: string): T | undefined {
|
|
114
|
+
return this._map()?.get(entryKey) as T | undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
has(entryKey: string): boolean {
|
|
118
|
+
return this._map()?.has(entryKey) ?? false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
keys(): string[] {
|
|
122
|
+
return this._map()?.keys() ?? [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
*entries(): IterableIterator<[string, T]> {
|
|
126
|
+
const map = this._map();
|
|
127
|
+
if (map !== null) yield* map.entries() as IterableIterator<[string, T]>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @internal The whole collection at once. */
|
|
131
|
+
read(): Record<string, T> {
|
|
132
|
+
const out: Record<string, T> = {};
|
|
133
|
+
for (const [key, value] of this.entries()) out[key] = value;
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
ordered(): { key: string; value: T }[] {
|
|
138
|
+
const out: { key: string; value: T }[] = [];
|
|
139
|
+
for (const [key, value] of this.entries()) out.push({ key, value });
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
subscribe(cb: MapSubscriber<T>): HandleUnsubscribe {
|
|
144
|
+
const off = this._subs.add(cb, (sub) => this._deliver(sub));
|
|
145
|
+
return () => {
|
|
146
|
+
off();
|
|
147
|
+
this._delivered.delete(cb);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private _deliver(cb: MapSubscriber<T>): void {
|
|
152
|
+
const set = new Map<string, T>();
|
|
153
|
+
const order: string[] = [];
|
|
154
|
+
const map = this._map();
|
|
155
|
+
if (map !== null) {
|
|
156
|
+
for (const [key, value] of map.entries() as IterableIterator<[string, T]>) {
|
|
157
|
+
set.set(key, value);
|
|
158
|
+
order.push(key);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const before = this._delivered.get(cb) ?? [];
|
|
162
|
+
const deleted = before.filter((key) => !set.has(key));
|
|
163
|
+
this._delivered.set(cb, order);
|
|
164
|
+
const change: Change<T> = this._ordered ? { set, deleted, order } : { set, deleted };
|
|
165
|
+
cb(change);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
onSettled(cb: () => void): HandleUnsubscribe {
|
|
169
|
+
return this._settled.add(cb, (sub) => sub());
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @internal Re-deliver every subscriber against the content now shown. */
|
|
173
|
+
_resync(): void {
|
|
174
|
+
this._subs.each((cb) => this._deliver(cb));
|
|
175
|
+
this._settled.each((cb) => cb());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
map<U>(entryKey: string): MapHandle<U> {
|
|
179
|
+
const value = this.get(entryKey);
|
|
180
|
+
return new SealedValueMap<U>(() => value, this._refuse);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
list<U>(entryKey: string): ListHandle<U> {
|
|
184
|
+
const value = this.get(entryKey);
|
|
185
|
+
return new SealedValueList<U>(() => value, this._refuse);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
mutate(): void {
|
|
189
|
+
refuse(this._refuse);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
insert(): void {
|
|
193
|
+
refuse(this._refuse);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
move(): void {
|
|
197
|
+
refuse(this._refuse);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** A map handle over a plain value decoded out of the store: a map root that
|
|
202
|
+
* holds no records, a record, or a field inside one. A scoped handle onto an
|
|
203
|
+
* entry a CRDT document has not created yet reads as empty, and so does this
|
|
204
|
+
* one. `value` resolves at call time, so a root follows the content its
|
|
205
|
+
* document shows. */
|
|
206
|
+
class SealedValueMap<T> implements MapHandle<T> {
|
|
207
|
+
private readonly _subs = new RootSubscribers<MapSubscriber<T>>();
|
|
208
|
+
private readonly _settled = new RootSubscribers<() => void>();
|
|
209
|
+
private readonly _delivered = new Map<MapSubscriber<T>, string[]>();
|
|
210
|
+
|
|
211
|
+
constructor(
|
|
212
|
+
private readonly _value: () => unknown,
|
|
213
|
+
private readonly _refuse: RefuseWrite,
|
|
214
|
+
) {}
|
|
215
|
+
|
|
216
|
+
private _object(): Record<string, unknown> {
|
|
217
|
+
const value = this._value();
|
|
218
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
219
|
+
? (value as Record<string, unknown>)
|
|
220
|
+
: {};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
get(entryKey: string): T | undefined {
|
|
224
|
+
const object = this._object();
|
|
225
|
+
return ownEntry(object, entryKey) ? (object[entryKey] as T) : undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
has(entryKey: string): boolean {
|
|
229
|
+
return ownEntry(this._object(), entryKey);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
keys(): string[] {
|
|
233
|
+
return Object.keys(this._object());
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
*entries(): IterableIterator<[string, T]> {
|
|
237
|
+
const object = this._object();
|
|
238
|
+
for (const key of Object.keys(object)) yield [key, object[key] as T];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** @internal The whole collection at once. */
|
|
242
|
+
read(): Record<string, T> {
|
|
243
|
+
return { ...this._object() } as Record<string, T>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
subscribe(cb: MapSubscriber<T>): HandleUnsubscribe {
|
|
247
|
+
const off = this._subs.add(cb, (sub) => this._deliver(sub));
|
|
248
|
+
return () => {
|
|
249
|
+
off();
|
|
250
|
+
this._delivered.delete(cb);
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private _deliver(cb: MapSubscriber<T>): void {
|
|
255
|
+
const object = this._object();
|
|
256
|
+
const keys = Object.keys(object);
|
|
257
|
+
const before = this._delivered.get(cb) ?? [];
|
|
258
|
+
this._delivered.set(cb, keys);
|
|
259
|
+
cb({
|
|
260
|
+
set: new Map(keys.map((key) => [key, object[key] as T])),
|
|
261
|
+
deleted: before.filter((key) => !ownEntry(object, key)),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
onSettled(cb: () => void): HandleUnsubscribe {
|
|
266
|
+
return this._settled.add(cb, (sub) => sub());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** @internal */
|
|
270
|
+
_resync(): void {
|
|
271
|
+
this._subs.each((cb) => this._deliver(cb));
|
|
272
|
+
this._settled.each((cb) => cb());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
map<U>(entryKey: string): MapHandle<U> {
|
|
276
|
+
const value = this._object()[entryKey];
|
|
277
|
+
return new SealedValueMap<U>(() => value, this._refuse);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
list<U>(entryKey: string): ListHandle<U> {
|
|
281
|
+
const value = this._object()[entryKey];
|
|
282
|
+
return new SealedValueList<U>(() => value, this._refuse);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
mutate(): void {
|
|
286
|
+
refuse(this._refuse);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
class SealedValueList<T> implements ListHandle<T> {
|
|
291
|
+
private readonly _subs = new RootSubscribers<ListSubscriber<T>>();
|
|
292
|
+
private readonly _settled = new RootSubscribers<() => void>();
|
|
293
|
+
private readonly _delivered = new Map<ListSubscriber<T>, number>();
|
|
294
|
+
|
|
295
|
+
constructor(
|
|
296
|
+
private readonly _value: () => unknown,
|
|
297
|
+
private readonly _refuse: RefuseWrite,
|
|
298
|
+
) {}
|
|
299
|
+
|
|
300
|
+
private _array(): T[] {
|
|
301
|
+
const value = this._value();
|
|
302
|
+
return Array.isArray(value) ? (value as T[]) : [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
length(): number {
|
|
306
|
+
return this._array().length;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
get(index: number): T | undefined {
|
|
310
|
+
if (!Number.isInteger(index) || index < 0) return undefined;
|
|
311
|
+
return this._array()[index];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
*values(): IterableIterator<T> {
|
|
315
|
+
yield* this._array();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** @internal The whole list at once. */
|
|
319
|
+
read(): T[] {
|
|
320
|
+
return [...this._array()];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
subscribe(cb: ListSubscriber<T>): HandleUnsubscribe {
|
|
324
|
+
const off = this._subs.add(cb, (sub) => this._deliver(sub));
|
|
325
|
+
return () => {
|
|
326
|
+
off();
|
|
327
|
+
this._delivered.delete(cb);
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private _deliver(cb: ListSubscriber<T>): void {
|
|
332
|
+
const items = this._array();
|
|
333
|
+
const removed = this._delivered.get(cb) ?? 0;
|
|
334
|
+
this._delivered.set(cb, items.length);
|
|
335
|
+
const splices: Splice<T>[] = [];
|
|
336
|
+
pushBoundedSplice(splices, { at: 0, removed, inserted: items });
|
|
337
|
+
cb({ splices });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
onSettled(cb: () => void): HandleUnsubscribe {
|
|
341
|
+
return this._settled.add(cb, (sub) => sub());
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** @internal */
|
|
345
|
+
_resync(): void {
|
|
346
|
+
this._subs.each((cb) => this._deliver(cb));
|
|
347
|
+
this._settled.each((cb) => cb());
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
mutate(): void {
|
|
351
|
+
refuse(this._refuse);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** A text root. */
|
|
356
|
+
class SealedText implements TextHandle {
|
|
357
|
+
private readonly _subs = new RootSubscribers<TextSubscriber>();
|
|
358
|
+
|
|
359
|
+
constructor(
|
|
360
|
+
private readonly _value: () => string,
|
|
361
|
+
private readonly _refuse: RefuseWrite,
|
|
362
|
+
) {}
|
|
363
|
+
|
|
364
|
+
read(): string {
|
|
365
|
+
return this._value();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
update(): void {
|
|
369
|
+
refuse(this._refuse);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
subscribe(cb: TextSubscriber): HandleUnsubscribe {
|
|
373
|
+
return this._subs.add(cb, (sub) => sub(this._value()));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** @internal */
|
|
377
|
+
_resync(): void {
|
|
378
|
+
this._subs.each((cb) => cb(this._value()));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** The roots of a document over frozen content. */
|
|
383
|
+
export type SealedRoots = ReturnType<typeof sealedRoots>;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The roots of a document over frozen content, whatever the content declares.
|
|
387
|
+
*
|
|
388
|
+
* `stored` may resolve the content the document shows now, so a version
|
|
389
|
+
* checkout swaps what every root reads and `resync` re-delivers the handles an
|
|
390
|
+
* app already holds. Each root handle is made once per key, which is what lets
|
|
391
|
+
* that reach them.
|
|
392
|
+
*
|
|
393
|
+
* A root the content does not hold reads as an empty map, list or text rather
|
|
394
|
+
* than throwing, which is what a CRDT document does for a container nothing
|
|
395
|
+
* has written: an app that reaches for a root of its own on an imported
|
|
396
|
+
* document gets the same empty answer either way.
|
|
397
|
+
*/
|
|
398
|
+
export function sealedRoots(
|
|
399
|
+
stored: SealedContent | SealedStore | (() => SealedContent),
|
|
400
|
+
refuseWrite: RefuseWrite,
|
|
401
|
+
) {
|
|
402
|
+
const content: () => SealedContent =
|
|
403
|
+
typeof stored === 'function'
|
|
404
|
+
? stored
|
|
405
|
+
: ((fixed) => () => fixed)(stored instanceof SealedStore ? stored.content : stored);
|
|
406
|
+
// Decoded lists, per content, so a checkout never reads a stale memo.
|
|
407
|
+
const lists = new WeakMap<SealedContent, Map<string, unknown[]>>();
|
|
408
|
+
const listOf = (key: string): unknown[] | undefined => {
|
|
409
|
+
const current = content();
|
|
410
|
+
if (current.kindOf(key) !== 'list') return undefined;
|
|
411
|
+
let memo = lists.get(current);
|
|
412
|
+
if (memo === undefined) {
|
|
413
|
+
memo = new Map();
|
|
414
|
+
lists.set(current, memo);
|
|
415
|
+
}
|
|
416
|
+
let items = memo.get(key);
|
|
417
|
+
if (items === undefined) {
|
|
418
|
+
items = current.list(key) ?? [];
|
|
419
|
+
memo.set(key, items);
|
|
420
|
+
}
|
|
421
|
+
return items;
|
|
422
|
+
};
|
|
423
|
+
const roots = new Map<string, { _resync(): void }>();
|
|
424
|
+
const once = <H extends { _resync(): void }>(cacheKey: string, make: () => H): H => {
|
|
425
|
+
let handle = roots.get(cacheKey) as H | undefined;
|
|
426
|
+
if (handle === undefined) {
|
|
427
|
+
handle = make();
|
|
428
|
+
roots.set(cacheKey, handle);
|
|
429
|
+
}
|
|
430
|
+
return handle;
|
|
431
|
+
};
|
|
432
|
+
return {
|
|
433
|
+
map<T>(key: string): MapHandle<T> {
|
|
434
|
+
return once(
|
|
435
|
+
`map:${key}`,
|
|
436
|
+
() => new SealedRecordMap<unknown>(content, key, refuseWrite, false),
|
|
437
|
+
) as MapHandle<T>;
|
|
438
|
+
},
|
|
439
|
+
list<T>(key: string): ListHandle<T> {
|
|
440
|
+
return once(
|
|
441
|
+
`list:${key}`,
|
|
442
|
+
() => new SealedValueList<unknown>(() => listOf(key), refuseWrite),
|
|
443
|
+
) as ListHandle<T>;
|
|
444
|
+
},
|
|
445
|
+
text(key: string): TextHandle {
|
|
446
|
+
return once(`text:${key}`, () => new SealedText(() => content().text(key) ?? '', refuseWrite));
|
|
447
|
+
},
|
|
448
|
+
orderedMap<T>(mapKey: string, _orderKey: string): OrderedMapHandle<T> {
|
|
449
|
+
// A stored map keeps its entries in the order its order list gave
|
|
450
|
+
// them, so the map's own walk is the ordered map's order.
|
|
451
|
+
return once(
|
|
452
|
+
`ordered:${mapKey}`,
|
|
453
|
+
() => new SealedRecordMap<unknown>(content, mapKey, refuseWrite, true),
|
|
454
|
+
) as OrderedMapHandle<T>;
|
|
455
|
+
},
|
|
456
|
+
/** Re-deliver every root handle's subscribers against the content now
|
|
457
|
+
* shown. */
|
|
458
|
+
resync(): void {
|
|
459
|
+
for (const handle of roots.values()) handle._resync();
|
|
460
|
+
},
|
|
461
|
+
/** The kind of every root that holds something, in key order, the way a
|
|
462
|
+
* CRDT document's `roots()` reports them. */
|
|
463
|
+
roots(): Record<string, 'map' | 'list' | 'text'> {
|
|
464
|
+
const current = content();
|
|
465
|
+
const found: [string, 'map' | 'list' | 'text'][] = [];
|
|
466
|
+
for (const name of current.rootNames()) {
|
|
467
|
+
const kind = current.kindOf(name);
|
|
468
|
+
if (kind === undefined) continue;
|
|
469
|
+
const empty =
|
|
470
|
+
kind === 'map'
|
|
471
|
+
? (current.mapRoot(name)?.size() ?? 0) === 0
|
|
472
|
+
: kind === 'list'
|
|
473
|
+
? (listOf(name)?.length ?? 0) === 0
|
|
474
|
+
: (current.text(name) ?? '') === '';
|
|
475
|
+
if (!empty) found.push([name, kind]);
|
|
476
|
+
}
|
|
477
|
+
found.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
478
|
+
return Object.fromEntries(found);
|
|
479
|
+
},
|
|
480
|
+
/** The whole document as plain JSON, empty roots included; the
|
|
481
|
+
* Document leaves those out. */
|
|
482
|
+
toJSON(): Record<string, unknown> {
|
|
483
|
+
return content().toJSON();
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|