@brass-build/client 0.2.0 → 0.4.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 +26 -5
- package/CHANGELOG.md +61 -2
- package/README.md +247 -87
- package/dist/acting-organization.d.ts +131 -0
- package/dist/acting-organization.d.ts.map +1 -0
- package/dist/acting-organization.js +278 -0
- package/dist/acting-organization.js.map +1 -0
- package/dist/api-headers.js +7 -7
- package/dist/api-headers.js.map +1 -1
- package/dist/call-service.d.ts +74 -0
- package/dist/call-service.d.ts.map +1 -0
- package/dist/call-service.js +253 -0
- package/dist/call-service.js.map +1 -0
- package/dist/client.d.ts +29 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +583 -12
- package/dist/client.js.map +1 -1
- package/dist/document-checkpoints.d.ts +1 -0
- package/dist/document-checkpoints.d.ts.map +1 -1
- package/dist/document-checkpoints.js +27 -0
- package/dist/document-checkpoints.js.map +1 -1
- package/dist/document-contents.d.ts +15 -0
- package/dist/document-contents.d.ts.map +1 -0
- package/dist/document-contents.js +88 -0
- package/dist/document-contents.js.map +1 -0
- package/dist/document-signals.d.ts +31 -1
- package/dist/document-signals.d.ts.map +1 -1
- package/dist/document-signals.js +59 -0
- package/dist/document-signals.js.map +1 -1
- package/dist/document-streams.d.ts +4 -2
- package/dist/document-streams.d.ts.map +1 -1
- package/dist/document-streams.js +6 -0
- package/dist/document-streams.js.map +1 -1
- package/dist/document-thumbnail-capture.d.ts +20 -0
- package/dist/document-thumbnail-capture.d.ts.map +1 -1
- package/dist/document-thumbnail-capture.js +247 -9
- package/dist/document-thumbnail-capture.js.map +1 -1
- package/dist/documents-namespace.d.ts.map +1 -1
- package/dist/documents-namespace.js +3 -0
- package/dist/documents-namespace.js.map +1 -1
- package/dist/documents.d.ts +51 -3
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +415 -36
- package/dist/documents.js.map +1 -1
- package/dist/export-guest.d.ts.map +1 -1
- package/dist/export-guest.js +5 -0
- package/dist/export-guest.js.map +1 -1
- package/dist/handle-types.d.ts +23 -0
- package/dist/handle-types.d.ts.map +1 -1
- package/dist/handles.d.ts +1 -1
- package/dist/handles.d.ts.map +1 -1
- package/dist/handles.js +3 -3
- package/dist/handles.js.map +1 -1
- package/dist/hosted-ui.d.ts +10 -0
- package/dist/hosted-ui.d.ts.map +1 -1
- package/dist/hosted-ui.js +115 -30
- package/dist/hosted-ui.js.map +1 -1
- package/dist/importer.d.ts.map +1 -1
- package/dist/importer.js +28 -66
- package/dist/importer.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -1
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -0
- package/dist/internal.js.map +1 -1
- package/dist/mutate-batching.js +2 -2
- package/dist/mutate-batching.js.map +1 -1
- package/dist/open-document.d.ts +16 -1
- package/dist/open-document.d.ts.map +1 -1
- package/dist/open-document.js +123 -35
- package/dist/open-document.js.map +1 -1
- package/dist/presence.d.ts +10 -0
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js.map +1 -1
- package/dist/record-paging.d.ts +13 -14
- package/dist/record-paging.d.ts.map +1 -1
- package/dist/record-paging.js +13 -14
- package/dist/record-paging.js.map +1 -1
- package/dist/serializer.d.ts +3 -3
- package/dist/session-toolbar.d.ts +9 -0
- package/dist/session-toolbar.d.ts.map +1 -1
- package/dist/session-toolbar.js +191 -21
- package/dist/session-toolbar.js.map +1 -1
- package/dist/tail-ops.js +2 -2
- package/dist/tail-ops.js.map +1 -1
- package/dist/types.d.ts +0 -42
- package/dist/types.d.ts.map +1 -1
- package/dist/undo.d.ts +1 -1
- package/dist/undo.d.ts.map +1 -1
- package/dist/undo.js +1 -1
- package/dist/undo.js.map +1 -1
- package/dist/version-bar.d.ts.map +1 -1
- package/dist/version-bar.js +75 -4
- package/dist/version-bar.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/dist/wasm-ready-gate.d.ts +13 -0
- package/dist/wasm-ready-gate.d.ts.map +1 -0
- package/dist/wasm-ready-gate.js +28 -0
- package/dist/wasm-ready-gate.js.map +1 -0
- package/dist/wasm-runtime.browser.d.ts +11 -0
- package/dist/wasm-runtime.browser.d.ts.map +1 -0
- package/dist/wasm-runtime.browser.js +44 -0
- package/dist/wasm-runtime.browser.js.map +1 -0
- package/dist/wasm-runtime.d.ts +6 -0
- package/dist/wasm-runtime.d.ts.map +1 -0
- package/dist/wasm-runtime.js +20 -0
- package/dist/wasm-runtime.js.map +1 -0
- package/dist/when-saved.d.ts +2 -0
- package/dist/when-saved.d.ts.map +1 -0
- package/dist/when-saved.js +70 -0
- package/dist/when-saved.js.map +1 -0
- package/dist/ws-client.d.ts +4 -0
- package/dist/ws-client.d.ts.map +1 -1
- package/dist/ws-client.js +70 -1
- package/dist/ws-client.js.map +1 -1
- package/dist/ws-frame-limit.d.ts.map +1 -1
- package/dist/ws-frame-limit.js +9 -10
- package/dist/ws-frame-limit.js.map +1 -1
- package/docs/app-manifest.md +335 -5
- package/docs/custom-chrome.md +7 -2
- package/docs/presence.md +12 -0
- package/docs/services.md +265 -0
- package/docs/text-fields.md +191 -0
- package/package.json +4 -5
- package/src/acting-organization.ts +396 -0
- package/src/api-headers.ts +7 -7
- package/src/call-service.ts +354 -0
- package/src/client.ts +655 -15
- package/src/document-checkpoints.ts +30 -0
- package/src/document-contents.ts +104 -0
- package/src/document-signals.ts +113 -1
- package/src/document-streams.ts +15 -1
- package/src/document-thumbnail-capture.ts +253 -9
- package/src/documents-namespace.ts +20 -10
- package/src/documents.ts +460 -33
- package/src/export-guest.ts +5 -0
- package/src/handle-types.ts +35 -0
- package/src/handles.ts +7 -6
- package/src/hosted-ui.ts +155 -29
- package/src/importer.ts +28 -62
- package/src/index.ts +43 -4
- package/src/internal.ts +10 -0
- package/src/mutate-batching.ts +2 -2
- package/src/open-document.ts +179 -39
- package/src/presence.ts +6 -0
- package/src/record-paging.ts +13 -14
- package/src/serializer.ts +3 -3
- package/src/session-toolbar.ts +219 -22
- package/src/tail-ops.ts +2 -2
- package/src/types.ts +6 -4
- package/src/undo.ts +1 -1
- package/src/version-bar.ts +77 -7
- package/src/version.ts +1 -1
- package/src/wasm-ready-gate.ts +30 -0
- package/src/wasm-runtime.browser.ts +48 -0
- package/src/wasm-runtime.ts +21 -0
- package/src/when-saved.ts +66 -0
- package/src/ws-client.ts +70 -1
- package/src/ws-frame-limit.ts +9 -10
- package/dist/vite.d.ts +0 -31
- package/dist/vite.d.ts.map +0 -1
- package/dist/vite.js +0 -161
- package/dist/vite.js.map +0 -1
- package/src/vite.ts +0 -214
package/dist/documents.js
CHANGED
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
// 2. The `getOwnPropertyDescriptor` trap participates in
|
|
9
9
|
// `Object.keys`; doing `loroMap.keys().includes(key)` there makes
|
|
10
10
|
// `Object.keys()` O(N^2). It reads a single `loroMap.get(key)`.
|
|
11
|
-
import { LoroDoc, LoroMap, VersionVector, decodeImportBlobMeta, } from '
|
|
11
|
+
import { LoroDoc, LoroMap, VersionVector, decodeImportBlobMeta, } from './wasm-runtime.js';
|
|
12
12
|
import { PAGING_COMMIT_ORIGIN, UndoController } from './undo.js';
|
|
13
13
|
import { createOrderedMapHandle, createRootListHandle, createRootMapHandle, rebindRootListAnchor, rebindRootMapAnchor, resyncRootHandle, snapshotRootMapKeys, teardownRootHandle, } from './handles.js';
|
|
14
14
|
import { flattenPagedLookup, isPagedMarker, META_KEY, pagedRootKey, } from './record-paging.js';
|
|
15
15
|
import { MAX_OP_BLOB_CHARS } from './ws-frame-limit.js';
|
|
16
|
-
import { GoneSignal, WorkStatusSignal, DriveStatusSignal, } from './document-signals.js';
|
|
16
|
+
import { GoneSignal, WorkStatusSignal, DriveStatusSignal, DrivePublishTargetSignal, } from './document-signals.js';
|
|
17
17
|
import { ThumbnailCapture, } from './document-thumbnail-capture.js';
|
|
18
18
|
import { CheckpointScheduler, } from './document-checkpoints.js';
|
|
19
|
+
import { whenSaved } from './when-saved.js';
|
|
19
20
|
import { BranchController, } from './document-branches.js';
|
|
20
21
|
import { releaseUnsavedWork, trackUnsavedWork } from './unload-guard.js';
|
|
21
22
|
import { CommandMenu, } from './document-commands.js';
|
|
@@ -24,6 +25,31 @@ import { createEmitter } from './emitter.js';
|
|
|
24
25
|
// The stream name `doc.stream()` resolves to the document's own default
|
|
25
26
|
// state; mirrors the server's reserved name.
|
|
26
27
|
export const DEFAULT_STREAM_NAME = 'default';
|
|
28
|
+
// Loro names a container's type in its id (`cid:root-notes:Map`), so the kind
|
|
29
|
+
// comes off the enumeration itself and costs no read. A type this package
|
|
30
|
+
// cannot hand anyone a handle for, and cannot describe as text either, is
|
|
31
|
+
// left out rather than guessed at, so every entry a reader gets is one they
|
|
32
|
+
// can act on.
|
|
33
|
+
function rootKindOf(containerId) {
|
|
34
|
+
const type = containerId.slice(containerId.lastIndexOf(':') + 1);
|
|
35
|
+
if (type === 'Map')
|
|
36
|
+
return 'map';
|
|
37
|
+
if (type === 'MovableList' || type === 'List')
|
|
38
|
+
return 'list';
|
|
39
|
+
if (type === 'Text')
|
|
40
|
+
return 'text';
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// Maps count entries, lists and text count length. Both are the container's
|
|
44
|
+
// own counter rather than a projection of what it holds.
|
|
45
|
+
function rootIsEmpty(container) {
|
|
46
|
+
const sized = container;
|
|
47
|
+
if (typeof sized.size === 'number')
|
|
48
|
+
return sized.size === 0;
|
|
49
|
+
if (typeof sized.length === 'number')
|
|
50
|
+
return sized.length === 0;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
27
53
|
// Commit-origin tags stamped on every locally-produced commit via Loro's
|
|
28
54
|
// `setNextCommitOrigin`, so `origin.source` can be recovered off the
|
|
29
55
|
// event. Stamping the commit (not a JS-side flag) survives a
|
|
@@ -51,6 +77,26 @@ function changeSourceOf(event) {
|
|
|
51
77
|
return 'redo';
|
|
52
78
|
return 'local';
|
|
53
79
|
}
|
|
80
|
+
// Whether a text container stores `value` unchanged. Loro's text is UTF-8, so
|
|
81
|
+
// an unpaired surrogate is stored as U+FFFD and what the document holds is
|
|
82
|
+
// not the string that was written. Scanned here rather than through
|
|
83
|
+
// `String.prototype.isWellFormed`, which is younger than the browsers this
|
|
84
|
+
// package runs in.
|
|
85
|
+
function storedVerbatim(value) {
|
|
86
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
87
|
+
const unit = value.charCodeAt(i);
|
|
88
|
+
if (unit < 0xd800 || unit > 0xdfff)
|
|
89
|
+
continue;
|
|
90
|
+
// A trailing surrogate reached before its lead is unpaired by itself.
|
|
91
|
+
if (unit > 0xdbff)
|
|
92
|
+
return false;
|
|
93
|
+
i += 1;
|
|
94
|
+
const low = i < value.length ? value.charCodeAt(i) : 0;
|
|
95
|
+
if (low < 0xdc00 || low > 0xdfff)
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
54
100
|
// Per-chunk cap on a blob this Document pushes as several ops: a durability
|
|
55
101
|
// resync's missing slice (`computeResyncPush`) and a local commit no write
|
|
56
102
|
// budget bounded (`splitLocalUpdate`).
|
|
@@ -60,11 +106,10 @@ function changeSourceOf(event) {
|
|
|
60
106
|
// the HTTP op write. Sizing the chunk to a WebSocket frame instead is what
|
|
61
107
|
// makes a heal the most expensive write the SDK performs: the slice a bulk
|
|
62
108
|
// write leaves behind is the whole document, and at frame size that is
|
|
63
|
-
// thousands of ops
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
// tens of ops whose bytes go to the bucket instead.
|
|
109
|
+
// thousands of ops written one after another against the same document.
|
|
110
|
+
// Measured, a 40 MB divergence healed in 16 KB chunks overwhelmed that
|
|
111
|
+
// document's write path and left the join a tail it refused to serve. The
|
|
112
|
+
// same divergence at this size is tens of ops.
|
|
68
113
|
const RESYNC_PUSH_MAX_BYTES = 512 * 1024;
|
|
69
114
|
// Hard ceiling for a single irreducible op (one Loro op, unsplittable),
|
|
70
115
|
// beyond which the chunk is skipped with a warning rather than retried
|
|
@@ -244,10 +289,11 @@ const READ_ONLY_MESSAGES = {
|
|
|
244
289
|
app: 'Document is read-only: this app is not permitted to edit it.',
|
|
245
290
|
unsaved: 'Document is read-only: a change could not be saved, so this copy holds ' +
|
|
246
291
|
'work the platform does not. Brass is replacing it with the platform copy.',
|
|
292
|
+
capacity: 'Stream is read-only: it reached the largest size the platform can keep. ' +
|
|
293
|
+
'Reopen it to keep working from the saved state.',
|
|
294
|
+
no_connection: 'Document is read-only: it opened with no live connection, so a change ' +
|
|
295
|
+
'would stay on this device.',
|
|
247
296
|
};
|
|
248
|
-
// Public Document class. Wraps a LoroDoc; exposes mutate / subscribe /
|
|
249
|
-
// toJSON / close. Construction is cheap: the Loro subscriber attaches
|
|
250
|
-
// lazily (see `_ensureLoroSubscribed`).
|
|
251
297
|
export class Document {
|
|
252
298
|
_doc;
|
|
253
299
|
_rootKey;
|
|
@@ -341,9 +387,24 @@ export class Document {
|
|
|
341
387
|
this._fireSubscribers({ kind: 'branches' });
|
|
342
388
|
},
|
|
343
389
|
});
|
|
390
|
+
// The version of the newest picture the server holds of this document, or
|
|
391
|
+
// `null` for none. Seeded from the join and moved by this instance's own
|
|
392
|
+
// uploads; a picture older than the document's head is what arms the seed
|
|
393
|
+
// trigger, so a placeholder taken before the content arrived is replaced
|
|
394
|
+
// once it does.
|
|
395
|
+
_thumbnailVersion = null;
|
|
344
396
|
_thumbnails = new ThumbnailCapture({
|
|
345
|
-
|
|
397
|
+
// `_forcedReadOnly` and not the whole `readOnly`: the server refuses a
|
|
398
|
+
// thumbnail from a reader it will not take writes from, so a capture
|
|
399
|
+
// there costs a request and a warn to be told so. The load lock is the
|
|
400
|
+
// other half of `readOnly` and must NOT gate this, because a document
|
|
401
|
+
// still hydrating is exactly the one the seed watch is armed for, and
|
|
402
|
+
// the arm happens while that lock is still on.
|
|
403
|
+
isCaptureAllowed: () => this._isLive && this._checkout === null && !this._forcedReadOnly,
|
|
346
404
|
isAttached: () => this._isLive,
|
|
405
|
+
onCaptured: () => {
|
|
406
|
+
this._thumbnailVersion = this.currentSeq ?? 0;
|
|
407
|
+
},
|
|
347
408
|
});
|
|
348
409
|
_checkpoints = new CheckpointScheduler({
|
|
349
410
|
docId: () => this._docId,
|
|
@@ -354,6 +415,7 @@ export class Document {
|
|
|
354
415
|
if (this._undo)
|
|
355
416
|
this._undo.sealStep();
|
|
356
417
|
},
|
|
418
|
+
whenLocalOpsSaved: () => whenSaved(this),
|
|
357
419
|
});
|
|
358
420
|
// Drive push hook. `null` on the offline / test path; `pushToDrive`
|
|
359
421
|
// then throws with a clear message rather than silently dropping.
|
|
@@ -418,6 +480,11 @@ export class Document {
|
|
|
418
480
|
// surfaces a terminally-rejected op here so a "could not be saved" outcome
|
|
419
481
|
// reaches the app instead of being a silent forever-retry.
|
|
420
482
|
_errorReporter = null;
|
|
483
|
+
// Set when this document was opened expecting a live transport that was not
|
|
484
|
+
// available. A write then changes local state and reaches no server, so the
|
|
485
|
+
// first one is reported rather than dropped in silence.
|
|
486
|
+
_transportExpected = false;
|
|
487
|
+
_transportUnavailableReported = false;
|
|
421
488
|
_closed = false;
|
|
422
489
|
// Set true when the Document is parked in the SDK's per-(docId, branchId)
|
|
423
490
|
// cache after an app-level `close()`. The underlying LoroDoc, UndoController,
|
|
@@ -430,6 +497,14 @@ export class Document {
|
|
|
430
497
|
// next session). `_resume()`
|
|
431
498
|
// flips the flag back when the SDK hands the Doc to a new open() call.
|
|
432
499
|
_suspended = false;
|
|
500
|
+
// How many times this instance has been handed to an open. The park cache
|
|
501
|
+
// hands the SAME Document back on a reopen, so "this document is open" is
|
|
502
|
+
// not one interval per instance and a per-instance mark cannot say which
|
|
503
|
+
// open it was made in. Anything that spends a once-per-open budget on a
|
|
504
|
+
// Document (the import runner's one attempt) records this alongside its
|
|
505
|
+
// mark and re-reads it, so a reopen is a new open rather than the old one
|
|
506
|
+
// continuing.
|
|
507
|
+
_openEpoch = 0;
|
|
433
508
|
// Live = neither closed nor parked. This is the public-surface gate;
|
|
434
509
|
// internal ingress (remote ops, status emits) deliberately checks
|
|
435
510
|
// `_closed` alone, because a parked doc keeps applying remote state.
|
|
@@ -462,6 +537,9 @@ export class Document {
|
|
|
462
537
|
// Drive sync state, latest-retained. `null` until the open factory
|
|
463
538
|
// seeds it (drive-linked docs only) and on docs with no drive link.
|
|
464
539
|
_driveStatus = new DriveStatusSignal();
|
|
540
|
+
// Where this document could be published when it mirrors nothing yet.
|
|
541
|
+
// Seeded from the open, emptied once it becomes a mirror.
|
|
542
|
+
_drivePublishTarget = new DrivePublishTargetSignal();
|
|
465
543
|
// Document-scoped presence. `null` until the WS transport is wired
|
|
466
544
|
// on, and on the offline / test path where there is no transport.
|
|
467
545
|
presence = null;
|
|
@@ -590,6 +668,126 @@ export class Document {
|
|
|
590
668
|
// Built over the same `map(mapKey)` / `list(orderKey)` handles, so the
|
|
591
669
|
// paged-store presentation and subscriptions carry through.
|
|
592
670
|
_orderedMapHandles = new Map();
|
|
671
|
+
/**
|
|
672
|
+
* A handle on the top-level text container at `key`, which `roots()` reports
|
|
673
|
+
* as kind `'text'`.
|
|
674
|
+
*
|
|
675
|
+
* Cached per key, so `doc.text('x') === doc.text('x')` holds as it does for
|
|
676
|
+
* `doc.map`, and a subscriber registered through one keeps firing across a
|
|
677
|
+
* version checkout.
|
|
678
|
+
*/
|
|
679
|
+
text(key) {
|
|
680
|
+
this._assertOpen();
|
|
681
|
+
const cacheKey = `text:${key}`;
|
|
682
|
+
const existing = this._rootHandles.get(cacheKey);
|
|
683
|
+
if (existing)
|
|
684
|
+
return existing.handle;
|
|
685
|
+
const owner = this._handleOwner;
|
|
686
|
+
const activeSource = () => this._checkout !== null ? this._checkout.previewDoc : this._doc;
|
|
687
|
+
// Reads and writes resolve the active source per call, so a checkout
|
|
688
|
+
// needs no rebinding for them. A SUBSCRIPTION cannot work that way: it
|
|
689
|
+
// anchors a listener to one container, and a checkout preview is a
|
|
690
|
+
// different LoroDoc, so the anchor has to be moved. That is what makes
|
|
691
|
+
// this handle cached and registered like a map or a list, rather than a
|
|
692
|
+
// fresh object per call: the app holds the object the rebind reaches.
|
|
693
|
+
let anchor = activeSource().getText(key);
|
|
694
|
+
const subscribers = new Set();
|
|
695
|
+
let detach = null;
|
|
696
|
+
// The string this handle's own `update` has just written, armed only
|
|
697
|
+
// across the synchronous window in which `commit` fires the Loro
|
|
698
|
+
// subscriber. Materializing a text container is the one cost here that
|
|
699
|
+
// grows with a document's accumulated edit history rather than with its
|
|
700
|
+
// length, and `update` has already paid it once to diff against, so a
|
|
701
|
+
// fanout that materialized again would double the per-keystroke cost of
|
|
702
|
+
// exactly the field the README teaches. Anything else, a remote op or a
|
|
703
|
+
// checkout, finds it null and materializes.
|
|
704
|
+
let justWrote = null;
|
|
705
|
+
const fanout = (origin) => {
|
|
706
|
+
// Read for the local write alone. A change from any other source
|
|
707
|
+
// describes a container this handle did not write, so its value comes
|
|
708
|
+
// from the container even while a local write is arming one.
|
|
709
|
+
const value = (origin?.source === 'local' ? justWrote : null) ?? anchor.toString();
|
|
710
|
+
// Fired off a snapshot: a subscriber may unsubscribe (or register
|
|
711
|
+
// another) while it runs, and the set it is iterating is the one that
|
|
712
|
+
// changes underneath it.
|
|
713
|
+
const current = Array.from(subscribers);
|
|
714
|
+
for (const cb of current)
|
|
715
|
+
cb(value, origin);
|
|
716
|
+
};
|
|
717
|
+
const attach = () => {
|
|
718
|
+
if (detach !== null || subscribers.size === 0)
|
|
719
|
+
return;
|
|
720
|
+
detach = anchor.subscribe((event) => {
|
|
721
|
+
fanout({ source: owner.originOf(event) });
|
|
722
|
+
});
|
|
723
|
+
};
|
|
724
|
+
const handle = {
|
|
725
|
+
read: () => {
|
|
726
|
+
owner.assertReadable();
|
|
727
|
+
return activeSource().getText(key).toString();
|
|
728
|
+
},
|
|
729
|
+
update: (next) => {
|
|
730
|
+
owner.assertWritable();
|
|
731
|
+
owner.ensureLocalUpdatesSubscribed();
|
|
732
|
+
activeSource().getText(key).update(next);
|
|
733
|
+
// Armed after the write, so the container already holds `next` for
|
|
734
|
+
// every subscriber the commit reaches, and cleared however the
|
|
735
|
+
// commit returns: outside this window the fanout has no way to know
|
|
736
|
+
// the value is still current, and reads it. Armed only for a string
|
|
737
|
+
// the container holds verbatim, so a subscriber is never handed a
|
|
738
|
+
// value the document and its peers disagree with.
|
|
739
|
+
justWrote = storedVerbatim(next) ? next : null;
|
|
740
|
+
try {
|
|
741
|
+
owner.commitMutate();
|
|
742
|
+
}
|
|
743
|
+
finally {
|
|
744
|
+
justWrote = null;
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
subscribe: (cb) => {
|
|
748
|
+
owner.assertReadable();
|
|
749
|
+
subscribers.add(cb);
|
|
750
|
+
attach();
|
|
751
|
+
// No origin: a registration fire describes no authored transition,
|
|
752
|
+
// matching what a map subscriber's first fire carries.
|
|
753
|
+
cb(anchor.toString(), undefined);
|
|
754
|
+
return () => {
|
|
755
|
+
subscribers.delete(cb);
|
|
756
|
+
if (subscribers.size === 0 && detach !== null) {
|
|
757
|
+
detach();
|
|
758
|
+
detach = null;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
},
|
|
762
|
+
_rebind: (next) => {
|
|
763
|
+
if (detach !== null) {
|
|
764
|
+
detach();
|
|
765
|
+
detach = null;
|
|
766
|
+
}
|
|
767
|
+
anchor = next;
|
|
768
|
+
attach();
|
|
769
|
+
},
|
|
770
|
+
// A checkout or a return to head swaps the container rather than
|
|
771
|
+
// writing into it, so the catch-up fire carries no origin.
|
|
772
|
+
_resync: () => {
|
|
773
|
+
fanout(undefined);
|
|
774
|
+
},
|
|
775
|
+
// Detach the container subscription and forget the callbacks behind it,
|
|
776
|
+
// as a map or a list handle does: the Document drops its handle cache
|
|
777
|
+
// here, so an app still holding this one subscribes onto a handle
|
|
778
|
+
// nothing else reaches, and every callback registered before the park
|
|
779
|
+
// would come back with it.
|
|
780
|
+
_teardown: () => {
|
|
781
|
+
if (detach !== null) {
|
|
782
|
+
detach();
|
|
783
|
+
detach = null;
|
|
784
|
+
}
|
|
785
|
+
subscribers.clear();
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
this._rootHandles.set(cacheKey, { kind: 'text', key, handle });
|
|
789
|
+
return handle;
|
|
790
|
+
}
|
|
593
791
|
orderedMap(mapKey, orderKey) {
|
|
594
792
|
this._assertOpen();
|
|
595
793
|
const cacheKey = `${mapKey}\u0000${orderKey}`;
|
|
@@ -825,6 +1023,11 @@ export class Document {
|
|
|
825
1023
|
// doc still imports.
|
|
826
1024
|
if (this._closed)
|
|
827
1025
|
return;
|
|
1026
|
+
// Arm the seed BEFORE the import, because this is the moment the content
|
|
1027
|
+
// arrives and the Loro subscriber cannot be relied on to report it: it is
|
|
1028
|
+
// attached lazily, so a document whose content lands before the session has
|
|
1029
|
+
// bound its canvas has nobody subscribed and the import fires nothing.
|
|
1030
|
+
this._armSeedIfPictureIsStale();
|
|
828
1031
|
this._doc.import(snapshot);
|
|
829
1032
|
// The undo stack pre-snapshot referenced ops the new snapshot
|
|
830
1033
|
// supersedes. Drop both stacks so undo doesn't try to revert
|
|
@@ -1218,12 +1421,18 @@ export class Document {
|
|
|
1218
1421
|
if (entry.kind === 'map') {
|
|
1219
1422
|
rebindRootMapAnchor(entry.handle, source.getMap(entry.key));
|
|
1220
1423
|
}
|
|
1221
|
-
else {
|
|
1424
|
+
else if (entry.kind === 'list') {
|
|
1222
1425
|
rebindRootListAnchor(entry.handle, source.getMovableList(entry.key));
|
|
1223
1426
|
}
|
|
1427
|
+
else {
|
|
1428
|
+
entry.handle._rebind(source.getText(entry.key));
|
|
1429
|
+
}
|
|
1224
1430
|
}
|
|
1225
1431
|
for (const entry of this._rootHandles.values()) {
|
|
1226
|
-
|
|
1432
|
+
if (entry.kind === 'text')
|
|
1433
|
+
entry.handle._resync();
|
|
1434
|
+
else
|
|
1435
|
+
resyncRootHandle(entry.handle);
|
|
1227
1436
|
}
|
|
1228
1437
|
}
|
|
1229
1438
|
/**
|
|
@@ -1300,6 +1509,21 @@ export class Document {
|
|
|
1300
1509
|
setThumbnailUploader(uploader) {
|
|
1301
1510
|
this._thumbnails.setUploader(uploader);
|
|
1302
1511
|
}
|
|
1512
|
+
// Tell the document which version the server's picture of it depicts, from
|
|
1513
|
+
// the join, and start a seed watch when that picture is behind the content.
|
|
1514
|
+
/** @internal */
|
|
1515
|
+
_setThumbnailVersion(version) {
|
|
1516
|
+
this._thumbnailVersion = version;
|
|
1517
|
+
this._armSeedIfPictureIsStale();
|
|
1518
|
+
}
|
|
1519
|
+
// A picture at or above the head depicts what the document currently holds.
|
|
1520
|
+
_armSeedIfPictureIsStale() {
|
|
1521
|
+
const head = this.currentSeq ?? 0;
|
|
1522
|
+
if (this._thumbnailVersion !== null && this._thumbnailVersion >= head) {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
this._thumbnails.armSeed();
|
|
1526
|
+
}
|
|
1303
1527
|
// Install the checkpoint-create hook; `null` detaches.
|
|
1304
1528
|
/** @internal */
|
|
1305
1529
|
setCheckpointCreator(creator) {
|
|
@@ -1365,11 +1589,11 @@ export class Document {
|
|
|
1365
1589
|
}
|
|
1366
1590
|
return await this._driveResolver();
|
|
1367
1591
|
}
|
|
1368
|
-
// Fetch the
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1592
|
+
// Fetch the file a document was uploaded as: a short-lived presigned GET
|
|
1593
|
+
// plus the file's name / content type / size. An app that knows the format
|
|
1594
|
+
// fetches the bytes from `url`, parses them, and writes the structured
|
|
1595
|
+
// content into this document through the normal handle surface
|
|
1596
|
+
// (`doc.map('instances').mutate(...)`):
|
|
1373
1597
|
//
|
|
1374
1598
|
// const src = await doc.sourceBlob();
|
|
1375
1599
|
// const bytes = new Uint8Array(await (await fetch(src.url)).arrayBuffer());
|
|
@@ -1381,9 +1605,8 @@ export class Document {
|
|
|
1381
1605
|
// each with its own URL, so a multi-stream format (a glTF with its
|
|
1382
1606
|
// `.bin` buffer) fetches only the streams it needs.
|
|
1383
1607
|
//
|
|
1384
|
-
// Rejects with a `BrassError` when the document
|
|
1385
|
-
//
|
|
1386
|
-
// reach it (both 404).
|
|
1608
|
+
// Rejects with a `BrassError` when the document holds no such file, or
|
|
1609
|
+
// the caller cannot reach it (both 404).
|
|
1387
1610
|
async sourceBlob() {
|
|
1388
1611
|
this._assertOpen();
|
|
1389
1612
|
if (this._docId === null) {
|
|
@@ -1480,11 +1703,24 @@ export class Document {
|
|
|
1480
1703
|
}
|
|
1481
1704
|
const inner = await runtime.openLine(info.line_id);
|
|
1482
1705
|
const stream = new DocumentStream(name, inner, true);
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1706
|
+
try {
|
|
1707
|
+
// Opened while a version preview is active: bring the new stream to
|
|
1708
|
+
// the same point in time rather than showing it live under a
|
|
1709
|
+
// historical default.
|
|
1710
|
+
if (this._checkout !== null) {
|
|
1711
|
+
await stream._previewAt(this._checkoutStreamSeqs?.[info.line_id], true);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
catch (err) {
|
|
1715
|
+
// The line is joined and subscribed by now, and a rejected open is
|
|
1716
|
+
// dropped from the cache so the next call opens a second one. Nothing
|
|
1717
|
+
// else holds this Document, and nothing else would ever close it: the
|
|
1718
|
+
// parent closes the streams it has, and a stream whose open failed is
|
|
1719
|
+
// not one of them. So the failure disposes what it is abandoning,
|
|
1720
|
+
// rather than leaving a line applying remote ops for the life of the
|
|
1721
|
+
// page.
|
|
1722
|
+
stream._close();
|
|
1723
|
+
throw err;
|
|
1488
1724
|
}
|
|
1489
1725
|
return stream;
|
|
1490
1726
|
})();
|
|
@@ -1675,6 +1911,37 @@ export class Document {
|
|
|
1675
1911
|
this._errorReporter = reporter;
|
|
1676
1912
|
this._commandMenu.setErrorReporter(reporter);
|
|
1677
1913
|
}
|
|
1914
|
+
// Called by the factory when a document is opened with no live transport,
|
|
1915
|
+
// which outside tests means `wsBaseUrl` was left unset. Marking it is what
|
|
1916
|
+
// separates that from the snapshot reads and test stubs that legitimately
|
|
1917
|
+
// have no transport and must stay quiet.
|
|
1918
|
+
/** @internal */
|
|
1919
|
+
markTransportUnavailable() {
|
|
1920
|
+
this._transportExpected = true;
|
|
1921
|
+
}
|
|
1922
|
+
// A write on such a document reaches nothing: local state changes, the
|
|
1923
|
+
// reader sees their edit, and no server ever hears about it, so the edit is
|
|
1924
|
+
// gone on the next open with no failure anywhere. This is what makes that
|
|
1925
|
+
// observable. Once per document, because the condition holds for as long as
|
|
1926
|
+
// the document is open and repeating it per keystroke buries it.
|
|
1927
|
+
_reportTransportUnavailable() {
|
|
1928
|
+
if (!this._transportExpected)
|
|
1929
|
+
return;
|
|
1930
|
+
if (this._transportUnavailableReported)
|
|
1931
|
+
return;
|
|
1932
|
+
this._transportUnavailableReported = true;
|
|
1933
|
+
// Lock the document as well as reporting it. The write that reached this
|
|
1934
|
+
// point stays local, exactly as `unsaved` describes for a refused one, so
|
|
1935
|
+
// the same gate closes: an app that disables on `readOnly` stops offering
|
|
1936
|
+
// an editor that cannot save, and a later `mutate` throws a sentence
|
|
1937
|
+
// naming the reason instead of succeeding into nothing. The lock lands
|
|
1938
|
+
// after this write's own gate has passed, so the first one still runs
|
|
1939
|
+
// locally and only what follows it is refused.
|
|
1940
|
+
this._markReadOnly('no_connection');
|
|
1941
|
+
this._reportError(new Error('This document has no live connection, so edits stay on this device and ' +
|
|
1942
|
+
'reach no one else. Pass wsBaseUrl to Brass.init alongside the other ' +
|
|
1943
|
+
'base URLs: naming any one of them leaves wsBaseUrl unset.'));
|
|
1944
|
+
}
|
|
1678
1945
|
_reportError(err) {
|
|
1679
1946
|
if (this._errorReporter)
|
|
1680
1947
|
this._errorReporter(err);
|
|
@@ -1749,8 +2016,9 @@ export class Document {
|
|
|
1749
2016
|
* reconnects, and under which correlator, which is what separates a
|
|
1750
2017
|
* connection that is answering nothing from one that is merely behind.
|
|
1751
2018
|
*
|
|
1752
|
-
* @internal
|
|
1753
|
-
* to know a save is outstanding, and the SDK resolves a stuck
|
|
2019
|
+
* @internal Read on demand by Brass's own surfaces. An app reads
|
|
2020
|
+
* `savingCount` to know a save is outstanding, and the SDK resolves a stuck
|
|
2021
|
+
* one itself.
|
|
1754
2022
|
*/
|
|
1755
2023
|
diagnostics() {
|
|
1756
2024
|
const own = this._diagnosticsProvider?.() ?? {
|
|
@@ -1946,8 +2214,10 @@ export class Document {
|
|
|
1946
2214
|
_ensureLocalUpdatesSubscribed() {
|
|
1947
2215
|
if (this._unsubLocal)
|
|
1948
2216
|
return;
|
|
1949
|
-
if (!this._localOpHook)
|
|
2217
|
+
if (!this._localOpHook) {
|
|
2218
|
+
this._reportTransportUnavailable();
|
|
1950
2219
|
return;
|
|
2220
|
+
}
|
|
1951
2221
|
// Loro's `subscribeLocalUpdates` fires after every local commit
|
|
1952
2222
|
// with the op-blob bytes for that commit. Importing a remote op
|
|
1953
2223
|
// does NOT trigger this callback, so we don't risk feeding the
|
|
@@ -2039,9 +2309,9 @@ export class Document {
|
|
|
2039
2309
|
this._maybeFireUndoState();
|
|
2040
2310
|
// A local commit (mutate / undo / redo) schedules a debounced
|
|
2041
2311
|
// thumbnail capture, when the app has registered a canvas via
|
|
2042
|
-
// `useCanvas`.
|
|
2043
|
-
// a passive collaborator
|
|
2044
|
-
// someone else's edit.
|
|
2312
|
+
// `useCanvas`. The debounce is for the peer that drew the change, so
|
|
2313
|
+
// it is local-only: a passive collaborator re-uploading a screenshot
|
|
2314
|
+
// of someone else's edit is what a per-op capture would be.
|
|
2045
2315
|
if (isLocalCommit) {
|
|
2046
2316
|
this._thumbnails.schedule();
|
|
2047
2317
|
// Same reasoning for the implicit auto-checkpoint: only the
|
|
@@ -2050,6 +2320,18 @@ export class Document {
|
|
|
2050
2320
|
// idle period.
|
|
2051
2321
|
this._checkpoints.scheduleAuto();
|
|
2052
2322
|
}
|
|
2323
|
+
else {
|
|
2324
|
+
// Content arriving with no local commit behind it is what an import
|
|
2325
|
+
// looks like from here, and it moves the head past whatever picture
|
|
2326
|
+
// the document has. This DOES reach a reader who drew none of it,
|
|
2327
|
+
// which the debounce above deliberately does not: an import runs on
|
|
2328
|
+
// whichever client claimed it, so gating the seed on local authorship
|
|
2329
|
+
// would leave the documents that most need a picture without one.
|
|
2330
|
+
// Every such reader captures the same document at the same version,
|
|
2331
|
+
// and the targeted snapshot each upload enqueues is idempotent on
|
|
2332
|
+
// that version, so the concurrent seeds resolve themselves.
|
|
2333
|
+
this._armSeedIfPictureIsStale();
|
|
2334
|
+
}
|
|
2053
2335
|
});
|
|
2054
2336
|
}
|
|
2055
2337
|
// Subscribe to document events. Returns an unsubscribe handle.
|
|
@@ -2245,6 +2527,49 @@ export class Document {
|
|
|
2245
2527
|
if (this._closed)
|
|
2246
2528
|
return;
|
|
2247
2529
|
this._driveStatus.emit(status);
|
|
2530
|
+
// A document that has become a mirror publishes through its link from
|
|
2531
|
+
// now on, so the create-an-artifact offer goes with the arrival of the
|
|
2532
|
+
// status that replaces it.
|
|
2533
|
+
this._drivePublishTarget.set(null);
|
|
2534
|
+
}
|
|
2535
|
+
// Whether this document's export runs in THIS app, which is what decides
|
|
2536
|
+
// whether a push carries bytes. Not on `driveStatus`: it is a fact about
|
|
2537
|
+
// the caller rather than about the document, and it is all an app is told
|
|
2538
|
+
// about the exporter, never which app it is when it is not this one.
|
|
2539
|
+
_exportsHere = false;
|
|
2540
|
+
/** @internal */
|
|
2541
|
+
get _exportRunsHere() {
|
|
2542
|
+
return this._exportsHere;
|
|
2543
|
+
}
|
|
2544
|
+
/** @internal */
|
|
2545
|
+
_setExportRunsHere(runsHere) {
|
|
2546
|
+
this._exportsHere = runsHere;
|
|
2547
|
+
}
|
|
2548
|
+
// Internal: where this document can be published while it mirrors nothing.
|
|
2549
|
+
/** @internal */
|
|
2550
|
+
get _publishTarget() {
|
|
2551
|
+
return this._drivePublishTarget.target;
|
|
2552
|
+
}
|
|
2553
|
+
/** @internal */
|
|
2554
|
+
_setPublishTarget(target) {
|
|
2555
|
+
if (this._closed)
|
|
2556
|
+
return;
|
|
2557
|
+
this._drivePublishTarget.set(target);
|
|
2558
|
+
}
|
|
2559
|
+
// Internal: how the publish of a document that mirrors nothing is going.
|
|
2560
|
+
/** @internal */
|
|
2561
|
+
get _publishState() {
|
|
2562
|
+
return this._drivePublishTarget.state;
|
|
2563
|
+
}
|
|
2564
|
+
/** @internal */
|
|
2565
|
+
_emitPublishState(state) {
|
|
2566
|
+
if (this._closed)
|
|
2567
|
+
return;
|
|
2568
|
+
this._drivePublishTarget.setState(state);
|
|
2569
|
+
}
|
|
2570
|
+
/** @internal */
|
|
2571
|
+
_onPublishState(cb) {
|
|
2572
|
+
return this._drivePublishTarget.onState(cb);
|
|
2248
2573
|
}
|
|
2249
2574
|
// Internal: mark the doc evicted. Idempotent (multiple frames arrive
|
|
2250
2575
|
// for the same doc when both `doc:<id>` and `presence:doc:<id>`
|
|
@@ -2263,6 +2588,45 @@ export class Document {
|
|
|
2263
2588
|
}
|
|
2264
2589
|
this._gone.mark(reason);
|
|
2265
2590
|
}
|
|
2591
|
+
/**
|
|
2592
|
+
* The top-level collections of the stream this document is open at, keyed
|
|
2593
|
+
* as it keys them, each naming the kind of handle that reads it.
|
|
2594
|
+
*
|
|
2595
|
+
* A partitioned document keeps its collections per stream, so this is the
|
|
2596
|
+
* open stream's answer rather than every stream's. `listStreams` names the
|
|
2597
|
+
* rest, and `stream.roots()` asks one of them the same question.
|
|
2598
|
+
*
|
|
2599
|
+
* `map` / `list` are get-or-create, and taking the wrong kind for a key
|
|
2600
|
+
* materializes a second container beside the real one, so read this before
|
|
2601
|
+
* taking a handle on a key you are unsure of. A collection holding nothing
|
|
2602
|
+
* is not listed, which is what keeps a handle taken but never written from
|
|
2603
|
+
* appearing as one.
|
|
2604
|
+
*
|
|
2605
|
+
* The cost is the number of collections rather than the size of the
|
|
2606
|
+
* document. In checkout, it describes the version being previewed.
|
|
2607
|
+
*/
|
|
2608
|
+
roots() {
|
|
2609
|
+
this._assertNotClosed();
|
|
2610
|
+
const source = this._checkout !== null ? this._checkout.previewDoc : this._doc;
|
|
2611
|
+
const found = [];
|
|
2612
|
+
for (const [key, containerId] of Object.entries(source.getShallowValue())) {
|
|
2613
|
+
const kind = rootKindOf(containerId);
|
|
2614
|
+
if (kind === null)
|
|
2615
|
+
continue;
|
|
2616
|
+
// Reading the container's own count, rather than its value, is what
|
|
2617
|
+
// keeps this off the projection: an empty collection is skipped without
|
|
2618
|
+
// anything being decoded.
|
|
2619
|
+
const container = source.getContainerById(containerId);
|
|
2620
|
+
if (container === undefined || rootIsEmpty(container))
|
|
2621
|
+
continue;
|
|
2622
|
+
found.push([key, kind]);
|
|
2623
|
+
}
|
|
2624
|
+
// Loro reports roots in registration order, which is an accident of who
|
|
2625
|
+
// wrote what first. Inserting in key order makes a reader's iteration
|
|
2626
|
+
// stable across opens.
|
|
2627
|
+
found.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
2628
|
+
return Object.fromEntries(found);
|
|
2629
|
+
}
|
|
2266
2630
|
// Plain-JS projection of the current state.
|
|
2267
2631
|
toJSON() {
|
|
2268
2632
|
this._assertNotClosed();
|
|
@@ -2496,7 +2860,7 @@ export class Document {
|
|
|
2496
2860
|
// undo step, so a bulk write that correctly split into many ops comes back
|
|
2497
2861
|
// as one op the size of the entire write. Past `MAX_OP_BLOB_CHARS` that op
|
|
2498
2862
|
// is refused outright and the redone edit never persists, and below it, it
|
|
2499
|
-
// still lands as
|
|
2863
|
+
// still lands as one very large write against the document.
|
|
2500
2864
|
//
|
|
2501
2865
|
// The ops already exist, so the only lever left is which spans they ship in.
|
|
2502
2866
|
// The blob names the counter range it covers, and re-exporting a sub-range
|
|
@@ -2580,6 +2944,7 @@ export class Document {
|
|
|
2580
2944
|
this._gone.clearListeners();
|
|
2581
2945
|
this._workStatus.clearListeners();
|
|
2582
2946
|
this._driveStatus.clearListeners();
|
|
2947
|
+
this._drivePublishTarget.clearListeners();
|
|
2583
2948
|
// Presence subscribes its OWN WS topic and keeps it alive across a park
|
|
2584
2949
|
// (resume does not re-create the controller), so drop its app-side
|
|
2585
2950
|
// change listeners here the same way as the sets above: a parked doc
|
|
@@ -2595,7 +2960,10 @@ export class Document {
|
|
|
2595
2960
|
// the app's callback on a doc it has closed. Detaching first stops that;
|
|
2596
2961
|
// fresh handles are minted on resume + next access.
|
|
2597
2962
|
for (const entry of this._rootHandles.values()) {
|
|
2598
|
-
|
|
2963
|
+
if (entry.kind === 'text')
|
|
2964
|
+
entry.handle._teardown();
|
|
2965
|
+
else
|
|
2966
|
+
teardownRootHandle(entry.handle);
|
|
2599
2967
|
}
|
|
2600
2968
|
this._rootHandles.clear();
|
|
2601
2969
|
// Drop the ordered-map cache too. Each wrapper holds the specific root
|
|
@@ -2611,6 +2979,7 @@ export class Document {
|
|
|
2611
2979
|
/** @internal */
|
|
2612
2980
|
_resume() {
|
|
2613
2981
|
this._suspended = false;
|
|
2982
|
+
this._openEpoch += 1;
|
|
2614
2983
|
this._setStreamsSuspended(false);
|
|
2615
2984
|
// Pair of the `_suspend` presence gate: reopen the presence controller's
|
|
2616
2985
|
// outbound path and re-advertise this client's cursor on the resumed open.
|
|
@@ -2627,6 +2996,12 @@ export class Document {
|
|
|
2627
2996
|
get _isSuspended() {
|
|
2628
2997
|
return this._suspended;
|
|
2629
2998
|
}
|
|
2999
|
+
/** @internal Which open this Document is currently in (see `_openEpoch`).
|
|
3000
|
+
* Read by anything holding a per-open mark on a Document, since a park hit
|
|
3001
|
+
* hands the same instance to the next open. */
|
|
3002
|
+
get _openGeneration() {
|
|
3003
|
+
return this._openEpoch;
|
|
3004
|
+
}
|
|
2630
3005
|
close() {
|
|
2631
3006
|
if (this._closed)
|
|
2632
3007
|
return;
|
|
@@ -2645,7 +3020,7 @@ export class Document {
|
|
|
2645
3020
|
this._undo.free();
|
|
2646
3021
|
this._undo = null;
|
|
2647
3022
|
}
|
|
2648
|
-
this._thumbnails.
|
|
3023
|
+
this._thumbnails.destroy();
|
|
2649
3024
|
this._checkpoints.cancelAuto();
|
|
2650
3025
|
this._localOpHook = null;
|
|
2651
3026
|
this._currentSeqProvider = null;
|
|
@@ -2661,11 +3036,15 @@ export class Document {
|
|
|
2661
3036
|
this._gone.clearListeners();
|
|
2662
3037
|
this._workStatus.clearListeners();
|
|
2663
3038
|
this._driveStatus.clearListeners();
|
|
3039
|
+
this._drivePublishTarget.clearListeners();
|
|
2664
3040
|
// Detach handle subscriptions before freeing the doc (below): a handle's
|
|
2665
3041
|
// per-container Loro subscription outlives the cache reference, and
|
|
2666
3042
|
// `_doc.free()` must not run with subscriptions still attached.
|
|
2667
3043
|
for (const entry of this._rootHandles.values()) {
|
|
2668
|
-
|
|
3044
|
+
if (entry.kind === 'text')
|
|
3045
|
+
entry.handle._teardown();
|
|
3046
|
+
else
|
|
3047
|
+
teardownRootHandle(entry.handle);
|
|
2669
3048
|
}
|
|
2670
3049
|
this._rootHandles.clear();
|
|
2671
3050
|
this._orderedMapHandles.clear();
|