@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/dist/handles.js CHANGED
@@ -5,8 +5,9 @@
5
5
  // Engine-neutral: Loro events are translated to a minimal `Change` shape at
6
6
  // the handle boundary, so handle-only app code does not pin the SDK to Loro.
7
7
  import { LoroMovableList, LoroMap, isContainer, } from './wasm-runtime.js';
8
- import { PAGE_TAG, UNPAGED_MAX_RECORDS, PAGE_KEYS_FIELD, assertUsableRecordKey, classifyStoreKey, decodePageKeys, decodePageRecords, decodeRecordValue, encodePage, flattenPagedLookup, isReservedKey, overlayKeyFor, packWholeValue, parseSessionPageKey, repackRecord, parsePatchKey, patchKeyFor, applyRecordPatches, recordsPerPage, sampleBodyBytes, sessionPageKey, tombstoneKeyFor, unpackRecord, } from './record-paging.js';
8
+ import { PAGE_TAG, UNPAGED_MAX_RECORDS, PAGE_KEYS_FIELD, BODY_FIELD, TAG_FIELD, assertUsableRecordKey, classifyStoreKey, decodePageKeys, decodePageRecords, decodeRecordValue, encodePage, flattenPagedLookup, isPagedMarkerEntry, isReservedKey, overlayKeyFor, packWholeValue, parseSessionPageKey, repackRecord, parsePatchKey, patchKeyFor, applyRecordPatches, recordsPerPage, sampleBodyBytes, sessionPageKey, tombstoneKeyFor, unpackRecord, } from './record-paging.js';
9
9
  import { MutateBatcher } from './mutate-batching.js';
10
+ import { pushBoundedSplice } from './subscriber-delivery.js';
10
11
  // ---- Plain-JS-proxy machinery, scoped per-container --------------------
