@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.
Files changed (327) hide show
  1. package/AGENTS.md +142 -41
  2. package/CHANGELOG.md +93 -4
  3. package/README.md +666 -179
  4. package/dist/acting-organization.d.ts.map +1 -1
  5. package/dist/acting-organization.js +6 -11
  6. package/dist/acting-organization.js.map +1 -1
  7. package/dist/api-fetch.d.ts +8 -0
  8. package/dist/api-fetch.d.ts.map +1 -1
  9. package/dist/api-fetch.js +35 -8
  10. package/dist/api-fetch.js.map +1 -1
  11. package/dist/batched-get.d.ts +11 -0
  12. package/dist/batched-get.d.ts.map +1 -0
  13. package/dist/batched-get.js +92 -0
  14. package/dist/batched-get.js.map +1 -0
  15. package/dist/call-service.d.ts +4 -0
  16. package/dist/call-service.d.ts.map +1 -1
  17. package/dist/call-service.js +66 -3
  18. package/dist/call-service.js.map +1 -1
  19. package/dist/client.d.ts +51 -2
  20. package/dist/client.d.ts.map +1 -1
  21. package/dist/client.js +982 -124
  22. package/dist/client.js.map +1 -1
  23. package/dist/document-checkpoints.d.ts +25 -2
  24. package/dist/document-checkpoints.d.ts.map +1 -1
  25. package/dist/document-checkpoints.js +258 -61
  26. package/dist/document-checkpoints.js.map +1 -1
  27. package/dist/document-commands.d.ts.map +1 -1
  28. package/dist/document-commands.js +5 -3
  29. package/dist/document-commands.js.map +1 -1
  30. package/dist/document-contents.d.ts +10 -0
  31. package/dist/document-contents.d.ts.map +1 -1
  32. package/dist/document-contents.js +23 -1
  33. package/dist/document-contents.js.map +1 -1
  34. package/dist/document-set-subscription.d.ts +56 -0
  35. package/dist/document-set-subscription.d.ts.map +1 -0
  36. package/dist/document-set-subscription.js +240 -0
  37. package/dist/document-set-subscription.js.map +1 -0
  38. package/dist/document-signals.d.ts +6 -0
  39. package/dist/document-signals.d.ts.map +1 -1
  40. package/dist/document-signals.js +31 -1
  41. package/dist/document-signals.js.map +1 -1
  42. package/dist/document-streams.d.ts +36 -2
  43. package/dist/document-streams.d.ts.map +1 -1
  44. package/dist/document-streams.js +69 -18
  45. package/dist/document-streams.js.map +1 -1
  46. package/dist/document-thumbnail-capture.d.ts +12 -2
  47. package/dist/document-thumbnail-capture.d.ts.map +1 -1
  48. package/dist/document-thumbnail-capture.js +103 -27
  49. package/dist/document-thumbnail-capture.js.map +1 -1
  50. package/dist/documents-namespace.d.ts +92 -10
  51. package/dist/documents-namespace.d.ts.map +1 -1
  52. package/dist/documents-namespace.js +285 -39
  53. package/dist/documents-namespace.js.map +1 -1
  54. package/dist/documents.d.ts +173 -8
  55. package/dist/documents.d.ts.map +1 -1
  56. package/dist/documents.js +1778 -310
  57. package/dist/documents.js.map +1 -1
  58. package/dist/errors.d.ts +3 -3
  59. package/dist/errors.d.ts.map +1 -1
  60. package/dist/errors.js.map +1 -1
  61. package/dist/fetch-retry.d.ts +1 -0
  62. package/dist/fetch-retry.d.ts.map +1 -1
  63. package/dist/fetch-retry.js +2 -0
  64. package/dist/fetch-retry.js.map +1 -1
  65. package/dist/file-choice.d.ts +2 -0
  66. package/dist/file-choice.d.ts.map +1 -0
  67. package/dist/file-choice.js +27 -0
  68. package/dist/file-choice.js.map +1 -0
  69. package/dist/guest-link.d.ts +18 -1
  70. package/dist/guest-link.d.ts.map +1 -1
  71. package/dist/guest-link.js +329 -1
  72. package/dist/guest-link.js.map +1 -1
  73. package/dist/handle-types.d.ts +67 -5
  74. package/dist/handle-types.d.ts.map +1 -1
  75. package/dist/handles.d.ts +15 -19
  76. package/dist/handles.d.ts.map +1 -1
  77. package/dist/handles.js +231 -76
  78. package/dist/handles.js.map +1 -1
  79. package/dist/hosted-ui.d.ts +61 -0
  80. package/dist/hosted-ui.d.ts.map +1 -1
  81. package/dist/hosted-ui.js +94 -42
  82. package/dist/hosted-ui.js.map +1 -1
  83. package/dist/import-contract.d.ts +74 -0
  84. package/dist/import-contract.d.ts.map +1 -0
  85. package/dist/import-contract.js +163 -0
  86. package/dist/import-contract.js.map +1 -0
  87. package/dist/import-guest.d.ts +9 -0
  88. package/dist/import-guest.d.ts.map +1 -0
  89. package/dist/import-guest.js +155 -0
  90. package/dist/import-guest.js.map +1 -0
  91. package/dist/import-report.d.ts +17 -0
  92. package/dist/import-report.d.ts.map +1 -0
  93. package/dist/import-report.js +7 -0
  94. package/dist/import-report.js.map +1 -0
  95. package/dist/importer-wait.d.ts +2 -0
  96. package/dist/importer-wait.d.ts.map +1 -0
  97. package/dist/importer-wait.js +5 -0
  98. package/dist/importer-wait.js.map +1 -0
  99. package/dist/importer.d.ts +18 -4
  100. package/dist/importer.d.ts.map +1 -1
  101. package/dist/importer.js +322 -105
  102. package/dist/importer.js.map +1 -1
  103. package/dist/in-development.d.ts +2 -0
  104. package/dist/in-development.d.ts.map +1 -0
  105. package/dist/in-development.js +10 -0
  106. package/dist/in-development.js.map +1 -0
  107. package/dist/index.d.ts +16 -8
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +24 -8
  110. package/dist/index.js.map +1 -1
  111. package/dist/init-keys.d.ts +1 -1
  112. package/dist/init-keys.d.ts.map +1 -1
  113. package/dist/init-keys.js +6 -1
  114. package/dist/init-keys.js.map +1 -1
  115. package/dist/input-documents.d.ts +67 -0
  116. package/dist/input-documents.d.ts.map +1 -0
  117. package/dist/input-documents.js +693 -0
  118. package/dist/input-documents.js.map +1 -0
  119. package/dist/inputs-status.d.ts +80 -0
  120. package/dist/inputs-status.d.ts.map +1 -0
  121. package/dist/inputs-status.js +460 -0
  122. package/dist/inputs-status.js.map +1 -0
  123. package/dist/inputs.d.ts +2 -0
  124. package/dist/inputs.d.ts.map +1 -0
  125. package/dist/inputs.js +793 -0
  126. package/dist/inputs.js.map +1 -0
  127. package/dist/internal.d.ts +6 -1
  128. package/dist/internal.d.ts.map +1 -1
  129. package/dist/internal.js +4 -1
  130. package/dist/internal.js.map +1 -1
  131. package/dist/join-content.d.ts +33 -0
  132. package/dist/join-content.d.ts.map +1 -0
  133. package/dist/join-content.js +114 -0
  134. package/dist/join-content.js.map +1 -0
  135. package/dist/join-stamp.d.ts +3 -0
  136. package/dist/join-stamp.d.ts.map +1 -0
  137. package/dist/join-stamp.js +12 -0
  138. package/dist/join-stamp.js.map +1 -0
  139. package/dist/manifest-drift.d.ts +9 -0
  140. package/dist/manifest-drift.d.ts.map +1 -0
  141. package/dist/manifest-drift.js +46 -0
  142. package/dist/manifest-drift.js.map +1 -0
  143. package/dist/open-document.d.ts +109 -2
  144. package/dist/open-document.d.ts.map +1 -1
  145. package/dist/open-document.js +1822 -811
  146. package/dist/open-document.js.map +1 -1
  147. package/dist/page-restore.d.ts +12 -0
  148. package/dist/page-restore.d.ts.map +1 -0
  149. package/dist/page-restore.js +23 -0
  150. package/dist/page-restore.js.map +1 -0
  151. package/dist/placement.d.ts +13 -0
  152. package/dist/placement.d.ts.map +1 -0
  153. package/dist/placement.js +34 -0
  154. package/dist/placement.js.map +1 -0
  155. package/dist/presence.d.ts +1 -0
  156. package/dist/presence.d.ts.map +1 -1
  157. package/dist/presence.js +16 -4
  158. package/dist/presence.js.map +1 -1
  159. package/dist/read-only-document.d.ts +87 -0
  160. package/dist/read-only-document.d.ts.map +1 -0
  161. package/dist/read-only-document.js +4 -0
  162. package/dist/read-only-document.js.map +1 -0
  163. package/dist/record-paging.d.ts +63 -6
  164. package/dist/record-paging.d.ts.map +1 -1
  165. package/dist/record-paging.js +122 -29
  166. package/dist/record-paging.js.map +1 -1
  167. package/dist/references.d.ts +40 -0
  168. package/dist/references.d.ts.map +1 -0
  169. package/dist/references.js +135 -0
  170. package/dist/references.js.map +1 -0
  171. package/dist/request-lane.d.ts +5 -0
  172. package/dist/request-lane.d.ts.map +1 -0
  173. package/dist/request-lane.js +48 -0
  174. package/dist/request-lane.js.map +1 -0
  175. package/dist/sealed-arrow.d.ts +170 -0
  176. package/dist/sealed-arrow.d.ts.map +1 -0
  177. package/dist/sealed-arrow.js +769 -0
  178. package/dist/sealed-arrow.js.map +1 -0
  179. package/dist/sealed-cache.d.ts +21 -0
  180. package/dist/sealed-cache.d.ts.map +1 -0
  181. package/dist/sealed-cache.js +247 -0
  182. package/dist/sealed-cache.js.map +1 -0
  183. package/dist/sealed-encode.d.ts +87 -0
  184. package/dist/sealed-encode.d.ts.map +1 -0
  185. package/dist/sealed-encode.js +876 -0
  186. package/dist/sealed-encode.js.map +1 -0
  187. package/dist/sealed-handles.d.ts +45 -0
  188. package/dist/sealed-handles.d.ts.map +1 -0
  189. package/dist/sealed-handles.js +408 -0
  190. package/dist/sealed-handles.js.map +1 -0
  191. package/dist/sealed-store.d.ts +242 -0
  192. package/dist/sealed-store.d.ts.map +1 -0
  193. package/dist/sealed-store.js +1155 -0
  194. package/dist/sealed-store.js.map +1 -0
  195. package/dist/serializer.d.ts +6 -15
  196. package/dist/serializer.d.ts.map +1 -1
  197. package/dist/session-toolbar.d.ts +20 -5
  198. package/dist/session-toolbar.d.ts.map +1 -1
  199. package/dist/session-toolbar.js +419 -67
  200. package/dist/session-toolbar.js.map +1 -1
  201. package/dist/session.d.ts +38 -1
  202. package/dist/session.d.ts.map +1 -1
  203. package/dist/session.js +562 -79
  204. package/dist/session.js.map +1 -1
  205. package/dist/sha256.d.ts +3 -0
  206. package/dist/sha256.d.ts.map +1 -0
  207. package/dist/sha256.js +74 -0
  208. package/dist/sha256.js.map +1 -0
  209. package/dist/stable-handles.d.ts +19 -0
  210. package/dist/stable-handles.d.ts.map +1 -0
  211. package/dist/stable-handles.js +193 -0
  212. package/dist/stable-handles.js.map +1 -0
  213. package/dist/staged-seal.d.ts +2 -0
  214. package/dist/staged-seal.d.ts.map +1 -0
  215. package/dist/staged-seal.js +76 -0
  216. package/dist/staged-seal.js.map +1 -0
  217. package/dist/subscriber-delivery.d.ts +8 -0
  218. package/dist/subscriber-delivery.d.ts.map +1 -0
  219. package/dist/subscriber-delivery.js +31 -0
  220. package/dist/subscriber-delivery.js.map +1 -0
  221. package/dist/tail-op-import.d.ts +10 -0
  222. package/dist/tail-op-import.d.ts.map +1 -0
  223. package/dist/tail-op-import.js +33 -0
  224. package/dist/tail-op-import.js.map +1 -0
  225. package/dist/topic-lifecycle.d.ts +6 -0
  226. package/dist/topic-lifecycle.d.ts.map +1 -1
  227. package/dist/topic-lifecycle.js +32 -2
  228. package/dist/topic-lifecycle.js.map +1 -1
  229. package/dist/trial-replace-dialog.d.ts +10 -0
  230. package/dist/trial-replace-dialog.d.ts.map +1 -0
  231. package/dist/trial-replace-dialog.js +153 -0
  232. package/dist/trial-replace-dialog.js.map +1 -0
  233. package/dist/types.d.ts +8 -1
  234. package/dist/types.d.ts.map +1 -1
  235. package/dist/undo.d.ts +60 -5
  236. package/dist/undo.d.ts.map +1 -1
  237. package/dist/undo.js +344 -75
  238. package/dist/undo.js.map +1 -1
  239. package/dist/version-bar.d.ts +1 -0
  240. package/dist/version-bar.d.ts.map +1 -1
  241. package/dist/version-bar.js +2 -1
  242. package/dist/version-bar.js.map +1 -1
  243. package/dist/version.d.ts +2 -2
  244. package/dist/version.js +1 -1
  245. package/dist/ws-client.d.ts +21 -0
  246. package/dist/ws-client.d.ts.map +1 -1
  247. package/dist/ws-client.js +192 -8
  248. package/dist/ws-client.js.map +1 -1
  249. package/dist/ws-frame-limit.d.ts +1 -0
  250. package/dist/ws-frame-limit.d.ts.map +1 -1
  251. package/dist/ws-frame-limit.js.map +1 -1
  252. package/docs/app-manifest.md +121 -17
  253. package/docs/custom-chrome.md +24 -16
  254. package/docs/hosted-ui-advanced.md +38 -6
  255. package/docs/imported-documents.md +104 -51
  256. package/docs/presence.md +16 -4
  257. package/docs/schemas.md +8 -5
  258. package/docs/services.md +6 -6
  259. package/docs/streams.md +139 -150
  260. package/docs/text-fields.md +26 -19
  261. package/package.json +5 -2
  262. package/src/acting-organization.ts +6 -12
  263. package/src/api-fetch.ts +53 -3
  264. package/src/auth-flow.ts +1 -1
  265. package/src/batched-get.ts +117 -0
  266. package/src/call-service.ts +78 -3
  267. package/src/client.ts +1092 -121
  268. package/src/document-checkpoints.ts +307 -65
  269. package/src/document-commands.ts +5 -5
  270. package/src/document-contents.ts +27 -1
  271. package/src/document-set-subscription.ts +274 -0
  272. package/src/document-signals.ts +47 -5
  273. package/src/document-streams.ts +85 -16
  274. package/src/document-thumbnail-capture.ts +118 -32
  275. package/src/documents-namespace.ts +539 -84
  276. package/src/documents.ts +2017 -348
  277. package/src/errors.ts +17 -6
  278. package/src/fetch-retry.ts +5 -0
  279. package/src/file-choice.ts +26 -0
  280. package/src/guest-link.ts +362 -1
  281. package/src/handle-types.ts +73 -47
  282. package/src/handles.ts +253 -83
  283. package/src/hosted-ui.ts +211 -50
  284. package/src/import-contract.ts +227 -0
  285. package/src/import-guest.ts +193 -0
  286. package/src/import-report.ts +24 -0
  287. package/src/importer-wait.ts +4 -0
  288. package/src/importer.ts +386 -131
  289. package/src/in-development.ts +10 -0
  290. package/src/index.ts +76 -16
  291. package/src/init-keys.ts +6 -1
  292. package/src/input-documents.ts +819 -0
  293. package/src/inputs-status.ts +598 -0
  294. package/src/inputs.ts +1090 -0
  295. package/src/internal.ts +7 -0
  296. package/src/join-content.ts +135 -0
  297. package/src/join-stamp.ts +12 -0
  298. package/src/manifest-drift.ts +54 -0
  299. package/src/open-document.ts +2346 -930
  300. package/src/page-restore.ts +38 -0
  301. package/src/placement.ts +54 -0
  302. package/src/presence.ts +21 -5
  303. package/src/read-only-document.ts +133 -0
  304. package/src/record-paging.ts +147 -25
  305. package/src/references.ts +207 -0
  306. package/src/request-lane.ts +59 -0
  307. package/src/sealed-arrow.ts +843 -0
  308. package/src/sealed-cache.ts +267 -0
  309. package/src/sealed-encode.ts +1015 -0
  310. package/src/sealed-handles.ts +486 -0
  311. package/src/sealed-store.ts +1330 -0
  312. package/src/serializer.ts +8 -20
  313. package/src/session-toolbar.ts +463 -72
  314. package/src/session.ts +740 -99
  315. package/src/sha256.ts +65 -0
  316. package/src/stable-handles.ts +235 -0
  317. package/src/staged-seal.ts +90 -0
  318. package/src/subscriber-delivery.ts +37 -0
  319. package/src/tail-op-import.ts +30 -0
  320. package/src/topic-lifecycle.ts +38 -2
  321. package/src/trial-replace-dialog.ts +163 -0
  322. package/src/types.ts +6 -6
  323. package/src/undo.ts +386 -73
  324. package/src/version-bar.ts +5 -1
  325. package/src/version.ts +1 -1
  326. package/src/ws-client.ts +208 -16
  327. 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
