@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
package/src/errors.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// The single error type every `@brass-build/client` call rejects with.
|
|
2
2
|
//
|
|
3
|
-
// One catch type
|
|
4
|
-
//
|
|
5
|
-
// HTTP
|
|
3
|
+
// One catch type: `.status` carries an HTTP rejection's status, and `.code`
|
|
4
|
+
// names a hosted-UI launcher's failure or the code an HTTP refusal carried.
|
|
5
|
+
// An HTTP refusal carries the platform's code whatever it is, so a code the
|
|
6
|
+
// platform adds after this SDK was built still reaches the app. The codes
|
|
7
|
+
// below are the ones an app is expected to act on.
|
|
6
8
|
// - `iframe_unavailable`: this browser / app context can't host the
|
|
7
9
|
// iframe (an app surfaces this as "open in a new tab").
|
|
8
10
|
// - `embed_error`: the embed page cannot proceed (e.g. not signed in).
|
|
@@ -10,17 +12,26 @@
|
|
|
10
12
|
// - `app_not_installed`: the server rejected the app's action because
|
|
11
13
|
// the target organization has no install of it; `organizationId`
|
|
12
14
|
// names the org so the install-consent flow can be offered.
|
|
15
|
+
// - `app_not_granted`: the reader can open the document, but nobody has
|
|
16
|
+
// handed it to this app. Opening it through `session.open()` does.
|
|
17
|
+
// - `not_imported`: the document's file has not been imported: another
|
|
18
|
+
// app imports it, or this app had not called `registerImporter` when
|
|
19
|
+
// the open stopped waiting for it. Show that the file has not been
|
|
20
|
+
// imported yet; a document that has it as an input lists it as changed
|
|
21
|
+
// once it has.
|
|
13
22
|
export type BrassErrorCode =
|
|
14
23
|
| 'not_in_browser'
|
|
15
24
|
| 'iframe_unavailable'
|
|
16
25
|
| 'embed_error'
|
|
17
26
|
| 'app_not_installed'
|
|
18
|
-
| 'schema_conflict'
|
|
27
|
+
| 'schema_conflict'
|
|
28
|
+
| 'app_not_granted'
|
|
29
|
+
| 'not_imported';
|
|
19
30
|
|
|
20
31
|
export class BrassError extends Error {
|
|
21
32
|
override name = 'BrassError';
|
|
22
33
|
readonly status?: number;
|
|
23
|
-
readonly code?: BrassErrorCode;
|
|
34
|
+
readonly code?: BrassErrorCode | (string & {});
|
|
24
35
|
readonly organizationId?: string;
|
|
25
36
|
/** Server-authored heading for a prompt this error drives, when the
|
|
26
37
|
* platform sends one. A client freezes into an app's bundle, so a string
|
|
@@ -31,7 +42,7 @@ export class BrassError extends Error {
|
|
|
31
42
|
message: string,
|
|
32
43
|
init?: {
|
|
33
44
|
status?: number;
|
|
34
|
-
code?: BrassErrorCode;
|
|
45
|
+
code?: BrassErrorCode | (string & {});
|
|
35
46
|
organizationId?: string;
|
|
36
47
|
title?: string;
|
|
37
48
|
},
|
package/src/fetch-retry.ts
CHANGED
|
@@ -72,6 +72,10 @@ export interface RetryOptions {
|
|
|
72
72
|
// Invoked before each replay with the 1-based attempt that just failed and
|
|
73
73
|
// the response (absent on a network throw). For diagnostics only.
|
|
74
74
|
onRetry?: (attempt: number, response: Response | undefined) => void;
|
|
75
|
+
// A response in the replay set that this answers true for is returned
|
|
76
|
+
// rather than replayed: a refusal that names its own wait, which the caller
|
|
77
|
+
// paces instead of this backoff.
|
|
78
|
+
isFinal?: (response: Response) => Promise<boolean>;
|
|
75
79
|
// Injected for tests; defaults to the real timer.
|
|
76
80
|
sleep?: (ms: number) => Promise<void>;
|
|
77
81
|
}
|
|
@@ -112,6 +116,7 @@ export async function fetchWithRetry(
|
|
|
112
116
|
if (response && !policy.replayStatuses.has(response.status)) {
|
|
113
117
|
return response;
|
|
114
118
|
}
|
|
119
|
+
if (response && options.isFinal && (await options.isFinal(response))) return response;
|
|
115
120
|
if (attempt === maxAttempts) {
|
|
116
121
|
if (response) return response;
|
|
117
122
|
throw lastError;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The browser's own file dialog, for a visitor with no account: there is
|
|
2
|
+
// nothing of theirs to browse, so which file is the whole question.
|
|
3
|
+
//
|
|
4
|
+
// `input.click()` needs the user activation of the press that called it,
|
|
5
|
+
// which lasts seconds rather than a task, so a short read may come first.
|
|
6
|
+
// Resolves null when the dialog is dismissed (`cancel`, which every current
|
|
7
|
+
// engine fires on an `<input type=file>`).
|
|
8
|
+
|
|
9
|
+
export function chooseFile(accept: string): Promise<File | null> {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
const input = document.createElement('input');
|
|
12
|
+
input.type = 'file';
|
|
13
|
+
if (accept !== '') input.accept = accept;
|
|
14
|
+
input.style.display = 'none';
|
|
15
|
+
const finish = (file: File | null): void => {
|
|
16
|
+
input.remove();
|
|
17
|
+
resolve(file);
|
|
18
|
+
};
|
|
19
|
+
input.addEventListener('change', () => finish(input.files?.[0] ?? null), { once: true });
|
|
20
|
+
input.addEventListener('cancel', () => finish(null), { once: true });
|
|
21
|
+
// Attached, because a detached input does not open its dialog in every
|
|
22
|
+
// engine.
|
|
23
|
+
document.body.append(input);
|
|
24
|
+
input.click();
|
|
25
|
+
});
|
|
26
|
+
}
|
package/src/guest-link.ts
CHANGED
|
@@ -6,9 +6,370 @@
|
|
|
6
6
|
// Pure + search-string-in so it stays testable, mirroring
|
|
7
7
|
// `isExportMode(search)`.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const GUEST_LINK_PARAM = 'brass_link';
|
|
10
10
|
|
|
11
11
|
export function readGuestLinkToken(search: string): string | null {
|
|
12
12
|
const value = new URLSearchParams(search).get(GUEST_LINK_PARAM);
|
|
13
13
|
return value && value.trim() !== '' ? value : null;
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
// Pin the token into the page URL (`replaceState`, never a history entry):
|
|
17
|
+
// a document created while signed out is reached again only through its
|
|
18
|
+
// token, so a reload restores it from here, and `signIn()` carries this URL
|
|
19
|
+
// as its return address.
|
|
20
|
+
export function writeGuestLinkToken(token: string): void {
|
|
21
|
+
if (typeof window === 'undefined') return;
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(window.location.href);
|
|
24
|
+
url.searchParams.set(GUEST_LINK_PARAM, token);
|
|
25
|
+
const next = url.toString();
|
|
26
|
+
if (next === window.location.href) return;
|
|
27
|
+
window.history.replaceState(window.history.state, '', next);
|
|
28
|
+
} catch {
|
|
29
|
+
// History API unavailable or URL exotic: leave the URL alone.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function clearGuestLinkToken(): void {
|
|
34
|
+
if (typeof window === 'undefined') return;
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(window.location.href);
|
|
37
|
+
if (!url.searchParams.has(GUEST_LINK_PARAM)) return;
|
|
38
|
+
url.searchParams.delete(GUEST_LINK_PARAM);
|
|
39
|
+
window.history.replaceState(window.history.state, '', url.toString());
|
|
40
|
+
} catch {
|
|
41
|
+
// See `writeGuestLinkToken`.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The last document a visitor created without an account, per app. The URL
|
|
46
|
+
// is that document's only handle, so a visitor who closes the tab and comes
|
|
47
|
+
// back to the app's root would have no way back to it; the app's signed-out
|
|
48
|
+
// gate offers it from here. localStorage, since the return is in a new tab.
|
|
49
|
+
// The expiry is stored beside the token so the gate can say when the work
|
|
50
|
+
// is lost without a request, and an entry past it reads as absent.
|
|
51
|
+
//
|
|
52
|
+
// The token is a credential to an editable document, which is why the
|
|
53
|
+
// session hands an app the expiry alone and opens the document itself.
|
|
54
|
+
const UNSAVED_KEY_PREFIX = 'brass:unsaved:';
|
|
55
|
+
|
|
56
|
+
export interface UnsavedWorkEntry {
|
|
57
|
+
token: string;
|
|
58
|
+
expiresAt: Date;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function unsavedStorage(): Storage | null {
|
|
62
|
+
if (typeof localStorage === 'undefined') return null;
|
|
63
|
+
try {
|
|
64
|
+
return localStorage;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function unsavedKey(appId: string): string {
|
|
71
|
+
return `${UNSAVED_KEY_PREFIX}${appId}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function storeUnsavedWork(
|
|
75
|
+
appId: string,
|
|
76
|
+
token: string,
|
|
77
|
+
expiresAt: Date,
|
|
78
|
+
): void {
|
|
79
|
+
const storage = unsavedStorage();
|
|
80
|
+
if (!storage) return;
|
|
81
|
+
try {
|
|
82
|
+
storage.setItem(
|
|
83
|
+
unsavedKey(appId),
|
|
84
|
+
JSON.stringify({ token, expires_at: expiresAt.toISOString() }),
|
|
85
|
+
);
|
|
86
|
+
} catch {
|
|
87
|
+
// Quota / disabled storage. The way back is a niceness; the open that
|
|
88
|
+
// just succeeded must not fail on it.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function readUnsavedWork(
|
|
93
|
+
appId: string,
|
|
94
|
+
now: number = Date.now(),
|
|
95
|
+
): UnsavedWorkEntry | null {
|
|
96
|
+
const storage = unsavedStorage();
|
|
97
|
+
if (!storage) return null;
|
|
98
|
+
let raw: string | null;
|
|
99
|
+
try {
|
|
100
|
+
raw = storage.getItem(unsavedKey(appId));
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (raw === null) return null;
|
|
105
|
+
let parsed: unknown;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(raw);
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const entry = parsed as { token?: unknown; expires_at?: unknown } | null;
|
|
112
|
+
const expiresAt =
|
|
113
|
+
typeof entry?.expires_at === 'string' ? new Date(entry.expires_at) : null;
|
|
114
|
+
if (
|
|
115
|
+
typeof entry?.token !== 'string' ||
|
|
116
|
+
entry.token === '' ||
|
|
117
|
+
expiresAt === null ||
|
|
118
|
+
Number.isNaN(expiresAt.getTime())
|
|
119
|
+
) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
// The platform removes the document at the expiry, so an entry past it
|
|
123
|
+
// names nothing and is dropped here rather than offered.
|
|
124
|
+
if (expiresAt.getTime() <= now) {
|
|
125
|
+
clearUnsavedWork(appId);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return { token: entry.token, expiresAt };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function clearUnsavedWork(appId: string): void {
|
|
132
|
+
const storage = unsavedStorage();
|
|
133
|
+
if (!storage) return;
|
|
134
|
+
try {
|
|
135
|
+
storage.removeItem(unsavedKey(appId));
|
|
136
|
+
} catch {
|
|
137
|
+
// See `storeUnsavedWork`.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Whether this browser has edited a document it made without an account. New
|
|
142
|
+
// and Upload ask before replacing one, so the marker outlives a reload; it
|
|
143
|
+
// carries the document's expiry so an entry past it is dropped on read.
|
|
144
|
+
const EDITED_KEY_PREFIX = 'brass:trial-edited:';
|
|
145
|
+
|
|
146
|
+
export function markTrialEdited(token: string, expiresAt: Date): void {
|
|
147
|
+
const storage = unsavedStorage();
|
|
148
|
+
if (!storage) return;
|
|
149
|
+
try {
|
|
150
|
+
storage.setItem(`${EDITED_KEY_PREFIX}${token}`, expiresAt.toISOString());
|
|
151
|
+
} catch {
|
|
152
|
+
// See `storeUnsavedWork`.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function trialWasEdited(token: string, now: number = Date.now()): boolean {
|
|
157
|
+
const storage = unsavedStorage();
|
|
158
|
+
if (!storage) return false;
|
|
159
|
+
let raw: string | null;
|
|
160
|
+
try {
|
|
161
|
+
raw = storage.getItem(`${EDITED_KEY_PREFIX}${token}`);
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (raw === null) return false;
|
|
166
|
+
const expiresAt = Date.parse(raw);
|
|
167
|
+
if (Number.isNaN(expiresAt) || expiresAt <= now) {
|
|
168
|
+
try {
|
|
169
|
+
storage.removeItem(`${EDITED_KEY_PREFIX}${token}`);
|
|
170
|
+
} catch {
|
|
171
|
+
// See `storeUnsavedWork`.
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// The claim secret the mint handed this browser, one per document, keyed by
|
|
179
|
+
// the app and the document's link token. Only the browser that made the
|
|
180
|
+
// document has one, so a sign-in here keeps the document and a sign-in
|
|
181
|
+
// wherever its link was shared does not. Written at the mint alone, so opening
|
|
182
|
+
// a shared link leaves it alone. Every mint prunes the secrets whose document
|
|
183
|
+
// has expired, since nothing else removes the secret of a document nobody
|
|
184
|
+
// came back to.
|
|
185
|
+
const CLAIM_KEY_PREFIX = 'brass:claim:';
|
|
186
|
+
|
|
187
|
+
function claimKey(appId: string, token: string): string {
|
|
188
|
+
return `${CLAIM_KEY_PREFIX}${appId}:${token}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Collected before any removal, since removing a key renumbers the rest.
|
|
192
|
+
function claimKeys(storage: Storage, prefix: string): string[] {
|
|
193
|
+
const keys: string[] = [];
|
|
194
|
+
try {
|
|
195
|
+
for (let i = 0; i < storage.length; i++) {
|
|
196
|
+
const key = storage.key(i);
|
|
197
|
+
if (key !== null && key.startsWith(prefix)) keys.push(key);
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// See `storeUnsavedWork`.
|
|
201
|
+
}
|
|
202
|
+
return keys;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function readClaimEntry(
|
|
206
|
+
storage: Storage,
|
|
207
|
+
key: string,
|
|
208
|
+
now: number,
|
|
209
|
+
): string | null {
|
|
210
|
+
let entry: { secret?: unknown; expires_at?: unknown } | null;
|
|
211
|
+
try {
|
|
212
|
+
entry = JSON.parse(storage.getItem(key) ?? 'null') as typeof entry;
|
|
213
|
+
} catch {
|
|
214
|
+
entry = null;
|
|
215
|
+
}
|
|
216
|
+
const expiresAt =
|
|
217
|
+
typeof entry?.expires_at === 'string' ? Date.parse(entry.expires_at) : NaN;
|
|
218
|
+
if (typeof entry?.secret !== 'string' || Number.isNaN(expiresAt) || expiresAt <= now) {
|
|
219
|
+
try {
|
|
220
|
+
storage.removeItem(key);
|
|
221
|
+
} catch {
|
|
222
|
+
// See `storeUnsavedWork`.
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return entry.secret;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function pruneClaimSecrets(storage: Storage, now: number): void {
|
|
230
|
+
for (const key of claimKeys(storage, CLAIM_KEY_PREFIX)) readClaimEntry(storage, key, now);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function storeClaimSecret(
|
|
234
|
+
appId: string,
|
|
235
|
+
token: string,
|
|
236
|
+
secret: string,
|
|
237
|
+
expiresAt: Date,
|
|
238
|
+
now: number = Date.now(),
|
|
239
|
+
): void {
|
|
240
|
+
const storage = unsavedStorage();
|
|
241
|
+
if (!storage) return;
|
|
242
|
+
pruneClaimSecrets(storage, now);
|
|
243
|
+
try {
|
|
244
|
+
storage.setItem(
|
|
245
|
+
claimKey(appId, token),
|
|
246
|
+
JSON.stringify({ secret, expires_at: expiresAt.toISOString() }),
|
|
247
|
+
);
|
|
248
|
+
} catch {
|
|
249
|
+
// See `storeUnsavedWork`.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function readClaimSecret(
|
|
254
|
+
appId: string,
|
|
255
|
+
token: string,
|
|
256
|
+
now: number = Date.now(),
|
|
257
|
+
): string | null {
|
|
258
|
+
const storage = unsavedStorage();
|
|
259
|
+
if (!storage) return null;
|
|
260
|
+
return readClaimEntry(storage, claimKey(appId, token), now);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// One document's secret, or with no token every secret this browser holds
|
|
264
|
+
// for the app.
|
|
265
|
+
export function clearClaimSecret(appId: string, token?: string): void {
|
|
266
|
+
const storage = unsavedStorage();
|
|
267
|
+
if (!storage) return;
|
|
268
|
+
const keys =
|
|
269
|
+
token === undefined
|
|
270
|
+
? claimKeys(storage, `${CLAIM_KEY_PREFIX}${appId}:`)
|
|
271
|
+
: [claimKey(appId, token)];
|
|
272
|
+
for (const key of keys) {
|
|
273
|
+
try {
|
|
274
|
+
storage.removeItem(key);
|
|
275
|
+
} catch {
|
|
276
|
+
// See `storeUnsavedWork`.
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// That this tab started a sign-in from a guest page, and on which token. Per
|
|
282
|
+
// tab (sessionStorage), written by `signIn()` and taken once on the return,
|
|
283
|
+
// so a URL carrying `brass_code` that this tab did not ask for claims nothing.
|
|
284
|
+
const SIGN_IN_INTENT_PREFIX = 'brass:claim-intent:';
|
|
285
|
+
|
|
286
|
+
function tabStorage(): Storage | null {
|
|
287
|
+
if (typeof sessionStorage === 'undefined') return null;
|
|
288
|
+
try {
|
|
289
|
+
return sessionStorage;
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function markClaimIntent(appId: string, token: string): void {
|
|
296
|
+
try {
|
|
297
|
+
tabStorage()?.setItem(`${SIGN_IN_INTENT_PREFIX}${appId}`, token);
|
|
298
|
+
} catch {
|
|
299
|
+
// See `storeUnsavedWork`.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function takeClaimIntent(appId: string): string | null {
|
|
304
|
+
const storage = tabStorage();
|
|
305
|
+
if (!storage) return null;
|
|
306
|
+
try {
|
|
307
|
+
const token = storage.getItem(`${SIGN_IN_INTENT_PREFIX}${appId}`);
|
|
308
|
+
storage.removeItem(`${SIGN_IN_INTENT_PREFIX}${appId}`);
|
|
309
|
+
return token;
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// The documents this browser claimed, per app, by the mint link each claim
|
|
316
|
+
// revoked. Reading that link again answers 410 for everyone, and a page that
|
|
317
|
+
// holds no session cannot ask the platform where it leads, so this is how a
|
|
318
|
+
// signed-out page on it knows to offer sign-in to the document instead. It
|
|
319
|
+
// confers no access (the open after sign-in is checked like any other), so a
|
|
320
|
+
// sign-out keeps it. Entries lapse after `CLAIMED_LINK_TTL_MS`.
|
|
321
|
+
const CLAIMED_KEY_PREFIX = 'brass:claimed:';
|
|
322
|
+
const CLAIMED_LINK_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
323
|
+
|
|
324
|
+
function claimedKey(appId: string, token: string): string {
|
|
325
|
+
return `${CLAIMED_KEY_PREFIX}${appId}:${token}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function readClaimedEntry(storage: Storage, key: string, now: number): string | null {
|
|
329
|
+
let entry: { doc_id?: unknown; claimed_at?: unknown } | null;
|
|
330
|
+
try {
|
|
331
|
+
entry = JSON.parse(storage.getItem(key) ?? 'null') as typeof entry;
|
|
332
|
+
} catch {
|
|
333
|
+
entry = null;
|
|
334
|
+
}
|
|
335
|
+
const claimedAt = typeof entry?.claimed_at === 'number' ? entry.claimed_at : NaN;
|
|
336
|
+
if (
|
|
337
|
+
typeof entry?.doc_id !== 'string' ||
|
|
338
|
+
Number.isNaN(claimedAt) ||
|
|
339
|
+
claimedAt + CLAIMED_LINK_TTL_MS <= now
|
|
340
|
+
) {
|
|
341
|
+
try {
|
|
342
|
+
storage.removeItem(key);
|
|
343
|
+
} catch {
|
|
344
|
+
// See `storeUnsavedWork`.
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
return entry.doc_id;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function storeClaimedLink(
|
|
352
|
+
appId: string,
|
|
353
|
+
token: string,
|
|
354
|
+
docId: string,
|
|
355
|
+
now: number = Date.now(),
|
|
356
|
+
): void {
|
|
357
|
+
const storage = unsavedStorage();
|
|
358
|
+
if (!storage) return;
|
|
359
|
+
for (const key of claimKeys(storage, CLAIMED_KEY_PREFIX)) readClaimedEntry(storage, key, now);
|
|
360
|
+
try {
|
|
361
|
+
storage.setItem(claimedKey(appId, token), JSON.stringify({ doc_id: docId, claimed_at: now }));
|
|
362
|
+
} catch {
|
|
363
|
+
// See `storeUnsavedWork`.
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function readClaimedLink(
|
|
368
|
+
appId: string,
|
|
369
|
+
token: string,
|
|
370
|
+
now: number = Date.now(),
|
|
371
|
+
): string | null {
|
|
372
|
+
const storage = unsavedStorage();
|
|
373
|
+
if (!storage) return null;
|
|
374
|
+
return readClaimedEntry(storage, claimedKey(appId, token), now);
|
|
375
|
+
}
|
package/src/handle-types.ts
CHANGED
|
@@ -90,8 +90,9 @@ export interface Splice<E> {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
* What changed in a list. Fires once on registration carrying the whole list
|
|
94
|
-
*
|
|
93
|
+
* What changed in a list. Fires once on registration carrying the whole list,
|
|
94
|
+
* then on every change carrying only what moved. No splice inserts more than
|
|
95
|
+
* 10,000 elements, so a spread apply stays inside the engine's argument limit.
|
|
95
96
|
*
|
|
96
97
|
* A registration fire and a resync are not special cases a subscriber has to
|
|
97
98
|
* recognise: both arrive as splices that replace what it was last told, so
|
|
@@ -137,28 +138,47 @@ export interface HandleOwner {
|
|
|
137
138
|
// Stamps the next commit with the mutate origin tag and commits, so the
|
|
138
139
|
// change's `origin.source` reads `'local'`.
|
|
139
140
|
commitMutate(): void;
|
|
141
|
+
// Runs a handle's `mutate` body, so every commit it takes carries the
|
|
142
|
+
// origin `options` asks for and the reference writes queued inside it
|
|
143
|
+
// commit after its last one.
|
|
144
|
+
runMutate(options: MutateOptions | undefined, fn: () => void): void;
|
|
140
145
|
// Classify a Loro event by what produced it. Derived from the commit's
|
|
141
146
|
// `by` + origin tag, so it survives microtask-deferred delivery.
|
|
142
147
|
originOf(event: unknown): ChangeSource;
|
|
148
|
+
// Report an app subscriber that threw. The handle carries on delivering to
|
|
149
|
+
// the others; this is how the app hears that its own callback failed.
|
|
150
|
+
reportSubscriberThrow(err: unknown): void;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Options every `mutate` takes. */
|
|
154
|
+
export interface MutateOptions {
|
|
155
|
+
/**
|
|
156
|
+
* `false` keeps the write off the reader's Undo stack, for a pass the app
|
|
157
|
+
* runs on its own rather than one the reader asked for. Defaults to true.
|
|
158
|
+
*/
|
|
159
|
+
undoable?: boolean;
|
|
143
160
|
}
|
|
144
161
|
|
|
145
162
|
// Public handle interfaces. Concrete classes below.
|
|
146
163
|
|
|
147
164
|
export interface MapHandle<T = unknown> {
|
|
148
|
-
|
|
165
|
+
/** Per-entry read. Returns undefined when missing. */
|
|
149
166
|
get(entryKey: string): T | undefined;
|
|
150
|
-
|
|
167
|
+
/** Keys currently in the map. */
|
|
151
168
|
keys(): string[];
|
|
152
|
-
|
|
169
|
+
/** Whether an entry exists at `entryKey`. */
|
|
153
170
|
has(entryKey: string): boolean;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
/** Every `[key, value]`, as a walk. Each step hands over one entry, so a
|
|
172
|
+
* caller that does not keep them never holds the whole map at once. */
|
|
173
|
+
entries(): IterableIterator<[string, T]>;
|
|
174
|
+
/** Mutate through a typed JS-proxy, scoped to this container; same
|
|
175
|
+
* ergonomics as `doc.mutate(d => { d.entities[id].field = ... })`. */
|
|
176
|
+
mutate(fn: (draft: Record<string, T>) => void, options?: MutateOptions): void;
|
|
177
|
+
/** ONE channel for every change whatever produced it (local / remote / undo
|
|
178
|
+
* / redo / checkout): the projection reconciles by value. The callback gets
|
|
179
|
+
* `change` and optional `origin` a render ignores. Fires once on
|
|
180
|
+
* registration with the whole map as `change.set`, then on every change
|
|
181
|
+
* (each later fire proportional to what that change wrote). */
|
|
162
182
|
subscribe(cb: MapSubscriber<T>): HandleUnsubscribe;
|
|
163
183
|
/**
|
|
164
184
|
* Report each time this container has been delivered whole: on
|
|
@@ -175,10 +195,10 @@ export interface MapHandle<T = unknown> {
|
|
|
175
195
|
*/
|
|
176
196
|
onSettled(cb: () => void): HandleUnsubscribe;
|
|
177
197
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
198
|
+
/** Child handle for `entries[entryKey]` when that entry is itself a map /
|
|
199
|
+
* list. Reads see empty until the container exists; the first mutate
|
|
200
|
+
* creates it, and a subscription taken before that delivers the container
|
|
201
|
+
* whole on whichever write creates it. */
|
|
182
202
|
map<U>(entryKey: string): MapHandle<U>;
|
|
183
203
|
list<U>(entryKey: string): ListHandle<U>;
|
|
184
204
|
}
|
|
@@ -202,12 +222,16 @@ export interface ListDraft<T> extends Array<T> {
|
|
|
202
222
|
|
|
203
223
|
export interface ListHandle<T = unknown> {
|
|
204
224
|
length(): number;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
225
|
+
/** The element at `index`, or undefined outside the list. */
|
|
226
|
+
get(index: number): T | undefined;
|
|
227
|
+
/** Every element in order, as a walk. */
|
|
228
|
+
values(): IterableIterator<T>;
|
|
229
|
+
mutate(fn: (draft: ListDraft<T>) => void, options?: MutateOptions): void;
|
|
230
|
+
/** Fires once on registration carrying the whole list, then on
|
|
231
|
+
* every change carrying only what moved, so a subscriber applies splices to
|
|
232
|
+
* what it already holds and never re-reads. A keyed collection belongs in a
|
|
233
|
+
* map (or `orderedMap`), which reconciles by key; a list is for ordering
|
|
234
|
+
* keys and small positional data. */
|
|
211
235
|
subscribe(cb: ListSubscriber<T>): HandleUnsubscribe;
|
|
212
236
|
/**
|
|
213
237
|
* Report each time this container has been delivered whole: on
|
|
@@ -255,36 +279,38 @@ export type OrderedMapPosition =
|
|
|
255
279
|
| { index: number };
|
|
256
280
|
|
|
257
281
|
export interface OrderedMapHandle<T = unknown> {
|
|
258
|
-
|
|
282
|
+
/** Per-entry read. Returns undefined when missing. */
|
|
259
283
|
get(entryKey: string): T | undefined;
|
|
260
|
-
|
|
284
|
+
/** Whether an entry exists in the map. */
|
|
261
285
|
has(entryKey: string): boolean;
|
|
262
|
-
|
|
286
|
+
/** Keys in order-list order, filtered to keys still present in the map. */
|
|
263
287
|
keys(): string[];
|
|
264
|
-
|
|
265
|
-
|
|
288
|
+
/** Every `[key, value]` in order, as a walk. */
|
|
289
|
+
entries(): IterableIterator<[string, T]>;
|
|
290
|
+
/** @deprecated Use `entries()`, which walks the same records in the same
|
|
291
|
+
* order without building the array. */
|
|
266
292
|
ordered(): { key: string; value: T }[];
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
mutate(fn: (draft: Record<string, T>) => void): void;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
293
|
+
/** Mutate the map; the sibling order list reconciles in the same commit
|
|
294
|
+
* (new keys appended in insertion order, deleted keys pruned). Drive it
|
|
295
|
+
* like `MapHandle.mutate`: `m[key] = record`, `delete m[key]`,
|
|
296
|
+
* `m[key].field = ...`. */
|
|
297
|
+
mutate(fn: (draft: Record<string, T>) => void, options?: MutateOptions): void;
|
|
298
|
+
/** Write `value` under `entryKey` and put that key at `at`, both in ONE
|
|
299
|
+
* commit (one undo step) like every other structural edit here. A key the
|
|
300
|
+
* map already holds is written and repositioned rather than named twice. */
|
|
275
301
|
insert(entryKey: string, value: T, at: OrderedMapPosition): void;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
302
|
+
/** Reposition a key the map already holds, writing the order list alone. A
|
|
303
|
+
* key the map does not hold does nothing, so a reorder racing a peer's
|
|
304
|
+
* delete of the same key leaves the delete standing. */
|
|
279
305
|
move(entryKey: string, to: OrderedMapPosition): void;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
306
|
+
/** Fires once on registration (the whole map as `change.set` in order),
|
|
307
|
+
* then on every change. A field edit carries only `set` / `deleted`; a
|
|
308
|
+
* change that moves the order also carries `change.order` (live keys in
|
|
309
|
+
* order) so the app re-sequences the views it holds. A move the map does
|
|
310
|
+
* not see (an edit of the sibling order list, a merge that resequences
|
|
311
|
+
* keys already present) arrives as an `order` of its own with an empty
|
|
312
|
+
* `set`. Content stays proportional to what the change wrote; re-sequencing
|
|
313
|
+
* happens only when the order moves, and at most once per change. */
|
|
288
314
|
subscribe(cb: MapSubscriber<T>): HandleUnsubscribe;
|
|
289
315
|
/**
|
|
290
316
|
* Report each time this container has been delivered whole: on
|