11
12
  function isPlainObject(v) {
12
13
  return (v !== null &&
@@ -153,19 +154,41 @@ function assignList(target, value) {
153
154
  // same position, so the pair leaves the entry stored twice while `set`
154
155
  // leaves it stored once. That is the same reasoning `assignListPosition`
155
156
  // carries, and a whole-array assign is the write that reaches it most.
157
+ //
158
+ // The entries both ends share are left alone, and only the span between
159
+ // them is rewritten. Dropping the first of `[a, b]` is then a delete of `a`
160
+ // rather than `b` set into position 0 and the tail cut off, which matters
161
+ // when a peer undoes its own edit to `b` concurrently: the undo removes the
162
+ // `b` it wrote, and a set of `b` would put one back.
156
163
  const oldLen = target.length;
157
- for (let i = 0; i < value.length; i++) {
164
+ let head = 0;
165
+ while (head < oldLen &&
166
+ head < value.length &&
167
+ samePlainValue(plainListItem(target, head), materializeContainers(value[head]))) {
168
+ head += 1;
169
+ }
170
+ let tail = 0;
171
+ while (tail < oldLen - head &&
172
+ tail < value.length - head &&
173
+ samePlainValue(plainListItem(target, oldLen - 1 - tail), materializeContainers(value[value.length - 1 - tail]))) {
174
+ tail += 1;
175
+ }
176
+ const oldMid = oldLen - head - tail;
177
+ const newMid = value.length - head - tail;
178
+ const shared = Math.min(oldMid, newMid);
179
+ for (let i = head; i < head + shared; i++) {
158
180
  const item = materializeContainers(value[i]);
159
- if (i >= oldLen) {
160
- insertListAt(target, i, item);
161
- continue;
162
- }
163
181
  if (samePlainValue(plainListItem(target, i), item))
164
182
  continue;
165
183
  assignListPosition(target, i, item);
166
184
  }
167
- if (value.length < oldLen) {
168
- target.delete(value.length, oldLen - value.length);
185
+ if (newMid > oldMid) {
186
+ for (let i = head + oldMid; i < head + newMid; i++) {
187
+ insertListAt(target, i, materializeContainers(value[i]));
188
+ }
189
+ }
190
+ else if (oldMid > newMid) {
191
+ target.delete(head + newMid, oldMid - newMid);
169
192
  }
170
193
  }
171
194
  /** Whether a position already holds this value, so assigning it writes
@@ -212,12 +235,21 @@ function insertListTail(target, rawItem) {
212
235
  // stored twice, and `set` leaves it stored once.
213
236
  function assignListPosition(target, idx, rawItem) {
214
237
  const item = materializeContainers(rawItem);
238
+ // A position already holding a container of the same kind is written in
239
+ // place, as `assignEntry` does for a map key. A fresh container at the
240
+ // position is a replace that a peer's concurrent replace of the same
241
+ // position does not merge with, so undoing either one restores its old
242
+ // container beside the other's instead of in its place, and the list
243
+ // comes out one element longer on every peer.
244
+ const existing = target.get(idx);
215
245
  if (isPlainObject(item)) {
216
- const child = target.setContainer(idx, new LoroMap());
246
+ const child = existing instanceof LoroMap ? existing : target.setContainer(idx, new LoroMap());
217
247
  assignPlain(child, item);
218
248
  }
219
249
  else if (Array.isArray(item)) {
220
- const child = target.setContainer(idx, new LoroMovableList());
250
+ const child = existing instanceof LoroMovableList
251
+ ? existing
252
+ : target.setContainer(idx, new LoroMovableList());
221
253
  assignList(child, item);
222
254
  }
223
255
  else {
@@ -297,7 +329,7 @@ function writeThroughBody(value, commit) {
297
329
  // `loroMap.get(key)` (O(1) per key, so `Object.keys()` is O(N)). Iterating
298
330
  // `loroMap.keys()` in the descriptor trap makes `Object.keys()` O(N^2) at
299
331
  // 50k entities (measured at >5 min of CPU before giving up).
300
- export function wrapLoroMapProxy(loroMap,
332
+ function wrapLoroMapProxy(loroMap,
301
333
  // Threaded into nested record proxies too, so a field edit counts. A
302
334
  // mutation can hold thousands of them (`m[id].params = ...` in a loop),
303
335
  // which reaches the same size a bulk write does while touching no
@@ -307,10 +339,14 @@ batcher, divert,
307
339
  // reserved space the store's own bookkeeping occupies. True for a root map,
308
340
  // which is what can page; false inside a record, whose field names are the
309
341
  // app's own and constrained by nothing.
310
- guardKeys = false) {
342
+ guardKeys = false,
343
+ // The root this map is, which decides whether it carries platform entries
344
+ // the draft hides and refuses (`isPagedMarkerEntry`).
345
+ rootKey = null) {
346
+ const hidden = (key) => isPagedMarkerEntry(rootKey, key);
311
347
  const handler = {
312
348
  get(_t, key) {
313
- if (typeof key === 'symbol')
349
+ if (typeof key === 'symbol' || hidden(key))
314
350
  return undefined;
315
351
  if (divert?.holds(key)) {
316
352
  const held = divert.peek(key);
@@ -354,7 +390,7 @@ guardKeys = false) {
354
390
  if (typeof key === 'symbol')
355
391
  return false;
356
392
  if (guardKeys)
357
- assertUsableRecordKey(key);
393
+ assertUsableRecordKey(key, rootKey);
358
394
  // Before the write, so a commit it triggers seals the preceding
359
395
  // batch rather than splitting this record out of it.
360
396
  batcher?.record(value);
@@ -371,7 +407,7 @@ guardKeys = false) {
371
407
  if (typeof key === 'symbol')
372
408
  return false;
373
409
  if (guardKeys)
374
- assertUsableRecordKey(key);
410
+ assertUsableRecordKey(key, rootKey);
375
411
  batcher?.recordDelete(key);
376
412
  divert?.drop(key);
377
413
  loroMap.delete(key);
@@ -379,19 +415,20 @@ guardKeys = false) {
379
415
  return true;
380
416
  },
381
417
  has(_t, key) {
382
- if (typeof key === 'symbol')
418
+ if (typeof key === 'symbol' || hidden(key))
383
419
  return false;
384
420
  if (divert?.holds(key))
385
421
  return true;
386
422
  return loroMap.get(key) !== undefined;
387
423
  },
388
424
  ownKeys() {
425
+ const stored = loroMap.keys().filter((key) => !hidden(key));
389
426
  if (!divert)
390
- return loroMap.keys();
391
- return [...new Set([...loroMap.keys(), ...divert.keys()])];
427
+ return stored;
428
+ return [...new Set([...stored, ...divert.keys()])];
392
429
  },
393
430
  getOwnPropertyDescriptor(_t, key) {
394
- if (typeof key === 'symbol')
431
+ if (typeof key === 'symbol' || hidden(key))
395
432
  return undefined;
396
433
  if (divert?.holds(key)) {
397
434
  return {
@@ -723,8 +760,6 @@ batcher) {
723
760
  };
724
761
  return new Proxy([], handler);
725
762
  }
726
- const TAG_FIELD = 'type';
727
- const BODY_FIELD = 'params';
728
763
  // The record keys one page packs, read off the live container without
729
764
  // materialising its bodies. Two fields out of wasm rather than the whole
730
765
  // container: a store's bodies are its bulk, and an index build asks every page
@@ -1244,7 +1279,9 @@ function buildPagedDeltaIndex(m) {
1244
1279
  }
1245
1280
  return { pages, shadowed };
1246
1281
  }
1247
- function unpagedMapDelta(m, depth, batch) {
1282
+ // Null when every key the batch touched is one `hidden` withholds, so a
1283
+ // subscriber is not handed an empty change for an edit it cannot see.
1284
+ function unpagedMapDelta(m, depth, batch, hidden) {
1248
1285
  const setKeys = new Set();
1249
1286
  const deleted = new Set();
1250
1287
  for (const ev of batch.events) {
@@ -1269,10 +1306,15 @@ function unpagedMapDelta(m, depth, batch) {
1269
1306
  }
1270
1307
  for (const key of deleted)
1271
1308
  setKeys.delete(key);
1309
+ const touched = setKeys.size + deleted.size;
1272
1310
  const set = new Map();
1273
1311
  for (const key of setKeys)
1274
- set.set(key, plainEntry(m, key));
1275
- return { set, deleted: [...deleted] };
1312
+ if (!hidden(key))
1313
+ set.set(key, plainEntry(m, key));
1314
+ const removed = [...deleted].filter((key) => !hidden(key));
1315
+ if (touched > 0 && set.size === 0 && removed.length === 0)
1316
+ return null;
1317
+ return { set, deleted: removed };
1276
1318
  }
1277
1319
  /**
1278
1320
  * Read a changed page out to the records it holds, and name the ones it lost.
@@ -2367,23 +2409,23 @@ function trackEntryContainer(root, spec) {
2367
2409
  // dedup, a different shape: it models no per-listener external resource, and
2368
2410
  // the Loro subscription each record must re-create on rebind is exactly
2369
2411
  // that.)
2370
- /** Run a callback the SDK owns, without letting it break the caller. */
2371
- /** Report a subscriber that threw, then carry on. One app callback failing
2372
- * must not break a registration or stop the other subscribers, but swallowing
2373
- * it leaves an app that stalled inside its own callback with nothing in the
2374
- * console to read. */
2375
- function reportSubscriberThrow(err) {
2376
- console.error('[Brass] subscriber threw:', err);
2377
- }
2378
- function fireQuietly(cb) {
2412
+ /** Run an app callback without letting a throw break the caller. One app
2413
+ * callback failing must not break a registration or stop the other
2414
+ * subscribers, so the throw goes to `report` (the owner's
2415
+ * `reportSubscriberThrow`) and the walk carries on. */
2416
+ function fireQuietly(cb, report) {
2379
2417
  try {
2380
2418
  cb();
2381
2419
  }
2382
2420
  catch (err) {
2383
- reportSubscriberThrow(err);
2421
+ report(err);
2384
2422
  }
2385
2423
  }
2386
2424
  class SubscriptionRegistry {
2425
+ _report;
2426
+ constructor(_report) {
2427
+ this._report = _report;
2428
+ }
2387
2429
  _subs = new Set();
2388
2430
  _settled = new Set();
2389
2431
  // Whether a whole-container delivery is in progress. Always false while a
@@ -2413,7 +2455,7 @@ class SubscriptionRegistry {
2413
2455
  onSettled(cb) {
2414
2456
  this._settled.add(cb);
2415
2457
  if (!this._delivering)
2416
- fireQuietly(cb);
2458
+ fireQuietly(cb, this._report);
2417
2459
  return () => {
2418
2460
  this._settled.delete(cb);
2419
2461
  };
@@ -2424,7 +2466,7 @@ class SubscriptionRegistry {
2424
2466
  // before it is reached is never visited.
2425
2467
  const listeners = Array.from(this._settled);
2426
2468
  for (const cb of listeners)
2427
- fireQuietly(cb);
2469
+ fireQuietly(cb, this._report);
2428
2470
  }
2429
2471
  // Fire the whole-container catch-up once, attach the per-container change
2430
2472
  // subscription, track the record, and return an idempotent unsubscribe.
@@ -2492,7 +2534,7 @@ function sameStateVersion(a, b) {
2492
2534
  * Every field is declared with an initialiser, so they all exist by the time
2493
2535
  * this runs and a later assignment keeps the descriptor this sets.
2494
2536
  */
2495
- function hideOwnFields(handle) {
2537
+ export function hideOwnFields(handle) {
2496
2538
  for (const key of Object.keys(handle)) {
2497
2539
  Object.defineProperty(handle, key, { enumerable: false });
2498
2540
  }
@@ -2515,7 +2557,7 @@ class MapHandleImpl {
2515
2557
  // Active subscriptions, tracked so a version checkout (which swaps the
2516
2558
  // active source doc) can re-fire and re-attach each one against the new
2517
2559
  // container.
2518
- _registry = new SubscriptionRegistry();
2560
+ _registry = new SubscriptionRegistry((err) => this._owner.reportSubscriberThrow(err));
2519
2561
  // Keys captured just before a rebind (`_rebindAnchor`), consumed by the
2520
2562
  // matching `_resync` to report the keys the source swap removed. Null
2521
2563
  // outside a rebind cycle, so a bare `_resync` reports no removals.
@@ -2524,13 +2566,28 @@ class MapHandleImpl {
2524
2566
  // taken before the container exists watches up this chain for the write
2525
2567
  // that creates it (`_nearestLiveContainer`).
2526
2568
  _parent;
2527
- constructor(anchor, owner, paging = null, parent = null) {
2569
+ // The document root this handle is, or null for a composed handle. Decides
2570
+ // which keys are the platform's own entries rather than the app's
2571
+ // (`isPagedMarkerEntry`), which every read here hides.
2572
+ _rootKey;
2573
+ constructor(anchor, owner, paging = null, parent = null, rootKey = null) {
2528
2574
  this._anchor = anchor;
2529
2575
  this._owner = owner;
2530
2576
  this._paging = paging;
2531
2577
  this._parent = parent;
2578
+ this._rootKey = rootKey;
2532
2579
  hideOwnFields(this);
2533
2580
  }
2581
+ _hidden(key) {
2582
+ return isPagedMarkerEntry(this._rootKey, key);
2583
+ }
2584
+ // An unpaged map's JSON without the platform's own entries.
2585
+ _visible(json) {
2586
+ for (const key of Object.keys(json))
2587
+ if (this._hidden(key))
2588
+ delete json[key];
2589
+ return json;
2590
+ }
2534
2591
  // The container at the top of this handle's composition chain: the root it
2535
2592
  // is composed off, which the document holds for its whole life (`getMap` is
2536
2593
  // get-or-create, and a root anchor always resolves). A composed handle's
@@ -2566,6 +2623,14 @@ class MapHandleImpl {
2566
2623
  const v = this._anchor.plain?.();
2567
2624
  return isPlainObject(v) ? v : undefined;
2568
2625
  }
2626
+ /** Whether the plain body this handle is scoped to holds this entry itself.
2627
+ * The prototype chain is not part of a record, so a key an app never wrote
2628
+ * must not resolve through it. */
2629
+ _plainHas(entryKey) {
2630
+ const body = this._plainBody();
2631
+ return (body !== undefined &&
2632
+ Object.prototype.hasOwnProperty.call(body, entryKey));
2633
+ }
2569
2634
  read() {
2570
2635
  this._owner.assertReadable();
2571
2636
  const m = this._anchor.resolve();
@@ -2575,7 +2640,7 @@ class MapHandleImpl {
2575
2640
  // paged or not.
2576
2641
  if (this._isPaged())
2577
2642
  return this._readPaged(m);
2578
- return m.toJSON();
2643
+ return this._visible(m.toJSON());
2579
2644
  }
2580
2645
  // The whole-store read, which decodes every page. That walk answers the same
2581
2646
  // question a keyed read asks (which page holds which record), so the index
@@ -2592,9 +2657,18 @@ class MapHandleImpl {
2592
2657
  }
2593
2658
  get(entryKey) {
2594
2659
  this._owner.assertReadable();
2660
+ if (this._hidden(entryKey))
2661
+ return undefined;
2595
2662
  const m = this._anchor.resolve();
2596
- if (!m)
2597
- return this._plainBody()?.[entryKey];
2663
+ if (!m) {
2664
+ // One read of the body, not one per check: `_plainBody` resolves the
2665
+ // anchor and reaches into wasm, and this runs on every keyed read.
2666
+ const body = this._plainBody();
2667
+ return body !== undefined &&
2668
+ Object.prototype.hasOwnProperty.call(body, entryKey)
2669
+ ? body[entryKey]
2670
+ : undefined;
2671
+ }
2598
2672
  if (this._isPaged()) {
2599
2673
  const pager = this._sessionPager(m);
2600
2674
  return recordFromPage(m, entryKey, this._pageMemo, pager.liveIndex, pager.patches(m));
@@ -2604,6 +2678,14 @@ class MapHandleImpl {
2604
2678
  keys() {
2605
2679
  return [...this.liveKeySet()];
2606
2680
  }
2681
+ // Taken from one whole read: on a paged store that is the pass resolving
2682
+ // every record against the patches and markers that shadow it, which a walk
2683
+ // key by key would repeat per record.
2684
+ *entries() {
2685
+ const records = this.read();
2686
+ for (const key of Object.keys(records))
2687
+ yield [key, records[key]];
2688
+ }
2607
2689
  /**
2608
2690
  * The keys this map holds, as a set, without materialising a record.
2609
2691
  *
@@ -2619,27 +2701,29 @@ class MapHandleImpl {
2619
2701
  return new Set(Object.keys(this._plainBody() ?? {}));
2620
2702
  if (this._isPaged())
2621
2703
  return this._sessionPager(m).ensureIndex(m).liveKeys();
2622
- return new Set(m.keys());
2704
+ return new Set(m.keys().filter((key) => !this._hidden(key)));
2623
2705
  }
2624
2706
  has(entryKey) {
2625
2707
  this._owner.assertReadable();
2708
+ if (this._hidden(entryKey))
2709
+ return false;
2626
2710
  const m = this._anchor.resolve();
2627
- if (!m) {
2628
- const body = this._plainBody();
2629
- return body !== undefined && Object.prototype.hasOwnProperty.call(body, entryKey);
2630
- }
2711
+ if (!m)
2712
+ return this._plainHas(entryKey);
2631
2713
  if (this._isPaged())
2632
2714
  return this.get(entryKey) !== undefined;
2633
2715
  return entryExists(m, entryKey);
2634
2716
  }
2635
- mutate(fn) {
2717
+ mutate(fn, options) {
2636
2718
  this._owner.assertWritable();
2637
2719
  this._owner.ensureLocalUpdatesSubscribed();
2638
- // Commits as it goes past a size budget; see `mutate-batching.ts`.
2639
- this._runMutate(fn, new MutateBatcher(() => {
2720
+ this._owner.runMutate(options, () => {
2721
+ // Commits as it goes past a size budget; see `mutate-batching.ts`.
2722
+ this._runMutate(fn, new MutateBatcher(() => {
2723
+ this._owner.commitMutate();
2724
+ }));
2640
2725
  this._owner.commitMutate();
2641
- }));
2642
- this._owner.commitMutate();
2726
+ });
2643
2727
  }
2644
2728
  // Run a mutate WITHOUT the commit pipeline (the caller owns
2645
2729
  // assertWritable / ensureLocalUpdatesSubscribed / commit). The seam the
@@ -2654,7 +2738,7 @@ class MapHandleImpl {
2654
2738
  return;
2655
2739
  }
2656
2740
  const divert = this._paging ? this._pagingDivert(m, batcher) : null;
2657
- const proxy = wrapLoroMapProxy(m, batcher, divert?.hooks, this._paging !== null);
2741
+ const proxy = wrapLoroMapProxy(m, batcher, divert?.hooks, this._paging !== null, this._rootKey);
2658
2742
  fn(proxy);
2659
2743
  divert?.finish();
2660
2744
  }
@@ -2977,7 +3061,7 @@ class MapHandleImpl {
2977
3061
  }
2978
3062
  // One walk of an unpaged map, whose size the paging threshold bounds, and
2979
3063
  // no wasm handle minted per key the way a per-key read would.
2980
- const keys = new Set(m.keys());
3064
+ const keys = new Set(m.keys().filter((key) => !this._hidden(key)));
2981
3065
  return (key) => keys.has(key);
2982
3066
  }
2983
3067
  // Fire a subscriber once with the whole map as `change.set`, so a
@@ -3003,7 +3087,7 @@ class MapHandleImpl {
3003
3087
  }, undefined, true);
3004
3088
  }
3005
3089
  catch (err) {
3006
- reportSubscriberThrow(err);
3090
+ this._owner.reportSubscriberThrow(err);
3007
3091
  }
3008
3092
  }
3009
3093
  // Attach the change subscription for `cb`, returning its detach.
@@ -3077,13 +3161,15 @@ class MapHandleImpl {
3077
3161
  }
3078
3162
  const change = (index
3079
3163
  ? pagedMapDelta(m, event, index, (key) => this.get(key))
3080
- : unpagedMapDelta(m, depth, event));
3164
+ : unpagedMapDelta(m, depth, event, (key) => this._hidden(key)));
3165
+ if (change === null)
3166
+ return;
3081
3167
  const origin = { source: this._owner.originOf(event) };
3082
3168
  try {
3083
3169
  cb(change, origin, false);
3084
3170
  }
3085
3171
  catch (err) {
3086
- reportSubscriberThrow(err);
3172
+ this._owner.reportSubscriberThrow(err);
3087
3173
  }
3088
3174
  });
3089
3175
  return () => sub();
@@ -3216,7 +3302,7 @@ class MapHandleImpl {
3216
3302
  const direct = childAnchor(this._anchor, entryKey, ctor);
3217
3303
  if (this._paging === null)
3218
3304
  return direct;
3219
- assertUsableRecordKey(entryKey);
3305
+ assertUsableRecordKey(entryKey, this._rootKey);
3220
3306
  const overlay = childAnchor(this._anchor, overlayKeyFor(entryKey), ctor);
3221
3307
  // Whether an edit written beside the pages is waiting to be folded over
3222
3308
  // this record. Asked only once a container has been found, since a record
@@ -3313,6 +3399,20 @@ class MapHandleImpl {
3313
3399
  * carry the position each edit lands at, so an app applies them in order and
3314
3400
  * is done; nothing about the cursor reaches it.
3315
3401
  */
3402
+ /** Whether position `at`, read after the first `count` splices, is one of the
3403
+ * elements those splices inserted. */
3404
+ function insertedBySplices(splices, count, at) {
3405
+ let pos = at;
3406
+ for (let i = count - 1; i >= 0; i--) {
3407
+ const { at: start, removed, inserted } = splices[i];
3408
+ if (pos < start)
3409
+ continue;
3410
+ if (pos < start + inserted.length)
3411
+ return true;
3412
+ pos += removed - inserted.length;
3413
+ }
3414
+ return false;
3415
+ }
3316
3416
  function splicesFromListDiff(diff) {
3317
3417
  const out = [];
3318
3418
  let at = 0;
@@ -3327,7 +3427,7 @@ function splicesFromListDiff(diff) {
3327
3427
  }
3328
3428
  if (part.insert !== undefined) {
3329
3429
  const inserted = part.insert.map(plainDiffItem);
3330
- out.push({ at, removed: 0, inserted });
3430
+ pushBoundedSplice(out, { at, removed: 0, inserted });
3331
3431
  at += inserted.length;
3332
3432
  }
3333
3433
  }
@@ -3358,7 +3458,7 @@ class ListHandleImpl {
3358
3458
  _owner;
3359
3459
  // Active subscriptions, tracked so a version checkout can re-fire and
3360
3460
  // re-attach each one against the new container.
3361
- _registry = new SubscriptionRegistry();
3461
+ _registry = new SubscriptionRegistry((err) => this._owner.reportSubscriberThrow(err));
3362
3462
  // See `MapHandleImpl._parent`.
3363
3463
  _parent;
3364
3464
  constructor(anchor, owner, parent = null) {
@@ -3385,14 +3485,39 @@ class ListHandleImpl {
3385
3485
  const l = this._anchor.resolve();
3386
3486
  return l ? l.length : (this._plainBody()?.length ?? 0);
3387
3487
  }
3388
- mutate(fn) {
3488
+ get(index) {
3489
+ this._owner.assertReadable();
3490
+ if (!Number.isInteger(index) || index < 0)
3491
+ return undefined;
3492
+ const l = this._anchor.resolve();
3493
+ if (!l)
3494
+ return this._plainBody()?.[index];
3495
+ if (index >= l.length)
3496
+ return undefined;
3497
+ const v = l.get(index);
3498
+ if (!isContainer(v))
3499
+ return v;
3500
+ const container = v;
3501
+ try {
3502
+ return container.toJSON();
3503
+ }
3504
+ finally {
3505
+ container.free();
3506
+ }
3507
+ }
3508
+ *values() {
3509
+ yield* this.read();
3510
+ }
3511
+ mutate(fn, options) {
3389
3512
  this._owner.assertWritable();
3390
3513
  this._owner.ensureLocalUpdatesSubscribed();
3391
- // Commits as it goes past a size budget; see `mutate-batching.ts`.
3392
- this._runMutate(fn, new MutateBatcher(() => {
3514
+ this._owner.runMutate(options, () => {
3515
+ // Commits as it goes past a size budget; see `mutate-batching.ts`.
3516
+ this._runMutate(fn, new MutateBatcher(() => {
3517
+ this._owner.commitMutate();
3518
+ }));
3393
3519
  this._owner.commitMutate();
3394
- }));
3395
- this._owner.commitMutate();
3520
+ });
3396
3521
  }
3397
3522
  // Run a list mutate without the commit pipeline; see
3398
3523
  // `MapHandleImpl._runMutate`. The ordered-map primitive drives this to
@@ -3405,7 +3530,7 @@ class ListHandleImpl {
3405
3530
  const proxy = wrapLoroListProxy(l, batcher);
3406
3531
  fn(proxy);
3407
3532
  }
3408
- // Fire the whole list as ONE splice replacing everything the subscriber was
3533
+ // Fire the whole list as one change replacing everything the subscriber was
3409
3534
  // last told. Used for registration and for a checkout / return resync, which
3410
3535
  // describe no authored transition and so carry no origin.
3411
3536
  //
@@ -3419,10 +3544,12 @@ class ListHandleImpl {
3419
3544
  const held = this._held.get(cb) ?? 0;
3420
3545
  this._held.set(cb, inserted.length);
3421
3546
  try {
3422
- cb({ splices: [{ at: 0, removed: held, inserted }] });
3547
+ const splices = [];
3548
+ pushBoundedSplice(splices, { at: 0, removed: held, inserted });
3549
+ cb({ splices });
3423
3550
  }
3424
3551
  catch (err) {
3425
- reportSubscriberThrow(err);
3552
+ this._owner.reportSubscriberThrow(err);
3426
3553
  }
3427
3554
  }
3428
3555
  // Attach the change subscription for `cb`, returning its detach. A root
@@ -3446,11 +3573,30 @@ class ListHandleImpl {
3446
3573
  // Subscribe `cb` to one container's own changes; see
3447
3574
  // `MapHandleImpl._subscribeContainer`.
3448
3575
  _subscribeContainer(l, cb) {
3576
+ const depth = containerDepth(l);
3449
3577
  const sub = l.subscribe((event) => {
3450
- const diff = event.events[0]?.diff.type === 'list'
3451
- ? event.events[0].diff.diff
3452
- : [];
3453
- const splices = splicesFromListDiff(diff);
3578
+ const splices = [];
3579
+ // An element held as a child container changes inside that container,
3580
+ // which the list's own diff does not name. Each such element is
3581
+ // reported as a replace of its position, read after the list's own
3582
+ // splices so the index is the one the subscriber holds by then. An
3583
+ // element those splices inserted already arrived as it now stands, so
3584
+ // its own fields being written in the same commit adds no replace.
3585
+ const touched = new Set();
3586
+ for (const e of event.events) {
3587
+ if (e.path.length === depth && e.diff.type === 'list') {
3588
+ splices.push(...splicesFromListDiff(e.diff.diff));
3589
+ }
3590
+ else if (e.path.length > depth && typeof e.path[depth] === 'number') {
3591
+ touched.add(e.path[depth]);
3592
+ }
3593
+ }
3594
+ const listSplices = splices.length;
3595
+ for (const at of [...touched].sort((x, y) => x - y)) {
3596
+ if (at >= l.length || insertedBySplices(splices, listSplices, at))
3597
+ continue;
3598
+ splices.push({ at, removed: 1, inserted: [plainListItem(l, at)] });
3599
+ }
3454
3600
  let held = this._held.get(cb) ?? 0;
3455
3601
  for (const splice of splices)
3456
3602
  held += splice.inserted.length - splice.removed;
@@ -3459,7 +3605,7 @@ class ListHandleImpl {
3459
3605
  cb({ splices }, { source: this._owner.originOf(event) });
3460
3606
  }
3461
3607
  catch (err) {
3462
- reportSubscriberThrow(err);
3608
+ this._owner.reportSubscriberThrow(err);
3463
3609
  }
3464
3610
  });
3465
3611
  return () => sub();
@@ -3591,6 +3737,10 @@ class OrderedMapHandleImpl {
3591
3737
  keys() {
3592
3738
  return sequenceOf(this._map.liveKeySet(), this._order.read());
3593
3739
  }
3740
+ *entries() {
3741
+ for (const { key, value } of this.ordered())
3742
+ yield [key, value];
3743
+ }
3594
3744
  ordered() {
3595
3745
  const records = this._map.read();
3596
3746
  const seen = new Set();
@@ -3723,9 +3873,14 @@ class OrderedMapHandleImpl {
3723
3873
  });
3724
3874
  return true;
3725
3875
  }
3726
- mutate(fn) {
3876
+ mutate(fn, options) {
3727
3877
  this._owner.assertWritable();
3728
3878
  this._owner.ensureLocalUpdatesSubscribed();
3879
+ this._owner.runMutate(options, () => {
3880
+ this._mutateOrdered(fn);
3881
+ });
3882
+ }
3883
+ _mutateOrdered(fn) {
3729
3884
  // Net structural effect: keys added (`added` iterates in insertion
3730
3885
  // order, and a delete-then-re-set re-appends, matching the order list's
3731
3886
  // append semantics) and removed. A field edit routes through the inner
@@ -3931,8 +4086,8 @@ class OrderedMapHandleImpl {
3931
4086
  // transparently as a flat record map. The Document supplies it for every root
3932
4087
  // map; a non-paged map never consults it, so passing it to non-record roots
3933
4088
  // is harmless. Omitted means the handle only serves the plain unpaged shape.
3934
- export function createRootMapHandle(root, owner, paging) {
3935
- return new MapHandleImpl(rootAnchor(root), owner, paging ?? null);
4089
+ export function createRootMapHandle(root, owner, paging, rootKey) {
4090
+ return new MapHandleImpl(rootAnchor(root), owner, paging ?? null, null, rootKey ?? null);
3936
4091
  }
3937
4092
  export function createRootListHandle(root, owner) {
3938
4093
  return new ListHandleImpl(rootAnchor(root), owner);