@brass-build/client 0.4.1 → 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 +124 -45
- package/CHANGELOG.md +80 -4
- package/README.md +666 -179
- 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 +48 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +918 -115
- 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/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 +1022 -112
- 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,693 @@
|
|
|
1
|
+
// `subscribeInputDocuments`: calls an app's handlers with the input documents
|
|
2
|
+
// of a key, each as a handle the app may open in any mode. `loaded` gets every
|
|
3
|
+
// input wherever the document is open; `changed` gets the inputs changed since
|
|
4
|
+
// the document last read them, where it can be edited, and records each as
|
|
5
|
+
// read once its handler resolves; `removed` gets each input that left.
|
|
6
|
+
import { inputStampFromUpdatedAt } from './inputs.js';
|
|
7
|
+
import { inDevelopment } from './in-development.js';
|
|
8
|
+
import { importerWaitRejections } from './importer-wait.js';
|
|
9
|
+
/** A failed file waits this long before its first retry at the same
|
|
10
|
+
* version, doubling up to the ceiling. A new version is tried at once. A file
|
|
11
|
+
* refused `not_imported` is tried again when its import lands instead. */
|
|
12
|
+
const RETRY_FIRST_MS = 2_000;
|
|
13
|
+
const RETRY_CEILING_MS = 5 * 60_000;
|
|
14
|
+
/** `settled` waits for this long with no call starting or finishing and no
|
|
15
|
+
* change to the set, so a burst of arrivals reaches it once. */
|
|
16
|
+
const QUIET_MS = 2_000;
|
|
17
|
+
/** The handlers that run at once when the app sets no `concurrency`. Each
|
|
18
|
+
* handler usually opens its input, and a few hundred opens started together
|
|
19
|
+
* are refused as throttled, for this app and every other caller. */
|
|
20
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
21
|
+
/** A call pending this long is reported in development builds: the input
|
|
22
|
+
* gets no further call until it settles, and it keeps its concurrency slot. */
|
|
23
|
+
const LONG_PENDING_MS = 60_000;
|
|
24
|
+
/** On the input a shared follower hands out: `open` on behalf of one
|
|
25
|
+
* subscription, so a copy another subscription is using in the same call is
|
|
26
|
+
* not replaced. */
|
|
27
|
+
const OPEN_FOR = Symbol('openFor');
|
|
28
|
+
/** Options that open the same copy compare equal. */
|
|
29
|
+
const optionsKey = (options) => JSON.stringify(Object.entries(options ?? {}).filter(([, v]) => v !== undefined).sort(([a], [b]) => (a < b ? -1 : 1)));
|
|
30
|
+
const failKeyOf = (w) => `${w.kind}:${w.docId}@${w.version}`;
|
|
31
|
+
const newer = (version, than) => {
|
|
32
|
+
if (than === null)
|
|
33
|
+
return true;
|
|
34
|
+
const a = inputStampFromUpdatedAt(version);
|
|
35
|
+
const b = inputStampFromUpdatedAt(than);
|
|
36
|
+
return a !== null && b !== null ? a > b : version !== than;
|
|
37
|
+
};
|
|
38
|
+
/** Whether a copy was loaded at a later version than the call it was opened for. */
|
|
39
|
+
const openedAhead = (copyAt, callAt) => {
|
|
40
|
+
if (copyAt === null)
|
|
41
|
+
return false;
|
|
42
|
+
const a = inputStampFromUpdatedAt(copyAt);
|
|
43
|
+
const b = inputStampFromUpdatedAt(callAt);
|
|
44
|
+
return a !== null && b !== null && a > b;
|
|
45
|
+
};
|
|
46
|
+
/** @internal */
|
|
47
|
+
export function followInputDocuments(host, key, handlers, options = {}) {
|
|
48
|
+
checkOptions(options);
|
|
49
|
+
return follow(host, key, handlers, () => options).stop;
|
|
50
|
+
}
|
|
51
|
+
const checkOptions = (options) => {
|
|
52
|
+
if (options.concurrency !== undefined && !(options.concurrency >= 1)) {
|
|
53
|
+
throw new TypeError('subscribeInputDocuments: concurrency must be at least 1');
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
function follow(host, key, handlers, options) {
|
|
57
|
+
const has = (name) => typeof handlers[name] === 'function';
|
|
58
|
+
// With none of the file handlers, the list alone decides `settled`.
|
|
59
|
+
const followsChanges = () => has('changed') || has('removed');
|
|
60
|
+
let stopped = false;
|
|
61
|
+
let latest = null;
|
|
62
|
+
// One call per file at a time; the file's next work is taken up once it
|
|
63
|
+
// settles.
|
|
64
|
+
const running = new Map();
|
|
65
|
+
// `<kind>:<doc_id>@<version>` -> the failed attempts at that version and
|
|
66
|
+
// when the next may run, or that it waits for the file's import. A call
|
|
67
|
+
// that failed has finished, so it does not hold `settled`.
|
|
68
|
+
const failed = new Map();
|
|
69
|
+
let retryTimer = null;
|
|
70
|
+
let quietTimer = null;
|
|
71
|
+
// `<kind>:<doc_id>` -> the versions it was last handled at. For `changed`
|
|
72
|
+
// this stands in for the read record until the status catches up with it.
|
|
73
|
+
const done = new Map();
|
|
74
|
+
// Files `loaded` was given this session, so their removal reaches `removed`.
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
// The copies open per file.
|
|
77
|
+
const copies = new Map();
|
|
78
|
+
// Moves when a handler set joins, so a `loaded` call that started before
|
|
79
|
+
// it does not count as given to the newcomer.
|
|
80
|
+
let joins = 0;
|
|
81
|
+
const isDone = (w) => {
|
|
82
|
+
const versions = done.get(`${w.kind}:${w.docId}`);
|
|
83
|
+
if (versions === undefined)
|
|
84
|
+
return false;
|
|
85
|
+
if (versions.includes(w.version))
|
|
86
|
+
return true;
|
|
87
|
+
const stamp = inputStampFromUpdatedAt(w.version);
|
|
88
|
+
return (stamp !== null &&
|
|
89
|
+
versions.some((v) => {
|
|
90
|
+
const at = inputStampFromUpdatedAt(v);
|
|
91
|
+
return at !== null && stamp <= at;
|
|
92
|
+
}));
|
|
93
|
+
};
|
|
94
|
+
const closeCopy = (docId, by) => {
|
|
95
|
+
const opened = copies.get(docId);
|
|
96
|
+
if (opened === undefined)
|
|
97
|
+
return;
|
|
98
|
+
copies.delete(docId);
|
|
99
|
+
opened.closedBy = by;
|
|
100
|
+
try {
|
|
101
|
+
opened.copy.close();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Already closed by the app.
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
// A handle that warns, once, when it is used after its copy closed.
|
|
108
|
+
const guard = (docId, copy, opened) => {
|
|
109
|
+
let warned = false;
|
|
110
|
+
const bound = new Map();
|
|
111
|
+
return new Proxy(copy, {
|
|
112
|
+
get(target, prop) {
|
|
113
|
+
const by = opened().closedBy;
|
|
114
|
+
if (by !== null && !warned && typeof prop === 'string' && prop !== 'then' && prop !== 'close') {
|
|
115
|
+
warned = true;
|
|
116
|
+
console.warn(`[brass] input ${docId}: the document open returned was closed ${by}, so it no longer follows the input.`);
|
|
117
|
+
}
|
|
118
|
+
if (prop === 'close') {
|
|
119
|
+
return () => {
|
|
120
|
+
if (copies.get(docId) === opened())
|
|
121
|
+
closeCopy(docId, 'by close()');
|
|
122
|
+
else
|
|
123
|
+
target.close();
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const value = Reflect.get(target, prop, target);
|
|
127
|
+
if (typeof value !== 'function')
|
|
128
|
+
return value;
|
|
129
|
+
let fn = bound.get(prop);
|
|
130
|
+
if (fn === undefined) {
|
|
131
|
+
fn = value.bind(target);
|
|
132
|
+
bound.set(prop, fn);
|
|
133
|
+
}
|
|
134
|
+
return fn;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
// The work a status asks for, before subtracting what is done or running.
|
|
139
|
+
const workOf = (status) => {
|
|
140
|
+
const out = [];
|
|
141
|
+
const listed = new Set();
|
|
142
|
+
if (has('loaded')) {
|
|
143
|
+
for (const f of status.inputs) {
|
|
144
|
+
listed.add(f.doc_id);
|
|
145
|
+
out.push({ docId: f.doc_id, kind: 'loaded', version: f.updated_at, from: f.from, entry: null });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
for (const f of status.inputs)
|
|
150
|
+
listed.add(f.doc_id);
|
|
151
|
+
}
|
|
152
|
+
const removing = new Set();
|
|
153
|
+
if (followsChanges()) {
|
|
154
|
+
const writer = host.canWrite();
|
|
155
|
+
for (const c of status.changed) {
|
|
156
|
+
if (c.change === 'removed') {
|
|
157
|
+
// A copy that cannot record the removal still reports a file
|
|
158
|
+
// `loaded` was given, below.
|
|
159
|
+
if (!writer && seen.has(c.doc_id))
|
|
160
|
+
continue;
|
|
161
|
+
removing.add(c.doc_id);
|
|
162
|
+
out.push({ docId: c.doc_id, kind: 'removed', version: `removed:${c.updated_at ?? ''}`, from: c.from, entry: c });
|
|
163
|
+
}
|
|
164
|
+
else if (has('changed')) {
|
|
165
|
+
const version = status.inputs.find((f) => f.doc_id === c.doc_id)?.updated_at ?? '';
|
|
166
|
+
out.push({ docId: c.doc_id, kind: 'changed', version, from: c.from, entry: c });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const docId of seen) {
|
|
171
|
+
if (!listed.has(docId) && !removing.has(docId)) {
|
|
172
|
+
out.push({ docId, kind: 'removed', version: 'gone', from: [], entry: null });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
};
|
|
177
|
+
// `loaded` and a removal of a file it was given run on any copy; work that
|
|
178
|
+
// records a read runs where the document can be written.
|
|
179
|
+
const canRun = (w) => !stopped && (w.entry === null || host.canWrite());
|
|
180
|
+
const scheduleRetry = () => {
|
|
181
|
+
if (stopped)
|
|
182
|
+
return;
|
|
183
|
+
let next = Infinity;
|
|
184
|
+
for (const f of failed.values())
|
|
185
|
+
next = Math.min(next, f.retryAt);
|
|
186
|
+
if (next === Infinity)
|
|
187
|
+
return;
|
|
188
|
+
if (retryTimer !== null)
|
|
189
|
+
clearTimeout(retryTimer);
|
|
190
|
+
retryTimer = setTimeout(() => {
|
|
191
|
+
retryTimer = null;
|
|
192
|
+
if (latest !== null)
|
|
193
|
+
start(latest);
|
|
194
|
+
}, Math.max(0, next - Date.now()));
|
|
195
|
+
};
|
|
196
|
+
// Whether the status lists the file as not imported yet.
|
|
197
|
+
const awaitsImport = (status, docId) => status.inputs.find((f) => f.doc_id === docId)?.awaits_import === true;
|
|
198
|
+
const fail = (failKey, untilImported, untilImporter = false) => {
|
|
199
|
+
const attempts = (failed.get(failKey)?.attempts ?? 0) + 1;
|
|
200
|
+
if (untilImporter) {
|
|
201
|
+
failed.set(failKey, { attempts, retryAt: Infinity, awaitsImporter: true });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (untilImported) {
|
|
205
|
+
failed.set(failKey, { attempts, retryAt: Infinity, awaitsImport: true });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const wait = Math.min(RETRY_FIRST_MS * 2 ** (attempts - 1), RETRY_CEILING_MS);
|
|
209
|
+
failed.set(failKey, { attempts, retryAt: Date.now() + wait });
|
|
210
|
+
scheduleRetry();
|
|
211
|
+
};
|
|
212
|
+
const forget = (docId) => {
|
|
213
|
+
seen.delete(docId);
|
|
214
|
+
done.delete(`loaded:${docId}`);
|
|
215
|
+
done.delete(`changed:${docId}`);
|
|
216
|
+
closeCopy(docId, 'because the input was removed');
|
|
217
|
+
};
|
|
218
|
+
// `retry` is a further call at a version whose call already finished: it
|
|
219
|
+
// changes what `settled` reports only if it resolves.
|
|
220
|
+
const run = (status, work, failKey, retry) => {
|
|
221
|
+
const { docId, kind } = work;
|
|
222
|
+
const abort = new AbortController();
|
|
223
|
+
const joinsAtStart = joins;
|
|
224
|
+
// Reported whether or not a newer version abandoned the call, since an
|
|
225
|
+
// abandoned call still holds the input and its slot.
|
|
226
|
+
const pendingTimer = inDevelopment()
|
|
227
|
+
? setTimeout(() => {
|
|
228
|
+
console.warn(`[brass] subscribeInputDocuments("${key}"): the ${kind} handler for input ${docId} has not settled after ${LONG_PENDING_MS / 1000}s. The input gets no other call until it resolves or rejects.`);
|
|
229
|
+
}, LONG_PENDING_MS)
|
|
230
|
+
: null;
|
|
231
|
+
running.set(docId, { work, abort, pendingTimer });
|
|
232
|
+
if (!retry)
|
|
233
|
+
touch();
|
|
234
|
+
let resolved = false;
|
|
235
|
+
// The copies this call opened, which it records the read at, and which
|
|
236
|
+
// subscriptions each was handed to.
|
|
237
|
+
const openedHere = [];
|
|
238
|
+
const users = new Map();
|
|
239
|
+
// Copies opened for this call alone, beside one another subscription uses.
|
|
240
|
+
const ownCopies = [];
|
|
241
|
+
const use = (opened, owner) => {
|
|
242
|
+
if (!openedHere.includes(opened))
|
|
243
|
+
openedHere.push(opened);
|
|
244
|
+
let by = users.get(opened);
|
|
245
|
+
if (by === undefined)
|
|
246
|
+
users.set(opened, (by = new Set()));
|
|
247
|
+
by.add(owner);
|
|
248
|
+
return opened;
|
|
249
|
+
};
|
|
250
|
+
let opening = null;
|
|
251
|
+
let settled = false;
|
|
252
|
+
let result;
|
|
253
|
+
let callAt = work.version;
|
|
254
|
+
try {
|
|
255
|
+
if (kind === 'removed') {
|
|
256
|
+
result = handlers.removed?.(docId);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
const handler = kind === 'loaded' ? handlers.loaded : handlers.changed;
|
|
260
|
+
const file = status.inputs.find((f) => f.doc_id === docId);
|
|
261
|
+
const updatedAt = file?.updated_at ?? work.version;
|
|
262
|
+
callAt = updatedAt;
|
|
263
|
+
if (kind === 'loaded')
|
|
264
|
+
seen.add(docId);
|
|
265
|
+
const refuseLate = () => {
|
|
266
|
+
if (stopped)
|
|
267
|
+
throw new Error('open: the subscription ended');
|
|
268
|
+
if (settled)
|
|
269
|
+
throw new Error(`open: call it before the ${kind} handler resolves`);
|
|
270
|
+
};
|
|
271
|
+
const openFor = (owner) => (async (openOptions) => {
|
|
272
|
+
const openById = host.open;
|
|
273
|
+
if (openById === null)
|
|
274
|
+
throw new Error('open: this document cannot open its inputs');
|
|
275
|
+
const wanted = optionsKey(openOptions);
|
|
276
|
+
// A concurrent open with the same options shares the one in flight;
|
|
277
|
+
// one with other options waits for it, then replaces it.
|
|
278
|
+
while (opening !== null) {
|
|
279
|
+
const pending = opening;
|
|
280
|
+
if (pending.options === wanted)
|
|
281
|
+
return use(await pending.opened, owner).handle;
|
|
282
|
+
await pending.opened.catch(() => undefined);
|
|
283
|
+
}
|
|
284
|
+
const existing = [copies.get(docId), ...ownCopies].find((o) => o !== undefined && o.options === wanted);
|
|
285
|
+
if (existing !== undefined) {
|
|
286
|
+
if (newer(updatedAt, existing.copy.updatedAt))
|
|
287
|
+
await existing.copy.reload?.();
|
|
288
|
+
return use(existing, owner).handle;
|
|
289
|
+
}
|
|
290
|
+
refuseLate();
|
|
291
|
+
const entry = {
|
|
292
|
+
options: wanted,
|
|
293
|
+
opened: (async () => {
|
|
294
|
+
const copy = (await openById(docId, openOptions));
|
|
295
|
+
if (settled || stopped) {
|
|
296
|
+
copy.close();
|
|
297
|
+
refuseLate();
|
|
298
|
+
}
|
|
299
|
+
const opened = { options: wanted, copy, handle: copy, closedBy: null };
|
|
300
|
+
opened.handle = guard(docId, copy, () => opened);
|
|
301
|
+
const current = copies.get(docId);
|
|
302
|
+
const sharedWith = current === undefined ? [] : Array.from(users.get(current) ?? []);
|
|
303
|
+
if (sharedWith.some((u) => u !== owner)) {
|
|
304
|
+
ownCopies.push(opened);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
// A copy opened with other options replaces the one open before.
|
|
308
|
+
closeCopy(docId, 'when the input was opened with other options');
|
|
309
|
+
copies.set(docId, opened);
|
|
310
|
+
}
|
|
311
|
+
return use(opened, owner);
|
|
312
|
+
})(),
|
|
313
|
+
};
|
|
314
|
+
opening = entry;
|
|
315
|
+
try {
|
|
316
|
+
return (await entry.opened).handle;
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
if (opening === entry)
|
|
320
|
+
opening = null;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
// The owner a direct handler's opens are made for.
|
|
324
|
+
const sole = {};
|
|
325
|
+
const input = {
|
|
326
|
+
doc_id: docId,
|
|
327
|
+
name: file?.name ?? '',
|
|
328
|
+
...(file?.family !== undefined ? { family: file.family } : {}),
|
|
329
|
+
updated_at: updatedAt,
|
|
330
|
+
from: work.from,
|
|
331
|
+
open: openFor(sole),
|
|
332
|
+
close: () => closeCopy(docId, 'by input.close()'),
|
|
333
|
+
[OPEN_FOR]: openFor,
|
|
334
|
+
};
|
|
335
|
+
result = handler?.(input);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
result = Promise.reject(err);
|
|
340
|
+
}
|
|
341
|
+
void Promise.resolve(result)
|
|
342
|
+
.then(() => {
|
|
343
|
+
if (stopped || abort.signal.aborted)
|
|
344
|
+
return;
|
|
345
|
+
if (work.entry === null) {
|
|
346
|
+
if (kind === 'removed')
|
|
347
|
+
forget(docId);
|
|
348
|
+
else if (joins === joinsAtStart)
|
|
349
|
+
done.set(`loaded:${docId}`, [work.version]);
|
|
350
|
+
failed.delete(failKey);
|
|
351
|
+
resolved = true;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!host.canWrite())
|
|
355
|
+
return;
|
|
356
|
+
try {
|
|
357
|
+
if (kind === 'removed') {
|
|
358
|
+
host.markRead({ key, changed: [work.entry], inputs: [] }, { files: [], undoable: false });
|
|
359
|
+
forget(docId);
|
|
360
|
+
done.set(`removed:${docId}`, [work.version]);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
const read = openedHere.map((o) => o.copy);
|
|
364
|
+
// Recorded at the call's version when a copy is ahead of it, so
|
|
365
|
+
// the newer version gets a call of its own.
|
|
366
|
+
const ahead = read.some((c) => openedAhead(c.updatedAt, callAt));
|
|
367
|
+
const files = ahead || read.length === 0 ? [{ doc_id: docId }] : read;
|
|
368
|
+
host.markRead(status, { files, undoable: false });
|
|
369
|
+
const versions = ahead ? [] : read.map((c) => c.updatedAt).filter((v) => v !== null);
|
|
370
|
+
done.set(`changed:${docId}`, [work.version, ...versions]);
|
|
371
|
+
}
|
|
372
|
+
failed.delete(failKey);
|
|
373
|
+
resolved = true;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// The document closed or stopped being writable while the handler
|
|
377
|
+
// ran, or refused the record; the file stays changed and waits
|
|
378
|
+
// like a rejected one, so a refusal cannot spin.
|
|
379
|
+
fail(failKey, false);
|
|
380
|
+
}
|
|
381
|
+
}, (err) => {
|
|
382
|
+
// Work abandoned on purpose is not a failure: its replacement, if
|
|
383
|
+
// any, is already listed.
|
|
384
|
+
if (stopped || abort.signal.aborted)
|
|
385
|
+
return;
|
|
386
|
+
// A file only another app imports is called again when the status
|
|
387
|
+
// reports its import landed, rather than on a timer.
|
|
388
|
+
const notImported = err?.code === 'not_imported';
|
|
389
|
+
// This app imports the file and has not registered its importer:
|
|
390
|
+
// the import waits for the registration rather than for the status.
|
|
391
|
+
const untilImporter = typeof err === 'object' && err !== null && importerWaitRejections.has(err);
|
|
392
|
+
fail(failKey, notImported && kind !== 'removed' && awaitsImport(latest ?? status, docId), untilImporter && kind !== 'removed');
|
|
393
|
+
if (!notImported) {
|
|
394
|
+
console.error(`[brass] input ${kind} handler for ${docId} failed`, err);
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
.finally(() => {
|
|
398
|
+
settled = true;
|
|
399
|
+
if (pendingTimer !== null)
|
|
400
|
+
clearTimeout(pendingTimer);
|
|
401
|
+
for (const o of ownCopies) {
|
|
402
|
+
o.closedBy = `when the ${kind} handler resolved`;
|
|
403
|
+
try {
|
|
404
|
+
o.copy.close();
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// Already closed by the app.
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (options().keepDocumentsOpen !== true) {
|
|
411
|
+
for (const o of openedHere) {
|
|
412
|
+
if (copies.get(docId) === o)
|
|
413
|
+
closeCopy(docId, `when the ${kind} handler resolved (pass keepDocumentsOpen to keep it)`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
running.delete(docId);
|
|
417
|
+
if (!stopped && !abort.signal.aborted && (!retry || resolved))
|
|
418
|
+
touch();
|
|
419
|
+
if (latest !== null)
|
|
420
|
+
start(latest);
|
|
421
|
+
});
|
|
422
|
+
};
|
|
423
|
+
// Abandons running work the status no longer asks for: the file left, or a
|
|
424
|
+
// newer version of it is listed. Its result is not recorded.
|
|
425
|
+
const abandon = (status) => {
|
|
426
|
+
if (running.size === 0)
|
|
427
|
+
return;
|
|
428
|
+
const versions = new Map(status.inputs.map((f) => [f.doc_id, f.updated_at]));
|
|
429
|
+
for (const { work, abort } of running.values()) {
|
|
430
|
+
if (work.kind === 'removed')
|
|
431
|
+
continue;
|
|
432
|
+
const now = versions.get(work.docId);
|
|
433
|
+
if (now === undefined || now !== work.version)
|
|
434
|
+
abort.abort();
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
const start = (status) => {
|
|
438
|
+
const now = Date.now();
|
|
439
|
+
for (const work of workOf(status)) {
|
|
440
|
+
if (running.size >= (options().concurrency ?? DEFAULT_CONCURRENCY))
|
|
441
|
+
return;
|
|
442
|
+
if (!canRun(work) || running.has(work.docId) || isDone(work))
|
|
443
|
+
continue;
|
|
444
|
+
const failKey = failKeyOf(work);
|
|
445
|
+
const f = failed.get(failKey);
|
|
446
|
+
if (f !== undefined) {
|
|
447
|
+
// An input refused for want of this page's importer runs again when
|
|
448
|
+
// the importer registers or when another client's import lands.
|
|
449
|
+
const waiting = f.awaitsImporter === true || f.awaitsImport === true
|
|
450
|
+
? awaitsImport(status, work.docId)
|
|
451
|
+
: now < f.retryAt;
|
|
452
|
+
if (waiting)
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
run(status, work, failKey, f !== undefined);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
// Settled once the file list is complete, every call it asks for has
|
|
459
|
+
// finished (resolved or rejected), and nothing has started, finished or
|
|
460
|
+
// changed for `QUIET_MS`. Work not yet called, or called and still running
|
|
461
|
+
// for the first time at its version, holds it; so does a change no writer
|
|
462
|
+
// has read yet.
|
|
463
|
+
let reportedSettled = false;
|
|
464
|
+
let delivering = false;
|
|
465
|
+
let lastShaped = null;
|
|
466
|
+
const outstanding = (status) => workOf(status).some((w) => !isDone(w) && !failed.has(failKeyOf(w)));
|
|
467
|
+
const emit = (status) => {
|
|
468
|
+
const reported = { ...status, settled: reportedSettled };
|
|
469
|
+
handlers.status?.(reported);
|
|
470
|
+
return reported;
|
|
471
|
+
};
|
|
472
|
+
const quiet = () => {
|
|
473
|
+
quietTimer = null;
|
|
474
|
+
if (stopped || latest === null || !latest.settled || outstanding(latest))
|
|
475
|
+
return;
|
|
476
|
+
reportedSettled = true;
|
|
477
|
+
const reported = emit(latest);
|
|
478
|
+
handlers.settled?.(reported);
|
|
479
|
+
};
|
|
480
|
+
// Something started, finished or changed: `settled` reads false until the
|
|
481
|
+
// set has been quiet again.
|
|
482
|
+
function touch() {
|
|
483
|
+
if (stopped)
|
|
484
|
+
return;
|
|
485
|
+
if (quietTimer !== null)
|
|
486
|
+
clearTimeout(quietTimer);
|
|
487
|
+
quietTimer = setTimeout(quiet, QUIET_MS);
|
|
488
|
+
if (!reportedSettled)
|
|
489
|
+
return;
|
|
490
|
+
reportedSettled = false;
|
|
491
|
+
if (!delivering && latest !== null)
|
|
492
|
+
emit(latest);
|
|
493
|
+
}
|
|
494
|
+
// What a status asks of the follower.
|
|
495
|
+
const shapeOf = (status) => JSON.stringify([
|
|
496
|
+
status.settled,
|
|
497
|
+
status.inputs.map((f) => [f.doc_id, f.updated_at, f.awaits_import === true]),
|
|
498
|
+
workOf(status).filter((w) => !isDone(w)).map(failKeyOf),
|
|
499
|
+
]);
|
|
500
|
+
const stopStatus = host.subscribeStatus(key, (status) => {
|
|
501
|
+
latest = status;
|
|
502
|
+
const first = lastShaped === null;
|
|
503
|
+
delivering = true;
|
|
504
|
+
try {
|
|
505
|
+
abandon(status);
|
|
506
|
+
start(status);
|
|
507
|
+
// Both read against what is done now, so a change this document has
|
|
508
|
+
// just read compares equal to the status that drops it.
|
|
509
|
+
const changed = !first && shapeOf(lastShaped) !== shapeOf(status);
|
|
510
|
+
lastShaped = status;
|
|
511
|
+
if (changed)
|
|
512
|
+
touch();
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
delivering = false;
|
|
516
|
+
}
|
|
517
|
+
if (stopped)
|
|
518
|
+
return;
|
|
519
|
+
// A first status that starts no call has nothing to wait out.
|
|
520
|
+
const settleNow = first && quietTimer === null && status.settled && !outstanding(status);
|
|
521
|
+
if (settleNow)
|
|
522
|
+
reportedSettled = true;
|
|
523
|
+
const reported = emit(status);
|
|
524
|
+
if (settleNow)
|
|
525
|
+
handlers.settled?.(reported);
|
|
526
|
+
});
|
|
527
|
+
const stop = () => {
|
|
528
|
+
if (stopped)
|
|
529
|
+
return;
|
|
530
|
+
stopped = true;
|
|
531
|
+
if (retryTimer !== null)
|
|
532
|
+
clearTimeout(retryTimer);
|
|
533
|
+
if (quietTimer !== null)
|
|
534
|
+
clearTimeout(quietTimer);
|
|
535
|
+
for (const { abort, pendingTimer } of running.values()) {
|
|
536
|
+
abort.abort();
|
|
537
|
+
if (pendingTimer !== null)
|
|
538
|
+
clearTimeout(pendingTimer);
|
|
539
|
+
}
|
|
540
|
+
for (const docId of copies.keys())
|
|
541
|
+
closeCopy(docId, 'when the app unsubscribed');
|
|
542
|
+
stopStatus();
|
|
543
|
+
};
|
|
544
|
+
const join = (joined) => {
|
|
545
|
+
if (stopped || latest === null)
|
|
546
|
+
return;
|
|
547
|
+
const reported = { ...latest, settled: reportedSettled };
|
|
548
|
+
joined.status?.(reported);
|
|
549
|
+
if (typeof joined.loaded === 'function') {
|
|
550
|
+
joins++;
|
|
551
|
+
for (const k of done.keys())
|
|
552
|
+
if (k.startsWith('loaded:'))
|
|
553
|
+
done.delete(k);
|
|
554
|
+
}
|
|
555
|
+
const quietBefore = quietTimer;
|
|
556
|
+
start(latest);
|
|
557
|
+
// Nothing started for the newcomer, so the set is as settled as it was.
|
|
558
|
+
if (reportedSettled && quietTimer === quietBefore)
|
|
559
|
+
joined.settled?.(reported);
|
|
560
|
+
};
|
|
561
|
+
const importerRegistered = () => {
|
|
562
|
+
if (stopped)
|
|
563
|
+
return;
|
|
564
|
+
let woke = false;
|
|
565
|
+
for (const [failKey, f] of failed) {
|
|
566
|
+
if (f.awaitsImporter !== true)
|
|
567
|
+
continue;
|
|
568
|
+
failed.set(failKey, { attempts: f.attempts, retryAt: 0 });
|
|
569
|
+
woke = true;
|
|
570
|
+
}
|
|
571
|
+
if (woke && latest !== null)
|
|
572
|
+
start(latest);
|
|
573
|
+
};
|
|
574
|
+
return { stop, join, importerRegistered };
|
|
575
|
+
}
|
|
576
|
+
// The input as one subscription sees it: its `open` is that subscription's.
|
|
577
|
+
const inputFor = (input, set) => {
|
|
578
|
+
const openFor = input[OPEN_FOR];
|
|
579
|
+
return openFor === undefined ? input : { ...input, open: openFor(set) };
|
|
580
|
+
};
|
|
581
|
+
// Calls `name` on every set that has not resolved it at `version`, and
|
|
582
|
+
// rejects with the first rejection once all settle, so a retry reaches only
|
|
583
|
+
// the sets that failed.
|
|
584
|
+
async function callEach(sets, name, docId, version, call) {
|
|
585
|
+
const given = `${name}:${docId}`;
|
|
586
|
+
const due = Array.from(sets).filter((s) => typeof s.handlers[name] === 'function' && s.given.get(given) !== version);
|
|
587
|
+
const results = await Promise.allSettled(due.map(async (set) => {
|
|
588
|
+
await call(set);
|
|
589
|
+
set.given.set(given, version);
|
|
590
|
+
if (name === 'removed') {
|
|
591
|
+
set.given.delete(`loaded:${docId}`);
|
|
592
|
+
set.given.delete(`changed:${docId}`);
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
set.given.delete(`removed:${docId}`);
|
|
596
|
+
}
|
|
597
|
+
}));
|
|
598
|
+
const failure = results.find((r) => r.status === 'rejected');
|
|
599
|
+
if (failure !== undefined)
|
|
600
|
+
throw failure.reason;
|
|
601
|
+
if (name === 'removed')
|
|
602
|
+
for (const set of due)
|
|
603
|
+
set.given.delete(given);
|
|
604
|
+
}
|
|
605
|
+
/** @internal The `subscribeInputDocuments` followers of one document: one
|
|
606
|
+
* per key, whatever number of subscriptions share it. Each input's work runs
|
|
607
|
+
* once and every subscription's handlers are called for it; a `changed`
|
|
608
|
+
* input is recorded as read once all of them resolve. */
|
|
609
|
+
export class InputFollowers {
|
|
610
|
+
byKey = new Map();
|
|
611
|
+
subscribe(host, key, handlers, options = {}) {
|
|
612
|
+
checkOptions(options);
|
|
613
|
+
const set = { handlers, options, given: new Map() };
|
|
614
|
+
let shared = this.byKey.get(key);
|
|
615
|
+
if (shared === undefined) {
|
|
616
|
+
const sets = new Set([set]);
|
|
617
|
+
const has = (name) => Array.from(sets).some((s) => typeof s.handlers[name] === 'function');
|
|
618
|
+
// Called on a snapshot of the sets, skipping one that left during an
|
|
619
|
+
// earlier set's handler.
|
|
620
|
+
const each = (fn) => {
|
|
621
|
+
for (const s of Array.from(sets))
|
|
622
|
+
if (sets.has(s))
|
|
623
|
+
fn(s);
|
|
624
|
+
};
|
|
625
|
+
// The tightest `concurrency` any subscription asks for, and documents
|
|
626
|
+
// kept open if any asks.
|
|
627
|
+
const merged = () => {
|
|
628
|
+
let concurrency;
|
|
629
|
+
let keepDocumentsOpen = false;
|
|
630
|
+
for (const s of sets) {
|
|
631
|
+
const c = s.options.concurrency;
|
|
632
|
+
if (c !== undefined)
|
|
633
|
+
concurrency = concurrency === undefined ? c : Math.min(concurrency, c);
|
|
634
|
+
if (s.options.keepDocumentsOpen === true)
|
|
635
|
+
keepDocumentsOpen = true;
|
|
636
|
+
}
|
|
637
|
+
return { ...(concurrency !== undefined ? { concurrency } : {}), keepDocumentsOpen };
|
|
638
|
+
};
|
|
639
|
+
const combined = {
|
|
640
|
+
get loaded() {
|
|
641
|
+
if (!has('loaded'))
|
|
642
|
+
return undefined;
|
|
643
|
+
return (input) => callEach(sets, 'loaded', input.doc_id, input.updated_at, (s) => s.handlers.loaded(inputFor(input, s)));
|
|
644
|
+
},
|
|
645
|
+
get changed() {
|
|
646
|
+
if (!has('changed'))
|
|
647
|
+
return undefined;
|
|
648
|
+
return (input) => callEach(sets, 'changed', input.doc_id, input.updated_at, (s) => s.handlers.changed(inputFor(input, s)));
|
|
649
|
+
},
|
|
650
|
+
get removed() {
|
|
651
|
+
if (!has('removed'))
|
|
652
|
+
return undefined;
|
|
653
|
+
return (docId) => callEach(sets, 'removed', docId, '', (s) => s.handlers.removed(docId));
|
|
654
|
+
},
|
|
655
|
+
status: (status) => each((s) => s.handlers.status?.(status)),
|
|
656
|
+
settled: (status) => each((s) => s.handlers.settled?.(status)),
|
|
657
|
+
};
|
|
658
|
+
shared = { sets, follower: follow(host, key, combined, merged) };
|
|
659
|
+
this.byKey.set(key, shared);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
shared.sets.add(set);
|
|
663
|
+
shared.follower.join(handlers);
|
|
664
|
+
}
|
|
665
|
+
const owner = shared;
|
|
666
|
+
let left = false;
|
|
667
|
+
return () => {
|
|
668
|
+
if (left)
|
|
669
|
+
return;
|
|
670
|
+
left = true;
|
|
671
|
+
owner.sets.delete(set);
|
|
672
|
+
if (owner.sets.size > 0)
|
|
673
|
+
return;
|
|
674
|
+
if (this.byKey.get(key) === owner)
|
|
675
|
+
this.byKey.delete(key);
|
|
676
|
+
owner.follower.stop();
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/** Runs again, in every follower, the inputs whose open stopped waiting
|
|
680
|
+
* for `registerImporter`. */
|
|
681
|
+
importerRegistered() {
|
|
682
|
+
for (const shared of this.byKey.values())
|
|
683
|
+
shared.follower.importerRegistered();
|
|
684
|
+
}
|
|
685
|
+
/** Stops every follower, as the document closes or parks. */
|
|
686
|
+
stopAll() {
|
|
687
|
+
const all = [...this.byKey.values()];
|
|
688
|
+
this.byKey.clear();
|
|
689
|
+
for (const shared of all)
|
|
690
|
+
shared.follower.stop();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
//# sourceMappingURL=input-documents.js.map
|