@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,274 @@
|
|
|
1
|
+
// What lets a listing notice that its set moved (`DocumentList.onChanged`).
|
|
2
|
+
//
|
|
3
|
+
// A feed frame is a trigger and the listing is the truth. The socket feeds
|
|
4
|
+
// that cover a query's references say THAT something in their scope moved;
|
|
5
|
+
// the listing decides what the set is, so folder ancestry, archive members,
|
|
6
|
+
// type matching and the reader's own access are decided in one place, the
|
|
7
|
+
// server's listing, and never re-derived from frames here.
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
DocumentListing,
|
|
11
|
+
ListDocumentsQuery,
|
|
12
|
+
} from './documents-namespace.js';
|
|
13
|
+
import type { InputReference } from './hosted-ui.js';
|
|
14
|
+
import type { SubscribeOptions, WsTopicCallback } from './ws-client.js';
|
|
15
|
+
|
|
16
|
+
/** The slice of the socket client a subscription uses. */
|
|
17
|
+
export interface DocumentSetSocket {
|
|
18
|
+
subscribe(
|
|
19
|
+
topic: string,
|
|
20
|
+
callback: WsTopicCallback,
|
|
21
|
+
options?: SubscribeOptions,
|
|
22
|
+
): () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The feed topic that covers each reference: `organization:<id>`,
|
|
27
|
+
* `project:<id>`, or `reference:<id>` for a document, which fires when the
|
|
28
|
+
* document or anything filed under it changes.
|
|
29
|
+
*/
|
|
30
|
+
export function feedTopicsFor(refs: readonly InputReference[]): string[] {
|
|
31
|
+
const topics = new Set<string>();
|
|
32
|
+
for (const ref of refs) {
|
|
33
|
+
if (ref.extent === 'organization') {
|
|
34
|
+
topics.add(`organization:${ref.organization_id}`);
|
|
35
|
+
} else if (ref.extent === 'project' && ref.project_id) {
|
|
36
|
+
topics.add(`project:${ref.project_id}`);
|
|
37
|
+
} else if (ref.extent === 'document' && ref.doc_id) {
|
|
38
|
+
topics.add(`reference:${ref.doc_id}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...topics];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Frames the socket client hands a topic's callback that report on the
|
|
45
|
+
// subscription itself rather than on anything in its scope.
|
|
46
|
+
const CONTROL_FRAMES = new Set([
|
|
47
|
+
'subscribed',
|
|
48
|
+
'subscribed_ack',
|
|
49
|
+
'unsubscribed',
|
|
50
|
+
'not_subscribed',
|
|
51
|
+
'server_ended',
|
|
52
|
+
'error',
|
|
53
|
+
'pong',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/** Whether a frame on a feed could have moved what a listing answers: any
|
|
57
|
+
* data frame does, whatever its type, since the listing decides. */
|
|
58
|
+
export function frameCouldMoveSet(frame: unknown): boolean {
|
|
59
|
+
if (frame === null || typeof frame !== 'object') return false;
|
|
60
|
+
const type = (frame as { type?: unknown }).type;
|
|
61
|
+
return typeof type === 'string' && !CONTROL_FRAMES.has(type);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether two listings differ, keyed by `doc_id`: a document joined, left,
|
|
65
|
+
* or has a different `updated_at` or work `status`. */
|
|
66
|
+
export function listingsDiffer(
|
|
67
|
+
previous: readonly DocumentListing[],
|
|
68
|
+
next: readonly DocumentListing[],
|
|
69
|
+
): boolean {
|
|
70
|
+
if (previous.length !== next.length) return true;
|
|
71
|
+
const before = new Map(previous.map((d) => [d.doc_id, d]));
|
|
72
|
+
return next.some((doc) => {
|
|
73
|
+
const was = before.get(doc.doc_id);
|
|
74
|
+
return was === undefined || was.updated_at !== doc.updated_at || !sameStatus(was.status, doc.status);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// An import that fails writes its status without moving the stamp, so the
|
|
79
|
+
// status is compared as well.
|
|
80
|
+
function sameStatus(a: DocumentListing['status'], b: DocumentListing['status']): boolean {
|
|
81
|
+
if (a === undefined || b === undefined) return a === b;
|
|
82
|
+
return a.pending === b.pending && a.blocking === b.blocking && a.message === b.message;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* How long a trigger waits before listing, so a burst of frames (a move
|
|
87
|
+
* stamps the document and nudges both containers) is one listing.
|
|
88
|
+
*/
|
|
89
|
+
export const RELIST_COALESCE_MS = 250;
|
|
90
|
+
|
|
91
|
+
/** The longest the first listing waits for its feeds to be established. */
|
|
92
|
+
export const FEED_WAIT_MS = 3_000;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Watch the set `query` lists and call `listener` whenever a listing differs
|
|
96
|
+
* from the last one, starting from `initial`. Returns the stop function.
|
|
97
|
+
*/
|
|
98
|
+
export function watchDocumentSet(args: {
|
|
99
|
+
query: ListDocumentsQuery;
|
|
100
|
+
list: (query: ListDocumentsQuery) => Promise<DocumentListing[]>;
|
|
101
|
+
socket: DocumentSetSocket | undefined;
|
|
102
|
+
listener: () => void;
|
|
103
|
+
initial: DocumentListing[];
|
|
104
|
+
}): () => void {
|
|
105
|
+
const { query, list, socket, listener } = args;
|
|
106
|
+
let stopped = false;
|
|
107
|
+
let last = args.initial;
|
|
108
|
+
let inFlight = false;
|
|
109
|
+
let again = false;
|
|
110
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
111
|
+
|
|
112
|
+
const relist = async (): Promise<void> => {
|
|
113
|
+
if (stopped) return;
|
|
114
|
+
if (inFlight) {
|
|
115
|
+
// A frame that lands while a listing is out may describe a write that
|
|
116
|
+
// listing already missed, so one more follows it.
|
|
117
|
+
again = true;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
inFlight = true;
|
|
121
|
+
try {
|
|
122
|
+
const next = await list(query);
|
|
123
|
+
if (stopped) return;
|
|
124
|
+
const moved = listingsDiffer(last, next);
|
|
125
|
+
last = next;
|
|
126
|
+
if (moved) listener();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// The watch keeps its last answer and lists again on the next trigger.
|
|
129
|
+
if (!stopped) console.error('Brass.documents.list:', error);
|
|
130
|
+
} finally {
|
|
131
|
+
inFlight = false;
|
|
132
|
+
if (again && !stopped) {
|
|
133
|
+
again = false;
|
|
134
|
+
schedule();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const schedule = (): void => {
|
|
140
|
+
// Before the first listing, that listing answers whatever moved.
|
|
141
|
+
if (stopped || !firstListed || timer !== null) return;
|
|
142
|
+
timer = setTimeout(() => {
|
|
143
|
+
timer = null;
|
|
144
|
+
void relist();
|
|
145
|
+
}, RELIST_COALESCE_MS);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// The first listing waits for the feeds to be established, so a write
|
|
149
|
+
// landing between that listing and the subscription cannot fall between
|
|
150
|
+
// them. A feed the socket refuses, or one that never answers, stops being
|
|
151
|
+
// waited for rather than holding the first answer back.
|
|
152
|
+
const awaiting = new Set<string>();
|
|
153
|
+
let firstListed = false;
|
|
154
|
+
const listFirst = (): void => {
|
|
155
|
+
if (firstListed) return;
|
|
156
|
+
firstListed = true;
|
|
157
|
+
if (firstTimer !== null) clearTimeout(firstTimer);
|
|
158
|
+
void relist();
|
|
159
|
+
};
|
|
160
|
+
const settled = (topic: string): void => {
|
|
161
|
+
awaiting.delete(topic);
|
|
162
|
+
if (awaiting.size === 0) listFirst();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const unsubscribes: Array<() => void> = [];
|
|
166
|
+
const topics = socket ? feedTopicsFor(query.from ?? []) : [];
|
|
167
|
+
for (const topic of topics) awaiting.add(topic);
|
|
168
|
+
let firstTimer: ReturnType<typeof setTimeout> | null =
|
|
169
|
+
topics.length > 0 ? setTimeout(listFirst, FEED_WAIT_MS) : null;
|
|
170
|
+
for (const topic of topics) {
|
|
171
|
+
unsubscribes.push(
|
|
172
|
+
socket!.subscribe(
|
|
173
|
+
topic,
|
|
174
|
+
(frame) => {
|
|
175
|
+
if (frameCouldMoveSet(frame)) schedule();
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
onSubscribed: ({ resubscribed }) => {
|
|
179
|
+
// Frames fanned out while the socket was down are gone, so a
|
|
180
|
+
// re-established feed is answered with a listing.
|
|
181
|
+
if (resubscribed) schedule();
|
|
182
|
+
else settled(topic);
|
|
183
|
+
},
|
|
184
|
+
onEnded: () => settled(topic),
|
|
185
|
+
},
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (topics.length === 0) listFirst();
|
|
190
|
+
|
|
191
|
+
return () => {
|
|
192
|
+
if (stopped) return;
|
|
193
|
+
stopped = true;
|
|
194
|
+
if (timer !== null) clearTimeout(timer);
|
|
195
|
+
if (firstTimer !== null) clearTimeout(firstTimer);
|
|
196
|
+
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** A listing, as `Brass.documents.list` returns it. */
|
|
201
|
+
export interface DocumentList {
|
|
202
|
+
/** The set as it stood when it was listed. */
|
|
203
|
+
readonly documents: DocumentListing[];
|
|
204
|
+
/** Whether the set has moved since: a document joined it, left it, or
|
|
205
|
+
* changed (its `updated_at` or work `status`). List again to see how. */
|
|
206
|
+
readonly changed: boolean;
|
|
207
|
+
/** Fires once, when the set moves. The set is watched only while a
|
|
208
|
+
* listener is registered and until `close`; throws after `close`. */
|
|
209
|
+
onChanged(cb: () => void): () => void;
|
|
210
|
+
close(): void;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function createDocumentList(args: {
|
|
214
|
+
documents: DocumentListing[];
|
|
215
|
+
query: ListDocumentsQuery;
|
|
216
|
+
list: (query: ListDocumentsQuery) => Promise<DocumentListing[]>;
|
|
217
|
+
socket: DocumentSetSocket | undefined;
|
|
218
|
+
}): DocumentList {
|
|
219
|
+
let changed = false;
|
|
220
|
+
let closed = false;
|
|
221
|
+
let stopWatching: (() => void) | null = null;
|
|
222
|
+
const listeners = new Set<() => void>();
|
|
223
|
+
const stopWatch = (): void => {
|
|
224
|
+
stopWatching?.();
|
|
225
|
+
stopWatching = null;
|
|
226
|
+
};
|
|
227
|
+
const markChanged = (): void => {
|
|
228
|
+
if (changed || closed) return;
|
|
229
|
+
changed = true;
|
|
230
|
+
stopWatch();
|
|
231
|
+
const fire = [...listeners];
|
|
232
|
+
listeners.clear();
|
|
233
|
+
for (const cb of fire) {
|
|
234
|
+
try {
|
|
235
|
+
cb();
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error('Brass: an onChanged listener threw', err);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const set: DocumentList = {
|
|
242
|
+
documents: args.documents,
|
|
243
|
+
get changed() {
|
|
244
|
+
return changed;
|
|
245
|
+
},
|
|
246
|
+
onChanged(cb) {
|
|
247
|
+
if (closed) throw new Error('DocumentList is closed');
|
|
248
|
+
if (changed) {
|
|
249
|
+
queueMicrotask(() => {
|
|
250
|
+
if (!closed) cb();
|
|
251
|
+
});
|
|
252
|
+
return () => {};
|
|
253
|
+
}
|
|
254
|
+
listeners.add(cb);
|
|
255
|
+
stopWatching ??= watchDocumentSet({
|
|
256
|
+
query: args.query,
|
|
257
|
+
list: args.list,
|
|
258
|
+
socket: args.socket,
|
|
259
|
+
initial: args.documents,
|
|
260
|
+
listener: markChanged,
|
|
261
|
+
});
|
|
262
|
+
return () => {
|
|
263
|
+
listeners.delete(cb);
|
|
264
|
+
if (listeners.size === 0) stopWatch();
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
close() {
|
|
268
|
+
closed = true;
|
|
269
|
+
listeners.clear();
|
|
270
|
+
stopWatch();
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
return set;
|
|
274
|
+
}
|
package/src/document-signals.ts
CHANGED
|
@@ -67,8 +67,10 @@ export interface DocumentDriveStatus {
|
|
|
67
67
|
driveName: string;
|
|
68
68
|
// The divergence state. `ahead` / `diverged` mean unpublished local edits.
|
|
69
69
|
state: DocumentDriveTrackingState;
|
|
70
|
-
// True when publishing would do something
|
|
71
|
-
//
|
|
70
|
+
// True when publishing would do something now, as the server decides it:
|
|
71
|
+
// edits the drive takes and something here can write, or a rename or move
|
|
72
|
+
// to carry, on an unbroken link with no publish in flight. The cue to
|
|
73
|
+
// surface a "Publish" affordance.
|
|
72
74
|
canPush: boolean;
|
|
73
75
|
// Server-authored short pill text (`null` when nothing worth surfacing).
|
|
74
76
|
label: string | null;
|
|
@@ -81,9 +83,14 @@ export interface DocumentDriveStatus {
|
|
|
81
83
|
// (the file-roundtrip path), so an app can warn that the source's newer
|
|
82
84
|
// version will be replaced, not merged.
|
|
83
85
|
pushReplacesRemote: boolean;
|
|
86
|
+
/** @internal True when the drive keeps every prior version of the item,
|
|
87
|
+
* so a version a publish replaces stays in the item's history on the
|
|
88
|
+
* drive. The SDK's own drive surface words its publish confirm from it. */
|
|
89
|
+
keepsPriorVersions: boolean;
|
|
84
90
|
// Whether the link round-trips (`two_way`) or only imports (`import_only`).
|
|
85
|
-
//
|
|
86
|
-
// a
|
|
91
|
+
// An `import_only` link publishes no content, so `canPush` is true there
|
|
92
|
+
// only for a rename or a move; an app surfaces the class so a one-way
|
|
93
|
+
// import doesn't read as a live two-way mirror.
|
|
87
94
|
mirrorDirection: DriveMirrorDirection;
|
|
88
95
|
// Current external head ref, empty when the adapter could not produce one.
|
|
89
96
|
ref: string;
|
|
@@ -135,6 +142,9 @@ export interface DrivePublishTarget {
|
|
|
135
142
|
// The last publish attempt failed, in which case `detail` carries the
|
|
136
143
|
// server's account of why rather than the resting sentence.
|
|
137
144
|
publishFailed: boolean;
|
|
145
|
+
// The drive does not take this document's file; `detail` says why, and
|
|
146
|
+
// there is nothing to publish.
|
|
147
|
+
unpublishable: boolean;
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
// Each signal is a `createEmitter` composed with the retain / attach / dedup
|
|
@@ -178,9 +188,19 @@ export class GoneSignal {
|
|
|
178
188
|
}
|
|
179
189
|
}
|
|
180
190
|
|
|
191
|
+
const NO_WORK_STATUS: DocumentWorkStatus = { message: '', pending: false, blocking: false };
|
|
192
|
+
|
|
181
193
|
// Latest-retained, re-emit-per-phase signal for `doc_status`. Not one-shot:
|
|
182
194
|
// phases arrive repeatedly until terminal (`pending: false`), and the latest
|
|
183
195
|
// is retained so a late subscriber still sees the final state.
|
|
196
|
+
//
|
|
197
|
+
// A notice is the SDK's own status, raised over the platform's work rather
|
|
198
|
+
// than replacing it: `status` reads the notice while one is up, and `work`
|
|
199
|
+
// keeps answering what the platform reported, which is what every SDK-side
|
|
200
|
+
// decision about pending work reads. A work status arriving under a notice
|
|
201
|
+
// still reaches the listeners (the SDK's own import waiters attach here and
|
|
202
|
+
// wait for a terminal one), and the notice is re-emitted straight after it so
|
|
203
|
+
// the last thing a listener hears matches `status`.
|
|
184
204
|
export class WorkStatusSignal {
|
|
185
205
|
private readonly _emitter = createEmitter<DocumentWorkStatus>({
|
|
186
206
|
fireOnAttach: true,
|
|
@@ -189,9 +209,19 @@ export class WorkStatusSignal {
|
|
|
189
209
|
console.error('[Brass] onStatus listener threw:', err);
|
|
190
210
|
},
|
|
191
211
|
});
|
|
212
|
+
private _work: DocumentWorkStatus | null = null;
|
|
213
|
+
private _notice: DocumentWorkStatus | null = null;
|
|
192
214
|
|
|
193
215
|
get status(): DocumentWorkStatus | null {
|
|
194
|
-
return this.
|
|
216
|
+
return this._notice ?? this._work;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
get work(): DocumentWorkStatus | null {
|
|
220
|
+
return this._work;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
get notice(): DocumentWorkStatus | null {
|
|
224
|
+
return this._notice;
|
|
195
225
|
}
|
|
196
226
|
|
|
197
227
|
on(cb: DocumentStatusListener): DocumentStatusUnsubscribe {
|
|
@@ -199,7 +229,18 @@ export class WorkStatusSignal {
|
|
|
199
229
|
}
|
|
200
230
|
|
|
201
231
|
emit(status: DocumentWorkStatus): void {
|
|
232
|
+
this._work = status;
|
|
202
233
|
this._emitter.emit(status);
|
|
234
|
+
if (this._notice !== null) this._emitter.emit(this._notice);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Raise or clear the notice. Clearing hands listeners the work status it
|
|
238
|
+
// covered, or a terminal empty one when there is none, since a listener
|
|
239
|
+
// takes a status rather than `null`.
|
|
240
|
+
setNotice(notice: DocumentWorkStatus | null): void {
|
|
241
|
+
if (notice === this._notice) return;
|
|
242
|
+
this._notice = notice;
|
|
243
|
+
this._emitter.emit(notice ?? this._work ?? NO_WORK_STATUS);
|
|
203
244
|
}
|
|
204
245
|
|
|
205
246
|
clearListeners(): void {
|
|
@@ -335,6 +376,7 @@ function driveStatusEqual(
|
|
|
335
376
|
a.detail === b.detail &&
|
|
336
377
|
a.broken === b.broken &&
|
|
337
378
|
a.pushReplacesRemote === b.pushReplacesRemote &&
|
|
379
|
+
a.keepsPriorVersions === b.keepsPriorVersions &&
|
|
338
380
|
a.mirrorDirection === b.mirrorDirection &&
|
|
339
381
|
a.ref === b.ref &&
|
|
340
382
|
a.refUpdatedAt === b.refUpdatedAt &&
|
package/src/document-streams.ts
CHANGED
|
@@ -26,12 +26,17 @@ import type {
|
|
|
26
26
|
DocumentStatusUnsubscribe,
|
|
27
27
|
DocumentWorkStatus,
|
|
28
28
|
} from './document-signals.js';
|
|
29
|
+
import type { InputStatus } from './inputs-status.js';
|
|
30
|
+
import type { InputDocumentHandlers, InputDocumentOptions } from './input-documents.js';
|
|
29
31
|
import type {
|
|
30
32
|
ListHandle,
|
|
31
33
|
MapHandle,
|
|
34
|
+
MutateOptions,
|
|
32
35
|
OrderedMapHandle,
|
|
33
36
|
TextHandle,
|
|
34
37
|
} from './handle-types.js';
|
|
38
|
+
import type { InputReference } from './hosted-ui.js';
|
|
39
|
+
import type { MarkReadOptions, MarkReadStatus } from './references.js';
|
|
35
40
|
|
|
36
41
|
// The wiring `Document.stream()` runs on: list/create manifests over
|
|
37
42
|
// HTTP, and open a stream's line (join + WS subscribe) returning the
|
|
@@ -80,8 +85,22 @@ export class DocumentStream {
|
|
|
80
85
|
|
|
81
86
|
mutate<E extends object = EntityProxy>(
|
|
82
87
|
fn: (d: DocumentMutator<E>) => void,
|
|
88
|
+
options?: MutateOptions,
|
|
83
89
|
): void {
|
|
84
|
-
this._inner.mutate<E>(fn);
|
|
90
|
+
this._inner.mutate<E>(fn, options);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Records the documents `key` depends on for this stream; see
|
|
94
|
+
* `Document.dependsOn`. A reference to any stream of this document is
|
|
95
|
+
* refused. */
|
|
96
|
+
dependsOn(key: string, refs: readonly InputReference[] | null): void {
|
|
97
|
+
this._inner.dependsOn(key, refs);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @internal Records what this reader read for the key `status` reports
|
|
101
|
+
* on this stream; see `Document.markRead`. */
|
|
102
|
+
markRead(status: MarkReadStatus, options?: MarkReadOptions): void {
|
|
103
|
+
this._inner.markRead(status, options);
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
roots(): Record<string, RootKind> {
|
|
@@ -111,6 +130,12 @@ export class DocumentStream {
|
|
|
111
130
|
return this._inner.readOnly;
|
|
112
131
|
}
|
|
113
132
|
|
|
133
|
+
/** @internal This stream's line's `updated_at` as it was opened; see
|
|
134
|
+
* `Document.updatedAt`. */
|
|
135
|
+
get updatedAt(): string | null {
|
|
136
|
+
return this._inner.updatedAt;
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
// Locally-issued ops on this stream the server has not acked yet. This
|
|
115
140
|
// stream's own line: `doc.savingCount` is the one that adds the streams up,
|
|
116
141
|
// and the default stream's inner IS the document, so reading that here would
|
|
@@ -126,14 +151,18 @@ export class DocumentStream {
|
|
|
126
151
|
return this._inner.atCapacity;
|
|
127
152
|
}
|
|
128
153
|
|
|
129
|
-
|
|
130
|
-
|
|
154
|
+
/**
|
|
155
|
+
* The work in flight on this stream, or `null` when there is none. Render
|
|
156
|
+
* `message` verbatim.
|
|
157
|
+
*/
|
|
131
158
|
get status(): DocumentWorkStatus | null {
|
|
132
159
|
return this._inner.status;
|
|
133
160
|
}
|
|
134
161
|
|
|
135
|
-
|
|
136
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Calls `cb` with every work-status update for this stream, and once on
|
|
164
|
+
* attach when a status is already known. Returns an unsubscribe.
|
|
165
|
+
*/
|
|
137
166
|
onStatus(cb: DocumentStatusListener): DocumentStatusUnsubscribe {
|
|
138
167
|
return this._inner.onStatus(cb);
|
|
139
168
|
}
|
|
@@ -144,23 +173,63 @@ export class DocumentStream {
|
|
|
144
173
|
return this._inner.onGone(cb);
|
|
145
174
|
}
|
|
146
175
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
176
|
+
/** @internal On a frozen copy: whether this stream has moved since it was
|
|
177
|
+
* read. Each stream is its own line, so each is watched on its own. */
|
|
178
|
+
get changed(): false {
|
|
179
|
+
return this._inner.changed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** @internal */
|
|
183
|
+
onChanged(cb: () => void): () => void {
|
|
184
|
+
return this._inner.onChanged(cb);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @internal Typed on `ReadOnlyStream`: loads this stream as it stands now
|
|
188
|
+
* on a frozen copy. */
|
|
189
|
+
reload(): Promise<void> {
|
|
190
|
+
return this._inner.reload();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The input files of a key this stream records references under;
|
|
194
|
+
* see `Document.subscribeInputDocuments`. A stream's keys are its own,
|
|
195
|
+
* apart from the document's and every other stream's. */
|
|
196
|
+
subscribeInputDocuments(
|
|
197
|
+
key: string,
|
|
198
|
+
handlers: InputDocumentHandlers,
|
|
199
|
+
options?: InputDocumentOptions,
|
|
200
|
+
): () => void {
|
|
201
|
+
return this._inner.subscribeInputDocuments(key, handlers, options);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @internal The status of a key this stream records references under. */
|
|
205
|
+
subscribeInputStatus(key: string, cb: (status: InputStatus) => void): () => void {
|
|
206
|
+
return this._inner.subscribeInputStatus(key, cb);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Reverts this stream's part of the newest undo step that touched it,
|
|
211
|
+
* leaving the document's other streams as they are.
|
|
212
|
+
* @deprecated Call `doc.undo()`, which reverts the newest step in every
|
|
213
|
+
* stream it touched.
|
|
214
|
+
*/
|
|
150
215
|
undo(): boolean {
|
|
151
|
-
return this._inner.undo
|
|
216
|
+
return this._inner._replayOwnUndoStep('undo');
|
|
152
217
|
}
|
|
153
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Re-applies this stream's part of the step it most recently undid.
|
|
221
|
+
* @deprecated Call `doc.redo()`.
|
|
222
|
+
*/
|
|
154
223
|
redo(): boolean {
|
|
155
|
-
return this._inner.redo
|
|
224
|
+
return this._inner._replayOwnUndoStep('redo');
|
|
156
225
|
}
|
|
157
226
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Ends the document's open undo step, so the next local edit in any stream
|
|
229
|
+
* starts a new one.
|
|
230
|
+
* @deprecated Call `doc.createCheckpoint()` at the end of each user action,
|
|
231
|
+
* which also ends the step.
|
|
232
|
+
*/
|
|
164
233
|
sealUndoStep(): void {
|
|
165
234
|
this._inner._sealUndoStep();
|
|
166
235
|
}
|