@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
package/src/handles.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
PAGE_KEYS_FIELD,
|
|
20
20
|
type EncodedPage,
|
|
21
21
|
type RecordLocationSink,
|
|
22
|
+
BODY_FIELD,
|
|
23
|
+
TAG_FIELD,
|
|
22
24
|
assertUsableRecordKey,
|
|
23
25
|
classifyStoreKey,
|
|
24
26
|
decodePageKeys,
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
decodeRecordValue,
|
|
27
29
|
encodePage,
|
|
28
30
|
flattenPagedLookup,
|
|
31
|
+
isPagedMarkerEntry,
|
|
29
32
|
isReservedKey,
|
|
30
33
|
overlayKeyFor,
|
|
31
34
|
packWholeValue,
|
|
@@ -58,9 +61,11 @@ import type {
|
|
|
58
61
|
ListSubscriber,
|
|
59
62
|
MapHandle,
|
|
60
63
|
MapSubscriber,
|
|
64
|
+
MutateOptions,
|
|
61
65
|
OrderedMapHandle,
|
|
62
66
|
OrderedMapPosition,
|
|
63
67
|
} from './handle-types.js';
|
|
68
|
+
import { pushBoundedSplice } from './subscriber-delivery.js';
|
|
64
69
|
|
|
65
70
|
export type {
|
|
66
71
|
Change,
|
|
@@ -227,18 +232,46 @@ function assignList(target: LoroMovableList, value: readonly unknown[]): void {
|
|
|
227
232
|
// same position, so the pair leaves the entry stored twice while `set`
|
|
228
233
|
// leaves it stored once. That is the same reasoning `assignListPosition`
|
|
229
234
|
// carries, and a whole-array assign is the write that reaches it most.
|
|
235
|
+
//
|
|
236
|
+
// The entries both ends share are left alone, and only the span between
|
|
237
|
+
// them is rewritten. Dropping the first of `[a, b]` is then a delete of `a`
|
|
238
|
+
// rather than `b` set into position 0 and the tail cut off, which matters
|
|
239
|
+
// when a peer undoes its own edit to `b` concurrently: the undo removes the
|
|
240
|
+
// `b` it wrote, and a set of `b` would put one back.
|
|
230
241
|
const oldLen = target.length;
|
|
231
|
-
|
|
242
|
+
let head = 0;
|
|
243
|
+
while (
|
|
244
|
+
head < oldLen &&
|
|
245
|
+
head < value.length &&
|
|
246
|
+
samePlainValue(plainListItem(target, head), materializeContainers(value[head]))
|
|
247
|
+
) {
|
|
248
|
+
head += 1;
|
|
249
|
+
}
|
|
250
|
+
let tail = 0;
|
|
251
|
+
while (
|
|
252
|
+
tail < oldLen - head &&
|
|
253
|
+
tail < value.length - head &&
|
|
254
|
+
samePlainValue(
|
|
255
|
+
plainListItem(target, oldLen - 1 - tail),
|
|
256
|
+
materializeContainers(value[value.length - 1 - tail]),
|
|
257
|
+
)
|
|
258
|
+
) {
|
|
259
|
+
tail += 1;
|
|
260
|
+
}
|
|
261
|
+
const oldMid = oldLen - head - tail;
|
|
262
|
+
const newMid = value.length - head - tail;
|
|
263
|
+
const shared = Math.min(oldMid, newMid);
|
|
264
|
+
for (let i = head; i < head + shared; i++) {
|
|
232
265
|
const item = materializeContainers(value[i]);
|
|
233
|
-
if (i >= oldLen) {
|
|
234
|
-
insertListAt(target, i, item);
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
266
|
if (samePlainValue(plainListItem(target, i), item)) continue;
|
|
238
267
|
assignListPosition(target, i, item);
|
|
239
268
|
}
|
|
240
|
-
if (
|
|
241
|
-
|
|
269
|
+
if (newMid > oldMid) {
|
|
270
|
+
for (let i = head + oldMid; i < head + newMid; i++) {
|
|
271
|
+
insertListAt(target, i, materializeContainers(value[i]));
|
|
272
|
+
}
|
|
273
|
+
} else if (oldMid > newMid) {
|
|
274
|
+
target.delete(head + newMid, oldMid - newMid);
|
|
242
275
|
}
|
|
243
276
|
}
|
|
244
277
|
|
|
@@ -292,11 +325,22 @@ function assignListPosition(
|
|
|
292
325
|
rawItem: unknown,
|
|
293
326
|
): void {
|
|
294
327
|
const item = materializeContainers(rawItem);
|
|
328
|
+
// A position already holding a container of the same kind is written in
|
|
329
|
+
// place, as `assignEntry` does for a map key. A fresh container at the
|
|
330
|
+
// position is a replace that a peer's concurrent replace of the same
|
|
331
|
+
// position does not merge with, so undoing either one restores its old
|
|
332
|
+
// container beside the other's instead of in its place, and the list
|
|
333
|
+
// comes out one element longer on every peer.
|
|
334
|
+
const existing = target.get(idx);
|
|
295
335
|
if (isPlainObject(item)) {
|
|
296
|
-
const child =
|
|
336
|
+
const child =
|
|
337
|
+
existing instanceof LoroMap ? existing : target.setContainer(idx, new LoroMap());
|
|
297
338
|
assignPlain(child, item);
|
|
298
339
|
} else if (Array.isArray(item)) {
|
|
299
|
-
const child =
|
|
340
|
+
const child =
|
|
341
|
+
existing instanceof LoroMovableList
|
|
342
|
+
? existing
|
|
343
|
+
: target.setContainer(idx, new LoroMovableList());
|
|
300
344
|
assignList(child, item);
|
|
301
345
|
} else {
|
|
302
346
|
target.set(idx, item as never);
|
|
@@ -398,7 +442,7 @@ interface WriteDivert {
|
|
|
398
442
|
// `loroMap.get(key)` (O(1) per key, so `Object.keys()` is O(N)). Iterating
|
|
399
443
|
// `loroMap.keys()` in the descriptor trap makes `Object.keys()` O(N^2) at
|
|
400
444
|
// 50k entities (measured at >5 min of CPU before giving up).
|
|
401
|
-
|
|
445
|
+
function wrapLoroMapProxy(
|
|
402
446
|
loroMap: LoroMap,
|
|
403
447
|
// Threaded into nested record proxies too, so a field edit counts. A
|
|
404
448
|
// mutation can hold thousands of them (`m[id].params = ...` in a loop),
|
|
@@ -411,10 +455,14 @@ export function wrapLoroMapProxy(
|
|
|
411
455
|
// which is what can page; false inside a record, whose field names are the
|
|
412
456
|
// app's own and constrained by nothing.
|
|
413
457
|
guardKeys = false,
|
|
458
|
+
// The root this map is, which decides whether it carries platform entries
|
|
459
|
+
// the draft hides and refuses (`isPagedMarkerEntry`).
|
|
460
|
+
rootKey: string | null = null,
|
|
414
461
|
): Record<string, unknown> {
|
|
462
|
+
const hidden = (key: string): boolean => isPagedMarkerEntry(rootKey, key);
|
|
415
463
|
const handler: ProxyHandler<object> = {
|
|
416
464
|
get(_t, key) {
|
|
417
|
-
if (typeof key === 'symbol') return undefined;
|
|
465
|
+
if (typeof key === 'symbol' || hidden(key)) return undefined;
|
|
418
466
|
if (divert?.holds(key)) {
|
|
419
467
|
const held = divert.peek(key);
|
|
420
468
|
if (held === null || typeof held !== 'object') return held;
|
|
@@ -452,7 +500,7 @@ export function wrapLoroMapProxy(
|
|
|
452
500
|
},
|
|
453
501
|
set(_t, key, value) {
|
|
454
502
|
if (typeof key === 'symbol') return false;
|
|
455
|
-
if (guardKeys) assertUsableRecordKey(key);
|
|
503
|
+
if (guardKeys) assertUsableRecordKey(key, rootKey);
|
|
456
504
|
// Before the write, so a commit it triggers seals the preceding
|
|
457
505
|
// batch rather than splitting this record out of it.
|
|
458
506
|
batcher?.record(value);
|
|
@@ -466,7 +514,7 @@ export function wrapLoroMapProxy(
|
|
|
466
514
|
},
|
|
467
515
|
deleteProperty(_t, key) {
|
|
468
516
|
if (typeof key === 'symbol') return false;
|
|
469
|
-
if (guardKeys) assertUsableRecordKey(key);
|
|
517
|
+
if (guardKeys) assertUsableRecordKey(key, rootKey);
|
|
470
518
|
batcher?.recordDelete(key);
|
|
471
519
|
divert?.drop(key);
|
|
472
520
|
loroMap.delete(key);
|
|
@@ -474,16 +522,17 @@ export function wrapLoroMapProxy(
|
|
|
474
522
|
return true;
|
|
475
523
|
},
|
|
476
524
|
has(_t, key) {
|
|
477
|
-
if (typeof key === 'symbol') return false;
|
|
525
|
+
if (typeof key === 'symbol' || hidden(key)) return false;
|
|
478
526
|
if (divert?.holds(key)) return true;
|
|
479
527
|
return loroMap.get(key) !== undefined;
|
|
480
528
|
},
|
|
481
529
|
ownKeys() {
|
|
482
|
-
|
|
483
|
-
|
|
530
|
+
const stored = loroMap.keys().filter((key) => !hidden(key));
|
|
531
|
+
if (!divert) return stored;
|
|
532
|
+
return [...new Set([...stored, ...divert.keys()])];
|
|
484
533
|
},
|
|
485
534
|
getOwnPropertyDescriptor(_t, key) {
|
|
486
|
-
if (typeof key === 'symbol') return undefined;
|
|
535
|
+
if (typeof key === 'symbol' || hidden(key)) return undefined;
|
|
487
536
|
if (divert?.holds(key)) {
|
|
488
537
|
return {
|
|
489
538
|
enumerable: true,
|
|
@@ -825,9 +874,6 @@ function wrapLoroListProxy(
|
|
|
825
874
|
// character.
|
|
826
875
|
type FlatRecord = unknown;
|
|
827
876
|
|
|
828
|
-
const TAG_FIELD = 'type';
|
|
829
|
-
const BODY_FIELD = 'params';
|
|
830
|
-
|
|
831
877
|
// The record keys one page packs, read off the live container without
|
|
832
878
|
// materialising its bodies. Two fields out of wasm rather than the whole
|
|
833
879
|
// container: a store's bodies are its bulk, and an index build asks every page
|
|
@@ -1391,11 +1437,14 @@ function buildPagedDeltaIndex(m: LoroMap): PagedDeltaIndex {
|
|
|
1391
1437
|
return { pages, shadowed };
|
|
1392
1438
|
}
|
|
1393
1439
|
|
|
1440
|
+
// Null when every key the batch touched is one `hidden` withholds, so a
|
|
1441
|
+
// subscriber is not handed an empty change for an edit it cannot see.
|
|
1394
1442
|
function unpagedMapDelta(
|
|
1395
1443
|
m: LoroMap,
|
|
1396
1444
|
depth: number,
|
|
1397
1445
|
batch: LoroEventBatch,
|
|
1398
|
-
|
|
1446
|
+
hidden: (key: string) => boolean,
|
|
1447
|
+
): Change<unknown> | null {
|
|
1399
1448
|
const setKeys = new Set<string>();
|
|
1400
1449
|
const deleted = new Set<string>();
|
|
1401
1450
|
for (const ev of batch.events) {
|
|
@@ -1414,9 +1463,12 @@ function unpagedMapDelta(
|
|
|
1414
1463
|
}
|
|
1415
1464
|
}
|
|
1416
1465
|
for (const key of deleted) setKeys.delete(key);
|
|
1466
|
+
const touched = setKeys.size + deleted.size;
|
|
1417
1467
|
const set = new Map<string, unknown>();
|
|
1418
|
-
for (const key of setKeys) set.set(key, plainEntry(m, key));
|
|
1419
|
-
|
|
1468
|
+
for (const key of setKeys) if (!hidden(key)) set.set(key, plainEntry(m, key));
|
|
1469
|
+
const removed = [...deleted].filter((key) => !hidden(key));
|
|
1470
|
+
if (touched > 0 && set.size === 0 && removed.length === 0) return null;
|
|
1471
|
+
return { set, deleted: removed };
|
|
1420
1472
|
}
|
|
1421
1473
|
|
|
1422
1474
|
/**
|
|
@@ -2641,24 +2693,21 @@ function trackEntryContainer<C extends LoroMap | LoroMovableList, V>(
|
|
|
2641
2693
|
// dedup, a different shape: it models no per-listener external resource, and
|
|
2642
2694
|
// the Loro subscription each record must re-create on rebind is exactly
|
|
2643
2695
|
// that.)
|
|
2644
|
-
/** Run
|
|
2645
|
-
|
|
2646
|
-
*
|
|
2647
|
-
*
|
|
2648
|
-
|
|
2649
|
-
function reportSubscriberThrow(err: unknown): void {
|
|
2650
|
-
console.error('[Brass] subscriber threw:', err);
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
function fireQuietly(cb: () => void): void {
|
|
2696
|
+
/** Run an app callback without letting a throw break the caller. One app
|
|
2697
|
+
* callback failing must not break a registration or stop the other
|
|
2698
|
+
* subscribers, so the throw goes to `report` (the owner's
|
|
2699
|
+
* `reportSubscriberThrow`) and the walk carries on. */
|
|
2700
|
+
function fireQuietly(cb: () => void, report: (err: unknown) => void): void {
|
|
2654
2701
|
try {
|
|
2655
2702
|
cb();
|
|
2656
2703
|
} catch (err) {
|
|
2657
|
-
|
|
2704
|
+
report(err);
|
|
2658
2705
|
}
|
|
2659
2706
|
}
|
|
2660
2707
|
|
|
2661
2708
|
class SubscriptionRegistry<Cb> {
|
|
2709
|
+
constructor(private readonly _report: (err: unknown) => void) {}
|
|
2710
|
+
|
|
2662
2711
|
private readonly _subs = new Set<{ cb: Cb; detach: () => void }>();
|
|
2663
2712
|
private readonly _settled = new Set<() => void>();
|
|
2664
2713
|
// Whether a whole-container delivery is in progress. Always false while a
|
|
@@ -2688,7 +2737,7 @@ class SubscriptionRegistry<Cb> {
|
|
|
2688
2737
|
*/
|
|
2689
2738
|
onSettled(cb: () => void): HandleUnsubscribe {
|
|
2690
2739
|
this._settled.add(cb);
|
|
2691
|
-
if (!this._delivering) fireQuietly(cb);
|
|
2740
|
+
if (!this._delivering) fireQuietly(cb, this._report);
|
|
2692
2741
|
return () => {
|
|
2693
2742
|
this._settled.delete(cb);
|
|
2694
2743
|
};
|
|
@@ -2699,7 +2748,7 @@ class SubscriptionRegistry<Cb> {
|
|
|
2699
2748
|
// otherwise remove it from the set mid-iteration, and an entry deleted
|
|
2700
2749
|
// before it is reached is never visited.
|
|
2701
2750
|
const listeners = Array.from(this._settled);
|
|
2702
|
-
for (const cb of listeners) fireQuietly(cb);
|
|
2751
|
+
for (const cb of listeners) fireQuietly(cb, this._report);
|
|
2703
2752
|
}
|
|
2704
2753
|
|
|
2705
2754
|
// Fire the whole-container catch-up once, attach the per-container change
|
|
@@ -2803,7 +2852,7 @@ export interface PagedRootHooks {
|
|
|
2803
2852
|
* Every field is declared with an initialiser, so they all exist by the time
|
|
2804
2853
|
* this runs and a later assignment keeps the descriptor this sets.
|
|
2805
2854
|
*/
|
|
2806
|
-
function hideOwnFields(handle: object): void {
|
|
2855
|
+
export function hideOwnFields(handle: object): void {
|
|
2807
2856
|
for (const key of Object.keys(handle)) {
|
|
2808
2857
|
Object.defineProperty(handle, key, { enumerable: false });
|
|
2809
2858
|
}
|
|
@@ -2829,7 +2878,7 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
2829
2878
|
// container.
|
|
2830
2879
|
private readonly _registry = new SubscriptionRegistry<
|
|
2831
2880
|
InternalMapSubscriber<T>
|
|
2832
|
-
>();
|
|
2881
|
+
>((err) => this._owner.reportSubscriberThrow(err));
|
|
2833
2882
|
// Keys captured just before a rebind (`_rebindAnchor`), consumed by the
|
|
2834
2883
|
// matching `_resync` to report the keys the source swap removed. Null
|
|
2835
2884
|
// outside a rebind cycle, so a bare `_resync` reports no removals.
|
|
@@ -2838,20 +2887,36 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
2838
2887
|
// taken before the container exists watches up this chain for the write
|
|
2839
2888
|
// that creates it (`_nearestLiveContainer`).
|
|
2840
2889
|
private readonly _parent: MapHandleImpl<unknown> | null;
|
|
2890
|
+
// The document root this handle is, or null for a composed handle. Decides
|
|
2891
|
+
// which keys are the platform's own entries rather than the app's
|
|
2892
|
+
// (`isPagedMarkerEntry`), which every read here hides.
|
|
2893
|
+
private readonly _rootKey: string | null;
|
|
2841
2894
|
|
|
2842
2895
|
constructor(
|
|
2843
2896
|
anchor: MapAnchor,
|
|
2844
2897
|
owner: HandleOwner,
|
|
2845
2898
|
paging: PagedRootHooks | null = null,
|
|
2846
2899
|
parent: MapHandleImpl<unknown> | null = null,
|
|
2900
|
+
rootKey: string | null = null,
|
|
2847
2901
|
) {
|
|
2848
2902
|
this._anchor = anchor;
|
|
2849
2903
|
this._owner = owner;
|
|
2850
2904
|
this._paging = paging;
|
|
2851
2905
|
this._parent = parent;
|
|
2906
|
+
this._rootKey = rootKey;
|
|
2852
2907
|
hideOwnFields(this);
|
|
2853
2908
|
}
|
|
2854
2909
|
|
|
2910
|
+
private _hidden(key: string): boolean {
|
|
2911
|
+
return isPagedMarkerEntry(this._rootKey, key);
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
// An unpaged map's JSON without the platform's own entries.
|
|
2915
|
+
private _visible(json: Record<string, T>): Record<string, T> {
|
|
2916
|
+
for (const key of Object.keys(json)) if (this._hidden(key)) delete json[key];
|
|
2917
|
+
return json;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2855
2920
|
// The container at the top of this handle's composition chain: the root it
|
|
2856
2921
|
// is composed off, which the document holds for its whole life (`getMap` is
|
|
2857
2922
|
// get-or-create, and a root anchor always resolves). A composed handle's
|
|
@@ -2891,6 +2956,17 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
2891
2956
|
return isPlainObject(v) ? v : undefined;
|
|
2892
2957
|
}
|
|
2893
2958
|
|
|
2959
|
+
/** Whether the plain body this handle is scoped to holds this entry itself.
|
|
2960
|
+
* The prototype chain is not part of a record, so a key an app never wrote
|
|
2961
|
+
* must not resolve through it. */
|
|
2962
|
+
private _plainHas(entryKey: string): boolean {
|
|
2963
|
+
const body = this._plainBody();
|
|
2964
|
+
return (
|
|
2965
|
+
body !== undefined &&
|
|
2966
|
+
Object.prototype.hasOwnProperty.call(body, entryKey)
|
|
2967
|
+
);
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2894
2970
|
read(): Record<string, T> {
|
|
2895
2971
|
this._owner.assertReadable();
|
|
2896
2972
|
const m = this._anchor.resolve();
|
|
@@ -2898,7 +2974,7 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
2898
2974
|
// A paged map flattens back to the flat lookup, so a read is identical
|
|
2899
2975
|
// paged or not.
|
|
2900
2976
|
if (this._isPaged()) return this._readPaged(m);
|
|
2901
|
-
return m.toJSON() as Record<string, T
|
|
2977
|
+
return this._visible(m.toJSON() as Record<string, T>);
|
|
2902
2978
|
}
|
|
2903
2979
|
|
|
2904
2980
|
// The whole-store read, which decodes every page. That walk answers the same
|
|
@@ -2916,8 +2992,17 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
2916
2992
|
|
|
2917
2993
|
get(entryKey: string): T | undefined {
|
|
2918
2994
|
this._owner.assertReadable();
|
|
2995
|
+
if (this._hidden(entryKey)) return undefined;
|
|
2919
2996
|
const m = this._anchor.resolve();
|
|
2920
|
-
if (!m)
|
|
2997
|
+
if (!m) {
|
|
2998
|
+
// One read of the body, not one per check: `_plainBody` resolves the
|
|
2999
|
+
// anchor and reaches into wasm, and this runs on every keyed read.
|
|
3000
|
+
const body = this._plainBody();
|
|
3001
|
+
return body !== undefined &&
|
|
3002
|
+
Object.prototype.hasOwnProperty.call(body, entryKey)
|
|
3003
|
+
? (body[entryKey] as T)
|
|
3004
|
+
: undefined;
|
|
3005
|
+
}
|
|
2921
3006
|
if (this._isPaged()) {
|
|
2922
3007
|
const pager = this._sessionPager(m);
|
|
2923
3008
|
return recordFromPage(
|
|
@@ -2935,6 +3020,14 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
2935
3020
|
return [...this.liveKeySet()];
|
|
2936
3021
|
}
|
|
2937
3022
|
|
|
3023
|
+
// Taken from one whole read: on a paged store that is the pass resolving
|
|
3024
|
+
// every record against the patches and markers that shadow it, which a walk
|
|
3025
|
+
// key by key would repeat per record.
|
|
3026
|
+
*entries(): IterableIterator<[string, T]> {
|
|
3027
|
+
const records = this.read();
|
|
3028
|
+
for (const key of Object.keys(records)) yield [key, records[key] as T];
|
|
3029
|
+
}
|
|
3030
|
+
|
|
2938
3031
|
/**
|
|
2939
3032
|
* The keys this map holds, as a set, without materialising a record.
|
|
2940
3033
|
*
|
|
@@ -2948,31 +3041,31 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
2948
3041
|
const m = this._anchor.resolve();
|
|
2949
3042
|
if (!m) return new Set(Object.keys(this._plainBody() ?? {}));
|
|
2950
3043
|
if (this._isPaged()) return this._sessionPager(m).ensureIndex(m).liveKeys();
|
|
2951
|
-
return new Set(m.keys());
|
|
3044
|
+
return new Set(m.keys().filter((key) => !this._hidden(key)));
|
|
2952
3045
|
}
|
|
2953
3046
|
|
|
2954
3047
|
has(entryKey: string): boolean {
|
|
2955
3048
|
this._owner.assertReadable();
|
|
3049
|
+
if (this._hidden(entryKey)) return false;
|
|
2956
3050
|
const m = this._anchor.resolve();
|
|
2957
|
-
if (!m)
|
|
2958
|
-
const body = this._plainBody();
|
|
2959
|
-
return body !== undefined && Object.prototype.hasOwnProperty.call(body, entryKey);
|
|
2960
|
-
}
|
|
3051
|
+
if (!m) return this._plainHas(entryKey);
|
|
2961
3052
|
if (this._isPaged()) return this.get(entryKey) !== undefined;
|
|
2962
3053
|
return entryExists(m, entryKey);
|
|
2963
3054
|
}
|
|
2964
3055
|
|
|
2965
|
-
mutate(fn: (draft: Record<string, T>) => void): void {
|
|
3056
|
+
mutate(fn: (draft: Record<string, T>) => void, options?: MutateOptions): void {
|
|
2966
3057
|
this._owner.assertWritable();
|
|
2967
3058
|
this._owner.ensureLocalUpdatesSubscribed();
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
3059
|
+
this._owner.runMutate(options, () => {
|
|
3060
|
+
// Commits as it goes past a size budget; see `mutate-batching.ts`.
|
|
3061
|
+
this._runMutate(
|
|
3062
|
+
fn,
|
|
3063
|
+
new MutateBatcher(() => {
|
|
3064
|
+
this._owner.commitMutate();
|
|
3065
|
+
}),
|
|
3066
|
+
);
|
|
3067
|
+
this._owner.commitMutate();
|
|
3068
|
+
});
|
|
2976
3069
|
}
|
|
2977
3070
|
|
|
2978
3071
|
// Run a mutate WITHOUT the commit pipeline (the caller owns
|
|
@@ -2996,6 +3089,7 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
2996
3089
|
batcher,
|
|
2997
3090
|
divert?.hooks,
|
|
2998
3091
|
this._paging !== null,
|
|
3092
|
+
this._rootKey,
|
|
2999
3093
|
) as Record<string, T>;
|
|
3000
3094
|
fn(proxy);
|
|
3001
3095
|
divert?.finish();
|
|
@@ -3313,7 +3407,7 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
3313
3407
|
}
|
|
3314
3408
|
// One walk of an unpaged map, whose size the paging threshold bounds, and
|
|
3315
3409
|
// no wasm handle minted per key the way a per-key read would.
|
|
3316
|
-
const keys = new Set(m.keys());
|
|
3410
|
+
const keys = new Set(m.keys().filter((key) => !this._hidden(key)));
|
|
3317
3411
|
return (key) => keys.has(key);
|
|
3318
3412
|
}
|
|
3319
3413
|
|
|
@@ -3347,7 +3441,7 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
3347
3441
|
true,
|
|
3348
3442
|
);
|
|
3349
3443
|
} catch (err) {
|
|
3350
|
-
reportSubscriberThrow(err);
|
|
3444
|
+
this._owner.reportSubscriberThrow(err);
|
|
3351
3445
|
}
|
|
3352
3446
|
}
|
|
3353
3447
|
|
|
@@ -3425,13 +3519,14 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
3425
3519
|
const change = (
|
|
3426
3520
|
index
|
|
3427
3521
|
? pagedMapDelta(m, event, index, (key) => this.get(key))
|
|
3428
|
-
: unpagedMapDelta(m, depth, event)
|
|
3429
|
-
) as Change<T
|
|
3522
|
+
: unpagedMapDelta(m, depth, event, (key) => this._hidden(key))
|
|
3523
|
+
) as Change<T> | null;
|
|
3524
|
+
if (change === null) return;
|
|
3430
3525
|
const origin: ChangeOrigin = { source: this._owner.originOf(event) };
|
|
3431
3526
|
try {
|
|
3432
3527
|
cb(change, origin, false);
|
|
3433
3528
|
} catch (err) {
|
|
3434
|
-
reportSubscriberThrow(err);
|
|
3529
|
+
this._owner.reportSubscriberThrow(err);
|
|
3435
3530
|
}
|
|
3436
3531
|
});
|
|
3437
3532
|
return () => sub();
|
|
@@ -3591,7 +3686,7 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
3591
3686
|
// so a per-container subscription keeps a stable instance across reads.
|
|
3592
3687
|
const direct = childAnchor(this._anchor, entryKey, ctor);
|
|
3593
3688
|
if (this._paging === null) return direct;
|
|
3594
|
-
assertUsableRecordKey(entryKey);
|
|
3689
|
+
assertUsableRecordKey(entryKey, this._rootKey);
|
|
3595
3690
|
const overlay = childAnchor(this._anchor, overlayKeyFor(entryKey), ctor);
|
|
3596
3691
|
|
|
3597
3692
|
// Whether an edit written beside the pages is waiting to be folded over
|
|
@@ -3694,6 +3789,23 @@ class MapHandleImpl<T> implements MapHandle<T> {
|
|
|
3694
3789
|
* carry the position each edit lands at, so an app applies them in order and
|
|
3695
3790
|
* is done; nothing about the cursor reaches it.
|
|
3696
3791
|
*/
|
|
3792
|
+
/** Whether position `at`, read after the first `count` splices, is one of the
|
|
3793
|
+
* elements those splices inserted. */
|
|
3794
|
+
function insertedBySplices(
|
|
3795
|
+
splices: readonly Splice<unknown>[],
|
|
3796
|
+
count: number,
|
|
3797
|
+
at: number,
|
|
3798
|
+
): boolean {
|
|
3799
|
+
let pos = at;
|
|
3800
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
3801
|
+
const { at: start, removed, inserted } = splices[i]!;
|
|
3802
|
+
if (pos < start) continue;
|
|
3803
|
+
if (pos < start + inserted.length) return true;
|
|
3804
|
+
pos += removed - inserted.length;
|
|
3805
|
+
}
|
|
3806
|
+
return false;
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3697
3809
|
function splicesFromListDiff(diff: readonly Delta<unknown[]>[]): Splice<unknown>[] {
|
|
3698
3810
|
const out: Splice<unknown>[] = [];
|
|
3699
3811
|
let at = 0;
|
|
@@ -3708,7 +3820,7 @@ function splicesFromListDiff(diff: readonly Delta<unknown[]>[]): Splice<unknown>
|
|
|
3708
3820
|
}
|
|
3709
3821
|
if (part.insert !== undefined) {
|
|
3710
3822
|
const inserted = part.insert.map(plainDiffItem);
|
|
3711
|
-
out
|
|
3823
|
+
pushBoundedSplice(out, { at, removed: 0, inserted });
|
|
3712
3824
|
at += inserted.length;
|
|
3713
3825
|
}
|
|
3714
3826
|
}
|
|
@@ -3742,8 +3854,9 @@ class ListHandleImpl<T> implements ListHandle<T> {
|
|
|
3742
3854
|
private readonly _owner: HandleOwner;
|
|
3743
3855
|
// Active subscriptions, tracked so a version checkout can re-fire and
|
|
3744
3856
|
// re-attach each one against the new container.
|
|
3745
|
-
private readonly _registry =
|
|
3746
|
-
|
|
3857
|
+
private readonly _registry = new SubscriptionRegistry<InternalListSubscriber>(
|
|
3858
|
+
(err) => this._owner.reportSubscriberThrow(err),
|
|
3859
|
+
);
|
|
3747
3860
|
// See `MapHandleImpl._parent`.
|
|
3748
3861
|
private readonly _parent: MapHandleImpl<unknown> | null;
|
|
3749
3862
|
|
|
@@ -3778,17 +3891,39 @@ class ListHandleImpl<T> implements ListHandle<T> {
|
|
|
3778
3891
|
return l ? l.length : (this._plainBody()?.length ?? 0);
|
|
3779
3892
|
}
|
|
3780
3893
|
|
|
3781
|
-
|
|
3894
|
+
get(index: number): T | undefined {
|
|
3895
|
+
this._owner.assertReadable();
|
|
3896
|
+
if (!Number.isInteger(index) || index < 0) return undefined;
|
|
3897
|
+
const l = this._anchor.resolve();
|
|
3898
|
+
if (!l) return this._plainBody()?.[index] as T | undefined;
|
|
3899
|
+
if (index >= l.length) return undefined;
|
|
3900
|
+
const v: unknown = l.get(index);
|
|
3901
|
+
if (!isContainer(v)) return v as T;
|
|
3902
|
+
const container = v as { toJSON(): unknown; free(): void };
|
|
3903
|
+
try {
|
|
3904
|
+
return container.toJSON() as T;
|
|
3905
|
+
} finally {
|
|
3906
|
+
container.free();
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
*values(): IterableIterator<T> {
|
|
3911
|
+
yield* this.read();
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
mutate(fn: (draft: ListDraft<T>) => void, options?: MutateOptions): void {
|
|
3782
3915
|
this._owner.assertWritable();
|
|
3783
3916
|
this._owner.ensureLocalUpdatesSubscribed();
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3917
|
+
this._owner.runMutate(options, () => {
|
|
3918
|
+
// Commits as it goes past a size budget; see `mutate-batching.ts`.
|
|
3919
|
+
this._runMutate(
|
|
3920
|
+
fn,
|
|
3921
|
+
new MutateBatcher(() => {
|
|
3922
|
+
this._owner.commitMutate();
|
|
3923
|
+
}),
|
|
3924
|
+
);
|
|
3925
|
+
this._owner.commitMutate();
|
|
3926
|
+
});
|
|
3792
3927
|
}
|
|
3793
3928
|
|
|
3794
3929
|
// Run a list mutate without the commit pipeline; see
|
|
@@ -3803,7 +3938,7 @@ class ListHandleImpl<T> implements ListHandle<T> {
|
|
|
3803
3938
|
fn(proxy);
|
|
3804
3939
|
}
|
|
3805
3940
|
|
|
3806
|
-
// Fire the whole list as
|
|
3941
|
+
// Fire the whole list as one change replacing everything the subscriber was
|
|
3807
3942
|
// last told. Used for registration and for a checkout / return resync, which
|
|
3808
3943
|
// describe no authored transition and so carry no origin.
|
|
3809
3944
|
//
|
|
@@ -3817,9 +3952,11 @@ class ListHandleImpl<T> implements ListHandle<T> {
|
|
|
3817
3952
|
const held = this._held.get(cb) ?? 0;
|
|
3818
3953
|
this._held.set(cb, inserted.length);
|
|
3819
3954
|
try {
|
|
3820
|
-
|
|
3955
|
+
const splices: Splice<unknown>[] = [];
|
|
3956
|
+
pushBoundedSplice(splices, { at: 0, removed: held, inserted });
|
|
3957
|
+
cb({ splices });
|
|
3821
3958
|
} catch (err) {
|
|
3822
|
-
reportSubscriberThrow(err);
|
|
3959
|
+
this._owner.reportSubscriberThrow(err);
|
|
3823
3960
|
}
|
|
3824
3961
|
}
|
|
3825
3962
|
|
|
@@ -3848,19 +3985,35 @@ class ListHandleImpl<T> implements ListHandle<T> {
|
|
|
3848
3985
|
l: LoroMovableList,
|
|
3849
3986
|
cb: InternalListSubscriber,
|
|
3850
3987
|
): () => void {
|
|
3988
|
+
const depth = containerDepth(l);
|
|
3851
3989
|
const sub = l.subscribe((event) => {
|
|
3852
|
-
const
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3990
|
+
const splices: Splice<unknown>[] = [];
|
|
3991
|
+
// An element held as a child container changes inside that container,
|
|
3992
|
+
// which the list's own diff does not name. Each such element is
|
|
3993
|
+
// reported as a replace of its position, read after the list's own
|
|
3994
|
+
// splices so the index is the one the subscriber holds by then. An
|
|
3995
|
+
// element those splices inserted already arrived as it now stands, so
|
|
3996
|
+
// its own fields being written in the same commit adds no replace.
|
|
3997
|
+
const touched = new Set<number>();
|
|
3998
|
+
for (const e of event.events) {
|
|
3999
|
+
if (e.path.length === depth && e.diff.type === 'list') {
|
|
4000
|
+
splices.push(...splicesFromListDiff(e.diff.diff as readonly Delta<unknown[]>[]));
|
|
4001
|
+
} else if (e.path.length > depth && typeof e.path[depth] === 'number') {
|
|
4002
|
+
touched.add(e.path[depth]);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
const listSplices = splices.length;
|
|
4006
|
+
for (const at of [...touched].sort((x, y) => x - y)) {
|
|
4007
|
+
if (at >= l.length || insertedBySplices(splices, listSplices, at)) continue;
|
|
4008
|
+
splices.push({ at, removed: 1, inserted: [plainListItem(l, at)] });
|
|
4009
|
+
}
|
|
3857
4010
|
let held = this._held.get(cb) ?? 0;
|
|
3858
4011
|
for (const splice of splices) held += splice.inserted.length - splice.removed;
|
|
3859
4012
|
this._held.set(cb, held);
|
|
3860
4013
|
try {
|
|
3861
4014
|
cb({ splices }, { source: this._owner.originOf(event) });
|
|
3862
4015
|
} catch (err) {
|
|
3863
|
-
reportSubscriberThrow(err);
|
|
4016
|
+
this._owner.reportSubscriberThrow(err);
|
|
3864
4017
|
}
|
|
3865
4018
|
});
|
|
3866
4019
|
return () => sub();
|
|
@@ -4014,6 +4167,10 @@ class OrderedMapHandleImpl<T> implements OrderedMapHandle<T> {
|
|
|
4014
4167
|
return sequenceOf(this._map.liveKeySet(), this._order.read());
|
|
4015
4168
|
}
|
|
4016
4169
|
|
|
4170
|
+
*entries(): IterableIterator<[string, T]> {
|
|
4171
|
+
for (const { key, value } of this.ordered()) yield [key, value];
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4017
4174
|
ordered(): { key: string; value: T }[] {
|
|
4018
4175
|
const records = this._map.read();
|
|
4019
4176
|
const seen = new Set<string>();
|
|
@@ -4150,9 +4307,15 @@ class OrderedMapHandleImpl<T> implements OrderedMapHandle<T> {
|
|
|
4150
4307
|
return true;
|
|
4151
4308
|
}
|
|
4152
4309
|
|
|
4153
|
-
mutate(fn: (draft: Record<string, T>) => void): void {
|
|
4310
|
+
mutate(fn: (draft: Record<string, T>) => void, options?: MutateOptions): void {
|
|
4154
4311
|
this._owner.assertWritable();
|
|
4155
4312
|
this._owner.ensureLocalUpdatesSubscribed();
|
|
4313
|
+
this._owner.runMutate(options, () => {
|
|
4314
|
+
this._mutateOrdered(fn);
|
|
4315
|
+
});
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
private _mutateOrdered(fn: (draft: Record<string, T>) => void): void {
|
|
4156
4319
|
|
|
4157
4320
|
// Net structural effect: keys added (`added` iterates in insertion
|
|
4158
4321
|
// order, and a delete-then-re-set re-appends, matching the order list's
|
|
@@ -4363,8 +4526,15 @@ export function createRootMapHandle<T>(
|
|
|
4363
4526
|
root: LoroMap,
|
|
4364
4527
|
owner: HandleOwner,
|
|
4365
4528
|
paging?: PagedRootHooks,
|
|
4529
|
+
rootKey?: string,
|
|
4366
4530
|
): MapHandle<T> {
|
|
4367
|
-
return new MapHandleImpl<T>(
|
|
4531
|
+
return new MapHandleImpl<T>(
|
|
4532
|
+
rootAnchor(root),
|
|
4533
|
+
owner,
|
|
4534
|
+
paging ?? null,
|
|
4535
|
+
null,
|
|
4536
|
+
rootKey ?? null,
|
|
4537
|
+
);
|
|
4368
4538
|
}
|
|
4369
4539
|
|
|
4370
4540
|
export function createRootListHandle<T>(
|