- for (let i = 0; i < value.length; i++) {
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 (value.length < oldLen) {
241
- target.delete(value.length, oldLen - value.length);
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 = target.setContainer(idx, new LoroMap());
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 = target.setContainer(idx, new LoroMovableList());
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
- export function wrapLoroMapProxy(
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
- if (!divert) return loroMap.keys();
483
- return [...new Set([...loroMap.keys(), ...divert.keys()])];
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
- ): Change<unknown> {
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
- return { set, deleted: [...deleted] };
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 a callback the SDK owns, without letting it break the caller. */
2645
- /** Report a subscriber that threw, then carry on. One app callback failing
2646
- * must not break a registration or stop the other subscribers, but swallowing
2647
- * it leaves an app that stalled inside its own callback with nothing in the
2648
- * console to read. */
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
- reportSubscriberThrow(err);
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) return this._plainBody()?.[entryKey] as T | undefined;
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
- // Commits as it goes past a size budget; see `mutate-batching.ts`.
2969
- this._runMutate(
2970
- fn,
2971
- new MutateBatcher(() => {
2972
- this._owner.commitMutate();
2973
- }),
2974
- );
2975
- this._owner.commitMutate();
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.push({ at, removed: 0, inserted });
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
- new SubscriptionRegistry<InternalListSubscriber>();
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
- mutate(fn: (draft: ListDraft<T>) => void): void {
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
- // Commits as it goes past a size budget; see `mutate-batching.ts`.
3785
- this._runMutate(
3786
- fn,
3787
- new MutateBatcher(() => {
3788
- this._owner.commitMutate();
3789
- }),
3790
- );
3791
- this._owner.commitMutate();
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 ONE splice replacing everything the subscriber was
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
- cb({ splices: [{ at: 0, removed: held, inserted }] });
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 diff =
3853
- event.events[0]?.diff.type === 'list'
3854
- ? (event.events[0].diff.diff as readonly Delta<unknown[]>[])
3855
- : [];
3856
- const splices = splicesFromListDiff(diff);
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>(rootAnchor(root), owner, paging ?? null);
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>(