@brass-build/client 0.1.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 (258) hide show
  1. package/AGENTS.md +255 -0
  2. package/CHANGELOG.md +15 -0
  3. package/LICENSE +21 -0
  4. package/README.md +836 -0
  5. package/dist/api-fetch.d.ts +16 -0
  6. package/dist/api-fetch.d.ts.map +1 -0
  7. package/dist/api-fetch.js +97 -0
  8. package/dist/api-fetch.js.map +1 -0
  9. package/dist/api-headers.d.ts +3 -0
  10. package/dist/api-headers.d.ts.map +1 -0
  11. package/dist/api-headers.js +25 -0
  12. package/dist/api-headers.js.map +1 -0
  13. package/dist/auth-flow.d.ts +23 -0
  14. package/dist/auth-flow.d.ts.map +1 -0
  15. package/dist/auth-flow.js +76 -0
  16. package/dist/auth-flow.js.map +1 -0
  17. package/dist/client.d.ts +106 -0
  18. package/dist/client.d.ts.map +1 -0
  19. package/dist/client.js +1278 -0
  20. package/dist/client.js.map +1 -0
  21. package/dist/doc-url-sync.d.ts +19 -0
  22. package/dist/doc-url-sync.d.ts.map +1 -0
  23. package/dist/doc-url-sync.js +181 -0
  24. package/dist/doc-url-sync.js.map +1 -0
  25. package/dist/document-branches.d.ts +32 -0
  26. package/dist/document-branches.d.ts.map +1 -0
  27. package/dist/document-branches.js +89 -0
  28. package/dist/document-branches.js.map +1 -0
  29. package/dist/document-checkpoints.d.ts +24 -0
  30. package/dist/document-checkpoints.d.ts.map +1 -0
  31. package/dist/document-checkpoints.js +115 -0
  32. package/dist/document-checkpoints.js.map +1 -0
  33. package/dist/document-commands.d.ts +93 -0
  34. package/dist/document-commands.d.ts.map +1 -0
  35. package/dist/document-commands.js +423 -0
  36. package/dist/document-commands.js.map +1 -0
  37. package/dist/document-signals.d.ts +53 -0
  38. package/dist/document-signals.d.ts.map +1 -0
  39. package/dist/document-signals.js +114 -0
  40. package/dist/document-signals.js.map +1 -0
  41. package/dist/document-streams.d.ts +22 -0
  42. package/dist/document-streams.d.ts.map +1 -0
  43. package/dist/document-streams.js +98 -0
  44. package/dist/document-streams.js.map +1 -0
  45. package/dist/document-thumbnail-capture.d.ts +27 -0
  46. package/dist/document-thumbnail-capture.d.ts.map +1 -0
  47. package/dist/document-thumbnail-capture.js +160 -0
  48. package/dist/document-thumbnail-capture.js.map +1 -0
  49. package/dist/documents-namespace.d.ts +134 -0
  50. package/dist/documents-namespace.d.ts.map +1 -0
  51. package/dist/documents-namespace.js +190 -0
  52. package/dist/documents-namespace.js.map +1 -0
  53. package/dist/documents.d.ts +146 -0
  54. package/dist/documents.d.ts.map +1 -0
  55. package/dist/documents.js +2045 -0
  56. package/dist/documents.js.map +1 -0
  57. package/dist/emitter.d.ts +15 -0
  58. package/dist/emitter.d.ts.map +1 -0
  59. package/dist/emitter.js +90 -0
  60. package/dist/emitter.js.map +1 -0
  61. package/dist/errors.d.ts +18 -0
  62. package/dist/errors.d.ts.map +1 -0
  63. package/dist/errors.js +22 -0
  64. package/dist/errors.js.map +1 -0
  65. package/dist/export-guest.d.ts +9 -0
  66. package/dist/export-guest.d.ts.map +1 -0
  67. package/dist/export-guest.js +225 -0
  68. package/dist/export-guest.js.map +1 -0
  69. package/dist/fetch-retry.d.ts +16 -0
  70. package/dist/fetch-retry.d.ts.map +1 -0
  71. package/dist/fetch-retry.js +90 -0
  72. package/dist/fetch-retry.js.map +1 -0
  73. package/dist/guest-link.d.ts +3 -0
  74. package/dist/guest-link.d.ts.map +1 -0
  75. package/dist/guest-link.js +13 -0
  76. package/dist/guest-link.js.map +1 -0
  77. package/dist/handles.d.ts +57 -0
  78. package/dist/handles.d.ts.map +1 -0
  79. package/dist/handles.js +1375 -0
  80. package/dist/handles.js.map +1 -0
  81. package/dist/hosted-runtime.d.ts +33 -0
  82. package/dist/hosted-runtime.d.ts.map +1 -0
  83. package/dist/hosted-runtime.js +99 -0
  84. package/dist/hosted-runtime.js.map +1 -0
  85. package/dist/hosted-ui.d.ts +84 -0
  86. package/dist/hosted-ui.d.ts.map +1 -0
  87. package/dist/hosted-ui.js +673 -0
  88. package/dist/hosted-ui.js.map +1 -0
  89. package/dist/importer.d.ts +22 -0
  90. package/dist/importer.d.ts.map +1 -0
  91. package/dist/importer.js +54 -0
  92. package/dist/importer.js.map +1 -0
  93. package/dist/index.d.ts +54 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +132 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/init-keys.d.ts +3 -0
  98. package/dist/init-keys.d.ts.map +1 -0
  99. package/dist/init-keys.js +13 -0
  100. package/dist/init-keys.js.map +1 -0
  101. package/dist/internal.d.ts +6 -0
  102. package/dist/internal.d.ts.map +1 -0
  103. package/dist/internal.js +17 -0
  104. package/dist/internal.js.map +1 -0
  105. package/dist/jwt.d.ts +3 -0
  106. package/dist/jwt.d.ts.map +1 -0
  107. package/dist/jwt.js +22 -0
  108. package/dist/jwt.js.map +1 -0
  109. package/dist/launch-context.d.ts +4 -0
  110. package/dist/launch-context.d.ts.map +1 -0
  111. package/dist/launch-context.js +44 -0
  112. package/dist/launch-context.js.map +1 -0
  113. package/dist/login-hint.d.ts +4 -0
  114. package/dist/login-hint.d.ts.map +1 -0
  115. package/dist/login-hint.js +63 -0
  116. package/dist/login-hint.js.map +1 -0
  117. package/dist/mutate-batching.d.ts +14 -0
  118. package/dist/mutate-batching.d.ts.map +1 -0
  119. package/dist/mutate-batching.js +65 -0
  120. package/dist/mutate-batching.js.map +1 -0
  121. package/dist/open-document.d.ts +127 -0
  122. package/dist/open-document.d.ts.map +1 -0
  123. package/dist/open-document.js +2121 -0
  124. package/dist/open-document.js.map +1 -0
  125. package/dist/page-title.d.ts +3 -0
  126. package/dist/page-title.d.ts.map +1 -0
  127. package/dist/page-title.js +32 -0
  128. package/dist/page-title.js.map +1 -0
  129. package/dist/perf.d.ts +9 -0
  130. package/dist/perf.d.ts.map +1 -0
  131. package/dist/perf.js +39 -0
  132. package/dist/perf.js.map +1 -0
  133. package/dist/presence.d.ts +62 -0
  134. package/dist/presence.d.ts.map +1 -0
  135. package/dist/presence.js +331 -0
  136. package/dist/presence.js.map +1 -0
  137. package/dist/record-paging.d.ts +99 -0
  138. package/dist/record-paging.d.ts.map +1 -0
  139. package/dist/record-paging.js +220 -0
  140. package/dist/record-paging.js.map +1 -0
  141. package/dist/schema-conflict-dialog.d.ts +10 -0
  142. package/dist/schema-conflict-dialog.d.ts.map +1 -0
  143. package/dist/schema-conflict-dialog.js +164 -0
  144. package/dist/schema-conflict-dialog.js.map +1 -0
  145. package/dist/selection-persistence.d.ts +5 -0
  146. package/dist/selection-persistence.d.ts.map +1 -0
  147. package/dist/selection-persistence.js +90 -0
  148. package/dist/selection-persistence.js.map +1 -0
  149. package/dist/serializer.d.ts +3 -0
  150. package/dist/serializer.d.ts.map +1 -0
  151. package/dist/serializer.js +2 -0
  152. package/dist/serializer.js.map +1 -0
  153. package/dist/session-persistence.d.ts +4 -0
  154. package/dist/session-persistence.d.ts.map +1 -0
  155. package/dist/session-persistence.js +68 -0
  156. package/dist/session-persistence.js.map +1 -0
  157. package/dist/session-toolbar.d.ts +62 -0
  158. package/dist/session-toolbar.d.ts.map +1 -0
  159. package/dist/session-toolbar.js +1205 -0
  160. package/dist/session-toolbar.js.map +1 -0
  161. package/dist/session.d.ts +69 -0
  162. package/dist/session.d.ts.map +1 -0
  163. package/dist/session.js +926 -0
  164. package/dist/session.js.map +1 -0
  165. package/dist/tail-ops.d.ts +28 -0
  166. package/dist/tail-ops.d.ts.map +1 -0
  167. package/dist/tail-ops.js +46 -0
  168. package/dist/tail-ops.js.map +1 -0
  169. package/dist/topic-lifecycle.d.ts +41 -0
  170. package/dist/topic-lifecycle.d.ts.map +1 -0
  171. package/dist/topic-lifecycle.js +169 -0
  172. package/dist/topic-lifecycle.js.map +1 -0
  173. package/dist/types.d.ts +22 -0
  174. package/dist/types.d.ts.map +1 -0
  175. package/dist/types.js +2 -0
  176. package/dist/types.js.map +1 -0
  177. package/dist/undo.d.ts +18 -0
  178. package/dist/undo.d.ts.map +1 -0
  179. package/dist/undo.js +110 -0
  180. package/dist/undo.js.map +1 -0
  181. package/dist/version-bar.d.ts +22 -0
  182. package/dist/version-bar.d.ts.map +1 -0
  183. package/dist/version-bar.js +169 -0
  184. package/dist/version-bar.js.map +1 -0
  185. package/dist/version.d.ts +3 -0
  186. package/dist/version.d.ts.map +1 -0
  187. package/dist/version.js +8 -0
  188. package/dist/version.js.map +1 -0
  189. package/dist/vite.d.ts +15 -0
  190. package/dist/vite.d.ts.map +1 -0
  191. package/dist/vite.js +36 -0
  192. package/dist/vite.js.map +1 -0
  193. package/dist/ws-client.d.ts +53 -0
  194. package/dist/ws-client.d.ts.map +1 -0
  195. package/dist/ws-client.js +604 -0
  196. package/dist/ws-client.js.map +1 -0
  197. package/dist/ws-frame-limit.d.ts +21 -0
  198. package/dist/ws-frame-limit.d.ts.map +1 -0
  199. package/dist/ws-frame-limit.js +40 -0
  200. package/dist/ws-frame-limit.js.map +1 -0
  201. package/docs/app-manifest.md +65 -0
  202. package/docs/custom-chrome.md +147 -0
  203. package/docs/hosted-ui-advanced.md +77 -0
  204. package/docs/imported-documents.md +261 -0
  205. package/docs/presence.md +129 -0
  206. package/docs/schemas.md +135 -0
  207. package/docs/streams.md +248 -0
  208. package/package.json +60 -0
  209. package/scripts/prepare.mjs +50 -0
  210. package/src/api-fetch.ts +127 -0
  211. package/src/api-headers.ts +28 -0
  212. package/src/auth-flow.ts +125 -0
  213. package/src/client.ts +1457 -0
  214. package/src/doc-url-sync.ts +183 -0
  215. package/src/document-branches.ts +124 -0
  216. package/src/document-checkpoints.ts +135 -0
  217. package/src/document-commands.ts +651 -0
  218. package/src/document-signals.ts +234 -0
  219. package/src/document-streams.ts +132 -0
  220. package/src/document-thumbnail-capture.ts +191 -0
  221. package/src/documents-namespace.ts +516 -0
  222. package/src/documents.ts +2426 -0
  223. package/src/emitter.ts +129 -0
  224. package/src/errors.ts +45 -0
  225. package/src/export-guest.ts +282 -0
  226. package/src/fetch-retry.ts +119 -0
  227. package/src/guest-link.ts +14 -0
  228. package/src/handles.ts +1731 -0
  229. package/src/hosted-runtime.ts +109 -0
  230. package/src/hosted-ui.ts +1020 -0
  231. package/src/importer.ts +89 -0
  232. package/src/index.ts +267 -0
  233. package/src/init-keys.ts +15 -0
  234. package/src/internal.ts +23 -0
  235. package/src/jwt.ts +18 -0
  236. package/src/launch-context.ts +43 -0
  237. package/src/login-hint.ts +60 -0
  238. package/src/mutate-batching.ts +63 -0
  239. package/src/open-document.ts +2869 -0
  240. package/src/page-title.ts +33 -0
  241. package/src/perf.ts +46 -0
  242. package/src/presence.ts +471 -0
  243. package/src/record-paging.ts +270 -0
  244. package/src/schema-conflict-dialog.ts +176 -0
  245. package/src/selection-persistence.ts +98 -0
  246. package/src/serializer.ts +15 -0
  247. package/src/session-persistence.ts +69 -0
  248. package/src/session-toolbar.ts +1457 -0
  249. package/src/session.ts +1262 -0
  250. package/src/tail-ops.ts +63 -0
  251. package/src/topic-lifecycle.ts +217 -0
  252. package/src/types.ts +75 -0
  253. package/src/undo.ts +125 -0
  254. package/src/version-bar.ts +221 -0
  255. package/src/version.ts +8 -0
  256. package/src/vite.ts +49 -0
  257. package/src/ws-client.ts +678 -0
  258. package/src/ws-frame-limit.ts +52 -0
@@ -0,0 +1,1375 @@
1
+ // Typed container handles. A handle is a scoped reference to a container in
2
+ // the CRDT doc; its `read` / `mutate` / `subscribe` only see (and only pay
3
+ // for) that container.
4
+ //
5
+ // Engine-neutral: Loro events are translated to a minimal `Change` shape at
6
+ // the handle boundary, so handle-only app code does not pin the SDK to Loro.
7
+ import { LoroList, LoroMap, isContainer, } from 'loro-crdt';
8
+ import { PAGE_TAG, decodePageBody, decodeRecordValue, encodePageBody, flattenPagedLookup, isPagingSchemeEditable, pageKeyFor, peekRecordTag, } from './record-paging.js';
9
+ import { BrassError } from './errors.js';
10
+ import { MutateBatcher } from './mutate-batching.js';
11
+ // ---- Plain-JS-proxy machinery, scoped per-container --------------------
12
+ function isPlainObject(v) {
13
+ return (v !== null &&
14
+ typeof v === 'object' &&
15
+ !Array.isArray(v) &&
16
+ v.constructor === Object);
17
+ }
18
+ // A value being assigned may be a live Loro container handle (a nested list
19
+ // read back through `m[key].listField`, or a nested-map proxy). Re-inserting
20
+ // an already-attached handle as a value corrupts it: it lands as an opaque
21
+ // reference and is lost on the next snapshot. Materialize it to a deep plain
22
+ // value first, so the assign path rebuilds it as a fresh container.
23
+ function materializeContainers(v) {
24
+ return isContainer(v) ? v.toJSON() : v;
25
+ }
26
+ // Deep-plain conversion for structures stored as plain Loro VALUES: any
27
+ // nested container handle or wrapped proxy materialises to pure JSON, and
28
+ // `undefined` entries drop (the documented JSON-serialise semantics of
29
+ // `mutate`).
30
+ function toPlainDeep(v) {
31
+ if (isContainer(v))
32
+ return v.toJSON();
33
+ if (Array.isArray(v))
34
+ return v.map(toPlainDeep);
35
+ if (v !== null && typeof v === 'object') {
36
+ const out = {};
37
+ for (const [k, x] of Object.entries(v)) {
38
+ if (x !== undefined)
39
+ out[k] = toPlainDeep(x);
40
+ }
41
+ return out;
42
+ }
43
+ return v;
44
+ }
45
+ function assignPlain(target, value) {
46
+ // Recursively materialize nested objects / arrays as nested LoroMap /
47
+ // LoroList containers so the CRDT mirrors the JS shape. Clear keys not in
48
+ // `value` so assignment is a replace, not a merge (property-level patching
49
+ // uses `d.entities[id].x = ...`).
50
+ for (const k of target.keys()) {
51
+ if (!(k in value))
52
+ target.delete(k);
53
+ }
54
+ for (const [k, rawV] of Object.entries(value)) {
55
+ const v = materializeContainers(rawV);
56
+ // Plain-VALUE preservation (as in the proxy set trap): a key holding a
57
+ // plain object/array value (a record body) stays a value, never promoted.
58
+ const existingV = target.get(k);
59
+ if (existingV !== undefined &&
60
+ !isContainer(existingV) &&
61
+ typeof existingV === 'object' &&
62
+ existingV !== null &&
63
+ typeof v === 'object' &&
64
+ v !== null) {
65
+ target.set(k, toPlainDeep(v));
66
+ continue;
67
+ }
68
+ if (isPlainObject(v)) {
69
+ // Reuse an existing LoroMap at this key: a fresh `setContainer` mints a
70
+ // new container ID, which under Loro's UndoManager is a structural
71
+ // destroy+create that does NOT merge within the merge window. In-place
72
+ // field overwrites DO merge, which whole-entity-rewrites-at-drag-cadence
73
+ // expects.
74
+ const existing = target.get(k);
75
+ const sub = existing instanceof LoroMap
76
+ ? existing
77
+ : target.setContainer(k, new LoroMap());
78
+ assignPlain(sub, v);
79
+ }
80
+ else if (Array.isArray(v)) {
81
+ const existingList = target.get(k);
82
+ const sub = existingList instanceof LoroList
83
+ ? existingList
84
+ : target.setContainer(k, new LoroList());
85
+ assignList(sub, v);
86
+ }
87
+ else {
88
+ target.set(k, v);
89
+ }
90
+ }
91
+ }
92
+ function assignList(target, value) {
93
+ // Same rationale as the LoroMap reuse above: replace contents in place
94
+ // rather than minting a new LoroList, so adjacent commits collapse into one
95
+ // undo step.
96
+ const oldLen = target.length;
97
+ for (let i = 0; i < value.length; i++) {
98
+ const item = materializeContainers(value[i]);
99
+ if (isPlainObject(item)) {
100
+ if (i < oldLen)
101
+ target.delete(i, 1);
102
+ const child = target.insertContainer(i, new LoroMap());
103
+ assignPlain(child, item);
104
+ }
105
+ else if (Array.isArray(item)) {
106
+ // A nested list: rebuild as a child LoroList rather than inserting the
107
+ // array as an opaque value.
108
+ if (i < oldLen)
109
+ target.delete(i, 1);
110
+ const child = target.insertContainer(i, new LoroList());
111
+ assignList(child, item);
112
+ }
113
+ else {
114
+ if (i < oldLen) {
115
+ target.delete(i, 1);
116
+ target.insert(i, item);
117
+ }
118
+ else {
119
+ target.insert(i, item);
120
+ }
121
+ }
122
+ }
123
+ if (value.length < oldLen) {
124
+ target.delete(value.length, oldLen - value.length);
125
+ }
126
+ }
127
+ // Append one value at the tail (same plain-vs-container handling as the
128
+ // indexed-set trap). The proxy's `push` fast-path uses this so a tail append
129
+ // stays O(1) instead of the full-list replay.
130
+ function insertListTail(target, rawItem) {
131
+ const idx = target.length;
132
+ const item = materializeContainers(rawItem);
133
+ if (isPlainObject(item)) {
134
+ const child = target.insertContainer(idx, new LoroMap());
135
+ assignPlain(child, item);
136
+ }
137
+ else if (Array.isArray(item)) {
138
+ const child = target.insertContainer(idx, new LoroList());
139
+ assignList(child, item);
140
+ }
141
+ else {
142
+ target.insert(idx, item);
143
+ }
144
+ }
145
+ // Exported because `Document.mutate` reuses the same proxy for its
146
+ // `entities` root. Load-bearing: `getOwnPropertyDescriptor` does a single
147
+ // `loroMap.get(key)` (O(1) per key, so `Object.keys()` is O(N)). Iterating
148
+ // `loroMap.keys()` in the descriptor trap makes `Object.keys()` O(N^2) at
149
+ // 50k entities (measured at >5 min of CPU before giving up).
150
+ export function wrapLoroMapProxy(loroMap,
151
+ // Threaded into nested record proxies too, so a field edit counts. A
152
+ // mutation can hold thousands of them (`m[id].params = ...` in a loop),
153
+ // which reaches the same size a bulk write does while touching no
154
+ // top-level key.
155
+ batcher) {
156
+ const handler = {
157
+ get(_t, key) {
158
+ if (typeof key === 'symbol')
159
+ return undefined;
160
+ const v = loroMap.get(key);
161
+ if (isContainer(v)) {
162
+ // Present an engine-neutral view, never the raw container, so app
163
+ // code can't take a hard dependency on the CRDT engine. A LoroMap
164
+ // recurses into another proxy; a LoroList reads back via `toJSON`
165
+ // (not `toArray`, which is shallow and hands back live element
166
+ // handles that corrupt on reassignment; list editing goes through
167
+ // `parent.list(key).mutate(...)`). Other container types are outside
168
+ // the v1 portable surface, so project them via toJSON().
169
+ if (v instanceof LoroMap)
170
+ return wrapLoroMapProxy(v, batcher);
171
+ if (v instanceof LoroList)
172
+ return v.toJSON();
173
+ return v.toJSON();
174
+ }
175
+ return v;
176
+ },
177
+ set(_t, key, value) {
178
+ if (typeof key === 'symbol')
179
+ return false;
180
+ // Before the write, so a commit it triggers seals the preceding
181
+ // batch rather than splitting this record out of it.
182
+ batcher?.record(value);
183
+ const existing = loroMap.get(key);
184
+ // Plain-VALUE preservation: when the key holds a plain (non-container)
185
+ // object or array, the new structure is written as a plain value too,
186
+ // never promoted. This keeps a record body (`rec.params`, one JSON
187
+ // value whose whole-replace is the merge unit) a value across edits;
188
+ // promoting it would change the merge granularity and bloat the
189
+ // snapshot with per-element containers.
190
+ if (existing !== undefined &&
191
+ !isContainer(existing) &&
192
+ typeof existing === 'object' &&
193
+ existing !== null &&
194
+ typeof value === 'object' &&
195
+ value !== null) {
196
+ loroMap.set(key, toPlainDeep(value));
197
+ return true;
198
+ }
199
+ if (isPlainObject(value)) {
200
+ // Reuse the existing LoroMap (see `assignPlain`): a fresh
201
+ // `setContainer` is a destroy+create Loro won't merge across, so
202
+ // `entities[id] = {...}` at drag cadence accumulates one undo entry
203
+ // per frame instead of collapsing into the merge window.
204
+ const sub = existing instanceof LoroMap
205
+ ? existing
206
+ : loroMap.setContainer(key, new LoroMap());
207
+ assignPlain(sub, value);
208
+ }
209
+ else if (Array.isArray(value)) {
210
+ const sub = existing instanceof LoroList
211
+ ? existing
212
+ : loroMap.setContainer(key, new LoroList());
213
+ assignList(sub, value);
214
+ }
215
+ else {
216
+ loroMap.set(key, value);
217
+ }
218
+ return true;
219
+ },
220
+ deleteProperty(_t, key) {
221
+ if (typeof key === 'symbol')
222
+ return false;
223
+ batcher?.recordDelete();
224
+ loroMap.delete(key);
225
+ return true;
226
+ },
227
+ has(_t, key) {
228
+ if (typeof key === 'symbol')
229
+ return false;
230
+ return loroMap.get(key) !== undefined;
231
+ },
232
+ ownKeys() {
233
+ return loroMap.keys();
234
+ },
235
+ getOwnPropertyDescriptor(_t, key) {
236
+ if (typeof key === 'symbol')
237
+ return undefined;
238
+ // Single get per key. Do NOT iterate `loroMap.keys()` here: it makes
239
+ // Object.keys() O(N^2) at 50k entities.
240
+ const v = loroMap.get(key);
241
+ if (v === undefined)
242
+ return undefined;
243
+ return {
244
+ enumerable: true,
245
+ configurable: true,
246
+ writable: true,
247
+ value: v,
248
+ };
249
+ },
250
+ };
251
+ return new Proxy({}, handler);
252
+ }
253
+ // Array.prototype methods that mutate the array in place. Called on a list
254
+ // mutate-proxy, their result is replayed into the LoroList; every other
255
+ // method is a pure read that leaves the container untouched, so a `find` /
256
+ // `findIndex` inside a mutate reads fields without rewriting the whole list.
257
+ // `push` is fast-pathed natively above the generic fallback and never reaches
258
+ // this set.
259
+ const MUTATING_ARRAY_METHODS = new Set([
260
+ 'pop',
261
+ 'shift',
262
+ 'unshift',
263
+ 'splice',
264
+ 'sort',
265
+ 'reverse',
266
+ 'fill',
267
+ 'copyWithin',
268
+ ]);
269
+ // An array-proxy for list mutators: index assignments, `.push` / `.splice`,
270
+ // and length-set route into the underlying LoroList so the user writes
271
+ // idiomatic JS. Reads delegate to a fresh snapshot per access so a sequence
272
+ // of reads inside the mutator sees each write.
273
+ function wrapLoroListProxy(loroList) {
274
+ const handler = {
275
+ get(_t, key) {
276
+ if (key === 'length')
277
+ return loroList.length;
278
+ if (typeof key === 'symbol') {
279
+ // `toJSON` (deep-plain), not `toArray` (shallow), so iterating the
280
+ // draft (`for (const c of l)`, `[...l]`) yields plain objects an app
281
+ // can read fields off, matching the array-method snapshot below.
282
+ return loroList.toJSON()[key];
283
+ }
284
+ if (typeof key === 'string' && /^\d+$/.test(key)) {
285
+ const idx = Number(key);
286
+ const v = loroList.get(idx);
287
+ if (isContainer(v)) {
288
+ // Engine-neutral view only (see the map proxy's get trap).
289
+ if (v instanceof LoroMap)
290
+ return wrapLoroMapProxy(v);
291
+ if (v instanceof LoroList)
292
+ return v.toJSON();
293
+ return v.toJSON();
294
+ }
295
+ return v;
296
+ }
297
+ // `push` is the hot path (the ordered-map primitive appends every new
298
+ // key through it; apps bulk-build lists with it), so append natively.
299
+ // The toArray + replay below is O(list) PER call, turning N appends
300
+ // into O(N^2) and faulting Loro at a few thousand keys; a tail insert
301
+ // is O(1).
302
+ if (key === 'push') {
303
+ return (...items) => {
304
+ for (const item of items)
305
+ insertListTail(loroList, item);
306
+ return loroList.length;
307
+ };
308
+ }
309
+ // Remaining array methods: bind to a deep-plain `toJSON` snapshot so a
310
+ // callback reading an element's fields (`findIndex((c) => c.id === x)`,
311
+ // `find`, `filter`, `map`) sees plain objects, not the raw child
312
+ // containers `toArray` hands back (whose fields read undefined). A
313
+ // mutating method replays its result into the LoroList; a pure read
314
+ // does not, or a `find` inside a mutate would rewrite the whole list.
315
+ // Best-effort for v1; efficient mutation uses indexed assignment /
316
+ // length-set / `push`.
317
+ const arr = loroList.toJSON();
318
+ const val = arr[key];
319
+ if (typeof val === 'function') {
320
+ return (...args) => {
321
+ const fn = val;
322
+ const result = fn.apply(arr, args);
323
+ if (MUTATING_ARRAY_METHODS.has(key))
324
+ assignList(loroList, arr);
325
+ return result;
326
+ };
327
+ }
328
+ return val;
329
+ },
330
+ set(_t, key, value) {
331
+ if (key === 'length') {
332
+ const newLen = Number(value);
333
+ const oldLen = loroList.length;
334
+ if (newLen < oldLen)
335
+ loroList.delete(newLen, oldLen - newLen);
336
+ return true;
337
+ }
338
+ if (typeof key === 'string' && /^\d+$/.test(key)) {
339
+ const idx = Number(key);
340
+ const oldLen = loroList.length;
341
+ if (idx < oldLen) {
342
+ loroList.delete(idx, 1);
343
+ }
344
+ if (isPlainObject(value)) {
345
+ const child = loroList.insertContainer(idx, new LoroMap());
346
+ assignPlain(child, value);
347
+ }
348
+ else if (Array.isArray(value)) {
349
+ const child = loroList.insertContainer(idx, new LoroList());
350
+ assignList(child, value);
351
+ }
352
+ else {
353
+ loroList.insert(idx, value);
354
+ }
355
+ return true;
356
+ }
357
+ return false;
358
+ },
359
+ };
360
+ return new Proxy([], handler);
361
+ }
362
+ const TAG_FIELD = 'type';
363
+ const BODY_FIELD = 'params';
364
+ // A page entry on the live LoroMap is a CONTAINER, not a plain
365
+ // `{ type, params }`, so it must be materialised before the page codec
366
+ // (which expects plain values) reads its tag / body.
367
+ function pageValue(m, key) {
368
+ const v = m.get(key);
369
+ if (v instanceof LoroMap)
370
+ return v.toJSON();
371
+ return v;
372
+ }
373
+ // Decode a live paged `instances` map to the flat `key -> { type, params }`
374
+ // lookup. `toJSON` materialises the page containers to the plain shape the
375
+ // shared `flattenPagedLookup` consumes, so page flattening lives in one place.
376
+ // Only ever called on the handle's paged path (guarded by `_isPaged()` /
377
+ // `_pageCount`), so the store is known paged. Pass `paged: true` rather than
378
+ // re-deriving it from a tag scan.
379
+ function flattenPagedMap(m) {
380
+ return flattenPagedLookup(m.toJSON(), true);
381
+ }
382
+ // Resolve a single record without flattening the whole store: hash the key
383
+ // to its page, decode only that page, find the triple. Keeps a per-key `get`
384
+ // proportional to one page, not the whole file.
385
+ function recordFromPage(m, recordKey, pageCount) {
386
+ const page = pageValue(m, pageKeyFor(recordKey, pageCount));
387
+ if (peekRecordTag(page) !== PAGE_TAG)
388
+ return undefined;
389
+ for (const packed of decodePageBody(decodeRecordValue(page)?.body)) {
390
+ if (Array.isArray(packed) && packed[0] === recordKey) {
391
+ return { [TAG_FIELD]: packed[1], [BODY_FIELD]: packed[2] };
392
+ }
393
+ }
394
+ return undefined;
395
+ }
396
+ // ---- Change deltas (`{ delta: true }` map subscribers) ----------------
397
+ //
398
+ // A delta subscriber reports which keys changed since the previous fire so a
399
+ // render loop patches O(changed) rather than re-reading the container.
400
+ //
401
+ // Unpaged: read it off Loro's diff. A value reassign / add / delete shows on
402
+ // the map's own diff (`target === m.id`); a field edit fires on the record's
403
+ // nested container, so the record's key is the first path segment under root.
404
+ //
405
+ // Paged: a record edit rewrites the whole page container, so the diff names
406
+ // the changed PAGE. Recover record granularity with a per-subscription
407
+ // page -> keys index, diffing a changed page's current keys against it; a
408
+ // deleted record was in the prior key set and absent from the rewritten page.
409
+ // The root key of a root container (`cid:root-<key>:Map`), else null. A field
410
+ // edit is keyed by its first path segment under root, which only a root
411
+ // record map can resolve; nested handles fall back to direct keys only.
412
+ function rootKeyOf(m) {
413
+ const match = /^cid:root-(.+):Map$/.exec(m.id);
414
+ return match?.[1] ?? null;
415
+ }
416
+ // The record keys currently packed into one page, or null for a missing /
417
+ // non-page entry. Seeds and advances the page index for delete detection.
418
+ function pageRecordKeys(m, pageKey) {
419
+ const records = pageRecords(m, pageKey);
420
+ return records ? new Set(records.keys()) : null;
421
+ }
422
+ // Decode one page to its `key -> { type, params }` records in a single pass,
423
+ // so a paged delta gets the changed records' VALUES from the same decode it
424
+ // uses to find the changed keys (a second per-key decode would make a large
425
+ // initial build O(records x page-size)).
426
+ function pageRecords(m, pageKey) {
427
+ const page = pageValue(m, pageKey);
428
+ if (peekRecordTag(page) !== PAGE_TAG)
429
+ return null;
430
+ const records = new Map();
431
+ for (const packed of decodePageBody(decodeRecordValue(page)?.body)) {
432
+ if (Array.isArray(packed) && typeof packed[0] === 'string') {
433
+ records.set(packed[0], { [TAG_FIELD]: packed[1], [BODY_FIELD]: packed[2] });
434
+ }
435
+ }
436
+ return records;
437
+ }
438
+ // Materialise one unpaged map entry to a plain value (containers via
439
+ // `toJSON`, scalars passthrough), so a delta carries the changed value
440
+ // without a re-read.
441
+ function entryValue(m, key) {
442
+ const v = m.get(key);
443
+ if (v instanceof LoroMap || v instanceof LoroList)
444
+ return v.toJSON();
445
+ return v;
446
+ }
447
+ function buildPageIndex(m) {
448
+ const index = new Map();
449
+ for (const pageKey of m.keys()) {
450
+ const keys = pageRecordKeys(m, pageKey);
451
+ if (keys)
452
+ index.set(pageKey, keys);
453
+ }
454
+ return index;
455
+ }
456
+ function unpagedMapDelta(m, rootKey, batch) {
457
+ const setKeys = new Set();
458
+ const deleted = new Set();
459
+ for (const ev of batch.events) {
460
+ if (ev.target === m.id) {
461
+ if (ev.diff.type !== 'map')
462
+ continue;
463
+ for (const [key, value] of Object.entries(ev.diff.updated)) {
464
+ if (value === undefined)
465
+ deleted.add(key);
466
+ else
467
+ setKeys.add(key);
468
+ }
469
+ }
470
+ else if (rootKey !== null) {
471
+ // A descendant event (a field edit on a record's nested container).
472
+ // Every batch event is within the root map's subtree, so the record's
473
+ // key is the path segment directly under root.
474
+ const key = ev.path[1];
475
+ if (typeof key === 'string')
476
+ setKeys.add(key);
477
+ }
478
+ }
479
+ for (const key of deleted)
480
+ setKeys.delete(key);
481
+ const set = new Map();
482
+ for (const key of setKeys)
483
+ set.set(key, entryValue(m, key));
484
+ return { set, deleted: [...deleted] };
485
+ }
486
+ function pagedMapDelta(m, batch, pageIndex) {
487
+ const set = new Map();
488
+ const deleted = new Set();
489
+ for (const ev of batch.events) {
490
+ if (ev.target !== m.id || ev.diff.type !== 'map')
491
+ continue;
492
+ for (const pageKey of Object.keys(ev.diff.updated)) {
493
+ const before = pageIndex.get(pageKey);
494
+ const after = pageRecords(m, pageKey);
495
+ if (after) {
496
+ for (const [key, value] of after)
497
+ set.set(key, value);
498
+ pageIndex.set(pageKey, new Set(after.keys()));
499
+ }
500
+ else {
501
+ pageIndex.delete(pageKey);
502
+ }
503
+ if (before) {
504
+ for (const key of before) {
505
+ if (!after || !after.has(key))
506
+ deleted.add(key);
507
+ }
508
+ }
509
+ }
510
+ }
511
+ for (const key of deleted)
512
+ set.delete(key);
513
+ return { set, deleted: [...deleted] };
514
+ }
515
+ // Run a flat-draft `mutate` against a PAGED store. The draft proxies an
516
+ // OVERLAY of in-batch changes (key -> record, or a tombstone for a delete);
517
+ // reads consult the overlay first then the stored page, so an edit is
518
+ // visible to later reads in the same `fn` as on an unpaged live proxy.
519
+ // Afterwards only the touched pages are decoded, rewritten, and re-encoded,
520
+ // so a single field edit rewrites a single page. A `pruneRefs` sweep that
521
+ // iterates the whole draft is the only path that reads every page, the rare
522
+ // structural-delete case already treated as O(file).
523
+ function mutatePaged(m, pageCount, fn) {
524
+ // A paged store always carries a positive `meta.page_count`, so a
525
+ // non-positive count means the store is corrupt: the page layout can't be
526
+ // resolved and guessing would scramble which record lives where. Refuse
527
+ // rather than corrupt.
528
+ if (pageCount <= 0) {
529
+ throw new Error('Cannot edit a paged record store with no page_count.');
530
+ }
531
+ const TOMBSTONE = Symbol('deleted');
532
+ // In-batch changes: key -> new record, or TOMBSTONE when deleted. Absent
533
+ // means "unchanged, read from the page".
534
+ const overlay = new Map();
535
+ // The record a draft read should see: the overlay value if touched this
536
+ // batch, else the stored record (one page decode).
537
+ const currentRecord = (key) => {
538
+ if (overlay.has(key)) {
539
+ const v = overlay.get(key);
540
+ return v === TOMBSTONE ? undefined : v;
541
+ }
542
+ return recordFromPage(m, key, pageCount);
543
+ };
544
+ // Per-record sub-proxy: a field assignment writes through to the overlay
545
+ // record so a later read sees it and the page rewrite picks it up. Field
546
+ // reads return the deep-plain stored value, so spreading `{ ...rec.params }`
547
+ // never leaks a live handle.
548
+ const recordProxy = (key) => {
549
+ return new Proxy({}, {
550
+ get(_t, field) {
551
+ if (typeof field === 'symbol')
552
+ return undefined;
553
+ const rec = currentRecord(key);
554
+ return rec ? rec[field] : undefined;
555
+ },
556
+ set(_t, field, value) {
557
+ if (typeof field === 'symbol')
558
+ return false;
559
+ const base = { ...currentRecord(key) };
560
+ base[field] = toPlainDeep(value);
561
+ overlay.set(key, base);
562
+ return true;
563
+ },
564
+ });
565
+ };
566
+ const draft = new Proxy({}, {
567
+ get(_t, key) {
568
+ if (typeof key === 'symbol')
569
+ return undefined;
570
+ if (currentRecord(key) === undefined)
571
+ return undefined;
572
+ return recordProxy(key);
573
+ },
574
+ set(_t, key, value) {
575
+ if (typeof key === 'symbol')
576
+ return false;
577
+ overlay.set(key, toPlainDeep(value));
578
+ return true;
579
+ },
580
+ deleteProperty(_t, key) {
581
+ if (typeof key === 'symbol')
582
+ return false;
583
+ overlay.set(key, TOMBSTONE);
584
+ return true;
585
+ },
586
+ has(_t, key) {
587
+ if (typeof key === 'symbol')
588
+ return false;
589
+ return currentRecord(key) !== undefined;
590
+ },
591
+ ownKeys() {
592
+ // Stored keys plus added, minus deleted.
593
+ const keys = new Set(Object.keys(flattenPagedMap(m)));
594
+ for (const [k, v] of overlay) {
595
+ if (v === TOMBSTONE)
596
+ keys.delete(k);
597
+ else
598
+ keys.add(k);
599
+ }
600
+ return [...keys];
601
+ },
602
+ getOwnPropertyDescriptor(_t, key) {
603
+ if (typeof key === 'symbol')
604
+ return undefined;
605
+ if (currentRecord(key) === undefined)
606
+ return undefined;
607
+ return { enumerable: true, configurable: true, writable: true };
608
+ },
609
+ });
610
+ fn(draft);
611
+ if (overlay.size === 0)
612
+ return;
613
+ applyOverlay(m, pageCount, overlay, TOMBSTONE);
614
+ }
615
+ // Rewrite the pages the overlay touched: each is decoded once, untouched
616
+ // records carried over, changed records re-stated (or dropped on a
617
+ // tombstone), then re-encoded (or deleted if it ends empty).
618
+ function applyOverlay(m, pageCount, overlay, tombstone) {
619
+ const touchedPages = new Set();
620
+ for (const key of overlay.keys()) {
621
+ touchedPages.add(pageKeyFor(key, pageCount));
622
+ }
623
+ for (const pageKey of touchedPages) {
624
+ const existing = pageValue(m, pageKey);
625
+ const triples = [];
626
+ // Carry over the page's untouched records.
627
+ if (peekRecordTag(existing) === PAGE_TAG) {
628
+ for (const packed of decodePageBody(decodeRecordValue(existing)?.body)) {
629
+ if (Array.isArray(packed) &&
630
+ typeof packed[0] === 'string' &&
631
+ !overlay.has(packed[0])) {
632
+ triples.push(packed);
633
+ }
634
+ }
635
+ }
636
+ // Add the overlay's survivors that hash to this page.
637
+ for (const [key, rec] of overlay) {
638
+ if (rec === tombstone)
639
+ continue;
640
+ if (pageKeyFor(key, pageCount) !== pageKey)
641
+ continue;
642
+ const r = rec;
643
+ triples.push([key, String(r[TAG_FIELD] ?? ''), r[BODY_FIELD]]);
644
+ }
645
+ if (triples.length === 0) {
646
+ if (existing !== undefined)
647
+ m.delete(pageKey);
648
+ continue;
649
+ }
650
+ const page = m.setContainer(pageKey, new LoroMap());
651
+ page.set(TAG_FIELD, PAGE_TAG);
652
+ page.set(BODY_FIELD, encodePageBody(triples));
653
+ }
654
+ }
655
+ // A root anchor holds one container that a version checkout swaps in place
656
+ // (`rebind`). Between rebinds `resolve()` returns the same instance, so the
657
+ // per-container Loro subscription attaches to a stable identity. `LoroMap` and
658
+ // `LoroList` roots share this; the kind is the type parameter.
659
+ function rootAnchor(root) {
660
+ let current = root;
661
+ return {
662
+ resolve: () => current,
663
+ resolveOrCreate: () => current,
664
+ invalidate: () => { },
665
+ rebind: (next) => {
666
+ current = next;
667
+ },
668
+ };
669
+ }
670
+ // A child anchor resolves `parent[key]` lazily, then caches the container so
671
+ // per-container subscribers attach to a stable instance. `ctor` selects the
672
+ // container kind: a value of the wrong kind at the key reads as absent, and
673
+ // `resolveOrCreate` mints one only inside a mutate context.
674
+ function childAnchor(parent, key, ctor) {
675
+ // `invalidate` clears the cache on a source swap so the next resolve
676
+ // re-derives through the (rebound) parent.
677
+ let cached = null;
678
+ // A cached container is stale once it has been detached from the document
679
+ // (deleted from `parent[key]`), which happens on a delete-then-recreate of
680
+ // the key: the recreate mints a NEW container, so a cache holding the old
681
+ // one would read and subscribe against a dead container. `isDeleted()` is
682
+ // the cheap per-resolve check that keeps the cache without going stale;
683
+ // `invalidate()` still handles the source swap (checkout / resync).
684
+ const live = (c) => c !== null && !c.isDeleted();
685
+ return {
686
+ resolve: () => {
687
+ if (live(cached))
688
+ return cached;
689
+ cached = null;
690
+ const p = parent.resolve();
691
+ if (!p)
692
+ return null;
693
+ const v = p.get(key);
694
+ if (v instanceof ctor) {
695
+ cached = v;
696
+ return cached;
697
+ }
698
+ return null;
699
+ },
700
+ resolveOrCreate: () => {
701
+ if (live(cached))
702
+ return cached;
703
+ cached = null;
704
+ const p = parent.resolveOrCreate();
705
+ const v = p.get(key);
706
+ if (v instanceof ctor) {
707
+ cached = v;
708
+ return cached;
709
+ }
710
+ const created = p.setContainer(key, new ctor());
711
+ cached = created;
712
+ return created;
713
+ },
714
+ invalidate: () => {
715
+ cached = null;
716
+ },
717
+ };
718
+ }
719
+ // Shared per-container subscription bookkeeping for both handle kinds. Holds
720
+ // the active { callback, detach } records so a version checkout can re-fire
721
+ // and re-attach each against the swapped source, and a suspend / close can
722
+ // detach them all. Each kind passes in its own whole-container fire and its
723
+ // per-container change-attach; this owns only the record set and its
724
+ // lifecycle. (The `createEmitter` primitive is a value-fanout with retain /
725
+ // dedup, a different shape: it models no per-listener external resource, and
726
+ // the Loro subscription each record must re-create on rebind is exactly
727
+ // that.)
728
+ class SubscriptionRegistry {
729
+ _subs = new Set();
730
+ // Fire the whole-container catch-up once, attach the per-container change
731
+ // subscription, track the record, and return an idempotent unsubscribe.
732
+ subscribe(cb, fireWhole, attach) {
733
+ fireWhole(cb);
734
+ const rec = { cb, detach: attach(cb) };
735
+ this._subs.add(rec);
736
+ return () => {
737
+ rec.detach();
738
+ this._subs.delete(rec);
739
+ };
740
+ }
741
+ // Re-fire every active subscriber with the whole current container and
742
+ // re-attach its change subscription to it (checkout / return-to-head).
743
+ resync(fireWhole, attach) {
744
+ for (const rec of this._subs) {
745
+ rec.detach();
746
+ fireWhole(rec.cb);
747
+ rec.detach = attach(rec.cb);
748
+ }
749
+ }
750
+ // Detach every active subscription and forget it (suspend / close).
751
+ teardown() {
752
+ for (const rec of this._subs)
753
+ rec.detach();
754
+ this._subs.clear();
755
+ }
756
+ }
757
+ class MapHandleImpl {
758
+ _anchor;
759
+ _owner;
760
+ // The store's page count (0 when unpaged). `null` for handles that can
761
+ // never be a record store's `instances` root (composed child handles, and
762
+ // doc-less roots), which skip the paged paths entirely.
763
+ _pageCount;
764
+ // The paging scheme of the record store this root maps, read the same way
765
+ // and at the same time as `_pageCount` (both off the active source's `meta`).
766
+ // `null` for any handle that is never a record store's `instances` root.
767
+ _pagingScheme;
768
+ _childMaps = new Map();
769
+ _childLists = new Map();
770
+ // Active subscriptions, tracked so a version checkout (which swaps the
771
+ // active source doc) can re-fire and re-attach each one against the new
772
+ // container.
773
+ _registry = new SubscriptionRegistry();
774
+ // Keys captured just before a rebind (`_rebindAnchor`), consumed by the
775
+ // matching `_resync` to report the keys the source swap removed. Null
776
+ // outside a rebind cycle, so a bare `_resync` reports no removals.
777
+ _priorKeys = null;
778
+ constructor(anchor, owner, pageCount = null, pagingScheme = null) {
779
+ this._anchor = anchor;
780
+ this._owner = owner;
781
+ this._pageCount = pageCount;
782
+ this._pagingScheme = pagingScheme;
783
+ }
784
+ // Whether the map is a PAGED record store. Defined by `meta.page_count`
785
+ // (set whenever the writer pages, never changed by a field edit), threaded
786
+ // in as `_pageCount` and read in O(1). MUST stay O(1): it is on the hot
787
+ // path of every `read` / `mutate` (twice per ordered-map edit), so scanning
788
+ // the map to peek tags would make a single field edit O(file).
789
+ _isPaged() {
790
+ return this._pageCount !== null && this._pageCount() > 0;
791
+ }
792
+ // Whether this build's page-key hash matches the scheme the store was
793
+ // written under. A per-key `get` hashes the key to its page (`pageKeyFor`),
794
+ // so a store written under a NEWER scheme (one that changed the hash) would
795
+ // be looked up in the wrong page and read back as absent. When this is
796
+ // false, the per-key read paths fall back to the hash-independent whole-store
797
+ // flatten (O(file), only on this rare newer-scheme read) so `get` / `has`
798
+ // agree with `read()` instead of silently reporting present records missing.
799
+ // The edit path refuses such a store loudly (`_runMutate`); reads stay
800
+ // best-effort. Absent / current-or-older scheme is a match.
801
+ _pageHashMatchesStore() {
802
+ return !this._pagingScheme || isPagingSchemeEditable(this._pagingScheme());
803
+ }
804
+ read() {
805
+ const m = this._anchor.resolve();
806
+ if (!m)
807
+ return {};
808
+ // A paged map flattens back to the flat lookup, so a read is identical
809
+ // paged or not.
810
+ if (this._isPaged())
811
+ return flattenPagedMap(m);
812
+ return m.toJSON();
813
+ }
814
+ get(entryKey) {
815
+ const m = this._anchor.resolve();
816
+ if (!m)
817
+ return undefined;
818
+ if (this._isPaged()) {
819
+ // A newer-scheme store hashes keys differently, so a per-page lookup
820
+ // would miss present records; flatten instead (see `_pageHashMatchesStore`).
821
+ if (!this._pageHashMatchesStore()) {
822
+ return flattenPagedMap(m)[entryKey];
823
+ }
824
+ // Decode only the key's page, so a per-key `get` stays proportional to
825
+ // one page.
826
+ return recordFromPage(m, entryKey, this._pageCount());
827
+ }
828
+ const v = m.get(entryKey);
829
+ if (v === undefined)
830
+ return undefined;
831
+ if (v instanceof LoroMap)
832
+ return v.toJSON();
833
+ if (v instanceof LoroList)
834
+ return v.toJSON();
835
+ return v;
836
+ }
837
+ keys() {
838
+ const m = this._anchor.resolve();
839
+ if (!m)
840
+ return [];
841
+ if (this._isPaged())
842
+ return Object.keys(flattenPagedMap(m));
843
+ return m.keys();
844
+ }
845
+ has(entryKey) {
846
+ const m = this._anchor.resolve();
847
+ if (!m)
848
+ return false;
849
+ if (this._isPaged())
850
+ return this.get(entryKey) !== undefined;
851
+ return m.get(entryKey) !== undefined;
852
+ }
853
+ mutate(fn) {
854
+ this._owner.assertWritable();
855
+ this._owner.ensureLocalUpdatesSubscribed();
856
+ // Commits as it goes past a size budget; see `mutate-batching.ts`.
857
+ this._runMutate(fn, new MutateBatcher(() => {
858
+ this._owner.commitMutate();
859
+ }));
860
+ this._owner.commitMutate();
861
+ }
862
+ // Run a mutate WITHOUT the commit pipeline (the caller owns
863
+ // assertWritable / ensureLocalUpdatesSubscribed / commit). The seam the
864
+ // ordered-map primitive drives so a map edit and its sibling order-list
865
+ // edit land in ONE commit (one undo step).
866
+ _runMutate(fn, batcher) {
867
+ const m = this._anchor.resolveOrCreate();
868
+ if (this._isPaged()) {
869
+ // A store written under a paging scheme this build does not implement
870
+ // would be mis-hashed by `pageKeyFor`, so an in-place edit would write a
871
+ // record into the wrong page. Refuse loudly rather than corrupt it; the
872
+ // read path stays best-effort (only a persisted edit can corrupt).
873
+ if (this._pagingScheme && !isPagingSchemeEditable(this._pagingScheme())) {
874
+ throw new BrassError('This document was saved in a newer storage format. Update this app to the latest @brass-build/client to edit it.', { status: 426 });
875
+ }
876
+ mutatePaged(m, this._pageCount(), fn);
877
+ return;
878
+ }
879
+ const proxy = wrapLoroMapProxy(m, batcher);
880
+ fn(proxy);
881
+ }
882
+ // Paged writes defer to `applyOverlay` at the end of the mutation, so an
883
+ // intermediate commit there would seal no records (and, on an ordered
884
+ // map, order entries for records not yet written). Their ops are bounded
885
+ // by page size anyway.
886
+ _canBatchMutate() {
887
+ return !this._isPaged();
888
+ }
889
+ // A predicate over the map's CURRENT stored key set, resolved ONCE so a
890
+ // caller checking many keys doesn't re-scan per key. The ordered-map
891
+ // primitive uses it to decide, per set, whether a key is new (and so must
892
+ // be appended to the order list) without paying O(map) per added key.
893
+ _existsChecker() {
894
+ const m = this._anchor.resolve();
895
+ if (!m)
896
+ return () => false;
897
+ if (this._isPaged()) {
898
+ // Newer-scheme store: the per-page hash would miss records, so resolve
899
+ // the whole flattened key set once (see `_pageHashMatchesStore`).
900
+ if (!this._pageHashMatchesStore()) {
901
+ const flat = flattenPagedMap(m);
902
+ return (key) => Object.prototype.hasOwnProperty.call(flat, key);
903
+ }
904
+ const pageCount = this._pageCount();
905
+ return (key) => recordFromPage(m, key, pageCount) !== undefined;
906
+ }
907
+ return (key) => m.get(key) !== undefined;
908
+ }
909
+ // Fire a subscriber once with the whole map as `change.set`, so a
910
+ // subscriber is ONE loop with no initial-vs-update branch and no
911
+ // `if (value)` guard (the footgun that left a view stale after an undo /
912
+ // remote delete emptied the container). Flagged `resync: true` and carries
913
+ // no `origin`: this is a whole-container catch-up (registration, or a
914
+ // checkout / return swapping the source), not an authored transition.
915
+ //
916
+ // `deleted` is empty for a registration fire (the subscriber holds no prior
917
+ // state). On a resync it carries the keys that were present before the
918
+ // source swap but are gone after it: a checkout to a version with fewer
919
+ // records must tell a delta-reconcile subscriber to drop those keys, or they
920
+ // linger as phantom records in the preview. `_resync` computes it.
921
+ _fireWhole(cb, deleted = []) {
922
+ try {
923
+ cb({ set: new Map(Object.entries(this.read())), deleted }, undefined, true);
924
+ }
925
+ catch {
926
+ // A throwing subscriber must not break registration / resync.
927
+ }
928
+ }
929
+ // Attach the per-container change subscription for `cb` to the CURRENT
930
+ // resolved container, returning its detach. The container instance is
931
+ // captured here (Loro's subscribe is per-container), so a source swap
932
+ // re-runs this against the new container.
933
+ _attachChange(cb) {
934
+ const m = this._anchor.resolve();
935
+ // If the container doesn't exist yet (a composed handle for a missing
936
+ // entry), only the whole-fire happens; the change subscription attaches
937
+ // once a later `doc.map(key)` / `mutate` materializes it. Root handles
938
+ // are always materialized (`getMap` is get-or-create).
939
+ if (!m)
940
+ return () => { };
941
+ // Paged delta needs prior page state to spot deleted records, so index
942
+ // the pages once now; the index advances on every fire or its next delete
943
+ // diff is wrong. Unpaged maps read their delta off the Loro diff.
944
+ const pageIndex = this._isPaged() ? buildPageIndex(m) : null;
945
+ const rootKey = rootKeyOf(m);
946
+ const sub = m.subscribe((event) => {
947
+ const change = (pageIndex
948
+ ? pagedMapDelta(m, event, pageIndex)
949
+ : unpagedMapDelta(m, rootKey, event));
950
+ const origin = { source: this._owner.originOf(event) };
951
+ try {
952
+ cb(change, origin, false);
953
+ }
954
+ catch {
955
+ // A throwing subscriber must not affect the others.
956
+ }
957
+ });
958
+ return () => sub();
959
+ }
960
+ subscribe(cb) {
961
+ // App subscribers see `(change, origin?)`; the internal re-sync flag is
962
+ // dropped here so it never leaks into the public surface.
963
+ return this._subscribeInternal((change, origin) => cb(change, origin));
964
+ }
965
+ // Internal: like `subscribe`, but the callback also receives whether the
966
+ // fire is a whole-container re-sync. Only the ordered-map wrapper uses it,
967
+ // to re-check `instance_order` on a source swap without inspecting `origin`.
968
+ _subscribeInternal(cb) {
969
+ return this._registry.subscribe(cb, (c) => this._fireWhole(c), (c) => this._attachChange(c));
970
+ }
971
+ // Re-point this ROOT handle to `next` (a container in the newly-active
972
+ // source doc) WITHOUT firing yet. Called by the Document on a version
973
+ // checkout / return-to-head. Snapshots the subtree's current keys first,
974
+ // before the anchor moves, so the subsequent `_resync` can report which keys
975
+ // the swap removed. Rebind and resync are two phases (see
976
+ // `_rebindRootHandles`) because the ordered map reads its sibling order list
977
+ // while firing its map: both anchors must already point at the new source
978
+ // when either fires, or the map redraw reads a half-swapped order list.
979
+ _rebindAnchor(next) {
980
+ this._snapshotPriorKeys();
981
+ this._anchor.rebind(next);
982
+ }
983
+ // Capture the current key set of this handle and its composed child maps,
984
+ // read against the CURRENT (pre-rebind) source, so `_resync` can diff it
985
+ // against the post-rebind source to find removed keys. Runs before the
986
+ // anchor moves; child maps resolve through this anchor, so they must be read
987
+ // now too.
988
+ _snapshotPriorKeys() {
989
+ this._priorKeys = new Set(Object.keys(this.read()));
990
+ for (const child of this._childMaps.values())
991
+ child._snapshotPriorKeys();
992
+ }
993
+ // Re-fire every active subscriber with the whole current container (plus the
994
+ // keys the source swap removed, from the pre-rebind snapshot) and re-attach
995
+ // its change subscription to it, then recurse into composed child handles
996
+ // (their anchors resolve through this one, so they already point at the new
997
+ // source; their subscriptions still need re-attaching). Idempotent and safe
998
+ // when there are no subscribers.
999
+ _resync() {
1000
+ // Re-derive this handle's container from the (already-rebound) parent
1001
+ // before reading or re-attaching: a child anchor caches its resolved
1002
+ // container, and after a source swap that cache points at the old source.
1003
+ // Prior keys were snapshotted pre-rebind, so this cannot lose them.
1004
+ this._anchor.invalidate();
1005
+ const prior = this._priorKeys;
1006
+ this._priorKeys = null;
1007
+ const currentKeys = new Set(Object.keys(this.read()));
1008
+ const removed = prior
1009
+ ? [...prior].filter((k) => !currentKeys.has(k))
1010
+ : [];
1011
+ this._registry.resync((c) => this._fireWhole(c, removed), (c) => this._attachChange(c));
1012
+ for (const child of this._childMaps.values())
1013
+ child._resync();
1014
+ for (const child of this._childLists.values())
1015
+ child._resync();
1016
+ }
1017
+ // Detach every active per-container subscription (this handle's and its
1018
+ // composed children's) and forget them. The Document calls this when it
1019
+ // suspends (parks) or closes: a parked doc keeps applying remote ops to its
1020
+ // live containers, so a Loro subscription an app registered before `close()`
1021
+ // would otherwise keep firing the app's callback on a doc it has closed.
1022
+ _teardownSubs() {
1023
+ this._registry.teardown();
1024
+ for (const child of this._childMaps.values())
1025
+ child._teardownSubs();
1026
+ for (const child of this._childLists.values())
1027
+ child._teardownSubs();
1028
+ }
1029
+ map(entryKey) {
1030
+ let h = this._childMaps.get(entryKey);
1031
+ if (!h) {
1032
+ const anchor = childAnchor(this._anchor, entryKey, LoroMap);
1033
+ h = new MapHandleImpl(anchor, this._owner);
1034
+ this._childMaps.set(entryKey, h);
1035
+ }
1036
+ return h;
1037
+ }
1038
+ list(entryKey) {
1039
+ let h = this._childLists.get(entryKey);
1040
+ if (!h) {
1041
+ const anchor = childAnchor(this._anchor, entryKey, LoroList);
1042
+ h = new ListHandleImpl(anchor, this._owner);
1043
+ this._childLists.set(entryKey, h);
1044
+ }
1045
+ return h;
1046
+ }
1047
+ }
1048
+ class ListHandleImpl {
1049
+ _anchor;
1050
+ _owner;
1051
+ // Active subscriptions, tracked so a version checkout can re-fire and
1052
+ // re-attach each one against the new container.
1053
+ _registry = new SubscriptionRegistry();
1054
+ constructor(anchor, owner) {
1055
+ this._anchor = anchor;
1056
+ this._owner = owner;
1057
+ }
1058
+ read() {
1059
+ const l = this._anchor.resolve();
1060
+ if (!l)
1061
+ return [];
1062
+ return l.toJSON();
1063
+ }
1064
+ length() {
1065
+ const l = this._anchor.resolve();
1066
+ return l ? l.length : 0;
1067
+ }
1068
+ mutate(fn) {
1069
+ this._owner.assertWritable();
1070
+ this._owner.ensureLocalUpdatesSubscribed();
1071
+ this._runMutate(fn);
1072
+ this._owner.commitMutate();
1073
+ }
1074
+ // Run a list mutate without the commit pipeline; see
1075
+ // `MapHandleImpl._runMutate`. The ordered-map primitive drives this to
1076
+ // reconcile the order list in the same commit as the map edit.
1077
+ _runMutate(fn) {
1078
+ const l = this._anchor.resolveOrCreate();
1079
+ const proxy = wrapLoroListProxy(l);
1080
+ fn(proxy);
1081
+ }
1082
+ // Fire once (positional lists have no per-key delta, so a fire is a signal
1083
+ // to re-read). Used for registration and for a checkout / return resync.
1084
+ _fireWhole(cb) {
1085
+ try {
1086
+ cb();
1087
+ }
1088
+ catch {
1089
+ // A throwing subscriber must not break registration / resync.
1090
+ }
1091
+ }
1092
+ // Attach the per-container change subscription for `cb` to the CURRENT
1093
+ // resolved container, returning its detach.
1094
+ _attachChange(cb) {
1095
+ // Same caveat as MapHandleImpl: the change subscription needs the
1096
+ // container to exist. Root lists are materialized by `doc.list(key)`;
1097
+ // nested lists need a prior mutate.
1098
+ const l = this._anchor.resolve();
1099
+ if (!l)
1100
+ return () => { };
1101
+ const sub = l.subscribe(() => this._fireWhole(cb));
1102
+ return () => sub();
1103
+ }
1104
+ subscribe(cb) {
1105
+ return this._registry.subscribe(cb, (c) => this._fireWhole(c), (c) => this._attachChange(c));
1106
+ }
1107
+ // Re-point this ROOT list handle to `next` without firing (see
1108
+ // MapHandleImpl._rebindAnchor). Lists carry no per-key delta, so there is
1109
+ // no prior-key snapshot to take; the resync fire is a re-read signal.
1110
+ _rebindAnchor(next) {
1111
+ this._anchor.rebind(next);
1112
+ }
1113
+ // Re-fire + re-attach every active subscriber against the current
1114
+ // container (see MapHandleImpl._resync). Lists have no composed children,
1115
+ // but a child list handle (`doc.map(k).list(j)`) has a cached anchor that a
1116
+ // source swap leaves pointing at the old source, so invalidate it first.
1117
+ _resync() {
1118
+ this._anchor.invalidate();
1119
+ this._registry.resync((c) => this._fireWhole(c), (c) => this._attachChange(c));
1120
+ }
1121
+ // Detach every active subscription and forget it (see
1122
+ // MapHandleImpl._teardownSubs). Lists have no composed children.
1123
+ _teardownSubs() {
1124
+ this._registry.teardown();
1125
+ }
1126
+ }
1127
+ // The ordered map composes a map handle and a list handle and keeps them in
1128
+ // lockstep. It reuses the concrete impls (not the interfaces) to drive their
1129
+ // no-commit `_runMutate` seams and the map's `_existsChecker`, so both halves
1130
+ // of a structural edit commit once. Built over the SAME handles
1131
+ // `doc.map(mapKey)` / `doc.list(orderKey)` return, so subscriptions stay
1132
+ // consistent.
1133
+ class OrderedMapHandleImpl {
1134
+ _map;
1135
+ _order;
1136
+ _owner;
1137
+ constructor(_map, _order, _owner) {
1138
+ this._map = _map;
1139
+ this._order = _order;
1140
+ this._owner = _owner;
1141
+ }
1142
+ read() {
1143
+ return this._map.read();
1144
+ }
1145
+ get(entryKey) {
1146
+ return this._map.get(entryKey);
1147
+ }
1148
+ has(entryKey) {
1149
+ return this._map.has(entryKey);
1150
+ }
1151
+ keys() {
1152
+ // Filter to keys still in the map: the primitive keeps the two in step,
1153
+ // but a hand-written or stale order list must not surface a key the map
1154
+ // no longer has. Read the map ONCE (its bulk snapshot) and test
1155
+ // membership against it, the same shape `ordered()` uses. A per-key
1156
+ // `_existsChecker` predicate would re-decode a paged store's page once
1157
+ // per record it holds (one gunzip per key, ~page-size times per page),
1158
+ // and `subscribe` calls this on every structural move, so a single edit
1159
+ // on a large paged doc would cost O(records) page decodes instead of
1160
+ // O(pages).
1161
+ const records = this._map.read();
1162
+ return this._order
1163
+ .read()
1164
+ .filter((k) => Object.prototype.hasOwnProperty.call(records, k));
1165
+ }
1166
+ ordered() {
1167
+ const records = this._map.read();
1168
+ const out = [];
1169
+ for (const key of this._order.read()) {
1170
+ if (Object.prototype.hasOwnProperty.call(records, key)) {
1171
+ out.push({ key, value: records[key] });
1172
+ }
1173
+ }
1174
+ return out;
1175
+ }
1176
+ mutate(fn) {
1177
+ this._owner.assertWritable();
1178
+ this._owner.ensureLocalUpdatesSubscribed();
1179
+ // Net structural effect: keys added (`added` iterates in insertion
1180
+ // order, and a delete-then-re-set re-appends, matching the order list's
1181
+ // append semantics) and removed. A field edit routes through the inner
1182
+ // record sub-proxy and never trips the top-level set / delete traps, so
1183
+ // it leaves the order list alone.
1184
+ const added = new Set();
1185
+ const removed = new Set();
1186
+ // Run at each batch boundary, so a commit carries the map writes AND
1187
+ // the order entries for those same records. Left to the end instead,
1188
+ // the final commit carries every key at once: measured at 229 KB for a
1189
+ // 20,000-record write, an oversized op of its own.
1190
+ const reconcileOrder = () => {
1191
+ if (added.size === 0 && removed.size === 0)
1192
+ return;
1193
+ this._order._runMutate((arr) => {
1194
+ if (removed.size > 0) {
1195
+ const kept = arr.filter((k) => !removed.has(k));
1196
+ arr.length = 0;
1197
+ for (const k of kept)
1198
+ arr.push(k);
1199
+ }
1200
+ for (const k of added)
1201
+ arr.push(k);
1202
+ });
1203
+ added.clear();
1204
+ removed.clear();
1205
+ };
1206
+ const batcher = this._map._canBatchMutate()
1207
+ ? new MutateBatcher(() => {
1208
+ reconcileOrder();
1209
+ this._owner.commitMutate();
1210
+ })
1211
+ : null;
1212
+ this._map._runMutate((inner) => {
1213
+ // "is this key new" (and so must be appended to the order list) is O(1)
1214
+ // per set; a repeat set of the same new key is guarded by `added`.
1215
+ // `exists` reflects the key set at the start of the batch for a paged
1216
+ // store (its writes are deferred to `applyOverlay`), but an UNPAGED
1217
+ // proxy writes through to the live map immediately, so a key deleted
1218
+ // earlier in this same batch reads as absent from `exists`. `removed`
1219
+ // records exactly those keys, so "present before the batch" is
1220
+ // `exists(k) || removed.has(k)`; consulting both keeps a delete-then-
1221
+ // re-add of a pre-existing key from being mis-seen as a fresh add and
1222
+ // appended to the order list a second time.
1223
+ const exists = this._map._existsChecker();
1224
+ const tracking = new Proxy({}, {
1225
+ get: (_t, key) => typeof key === 'symbol'
1226
+ ? inner[key]
1227
+ : inner[key],
1228
+ set: (_t, key, value) => {
1229
+ if (typeof key === 'symbol')
1230
+ return false;
1231
+ const k = key;
1232
+ if (!added.has(k) && !exists(k) && !removed.has(k)) {
1233
+ added.add(k);
1234
+ }
1235
+ // A re-add after a delete in the same batch cancels the delete.
1236
+ removed.delete(k);
1237
+ inner[k] = value;
1238
+ return true;
1239
+ },
1240
+ deleteProperty: (_t, key) => {
1241
+ if (typeof key === 'symbol')
1242
+ return false;
1243
+ const k = key;
1244
+ if (added.has(k)) {
1245
+ // Added and removed in one batch: it never reached the order
1246
+ // list, so just forget the addition.
1247
+ added.delete(k);
1248
+ }
1249
+ else if (exists(k)) {
1250
+ // Only track a removal that actually drops a key present in the
1251
+ // order list before this batch. `exists(k)` reads the key set at
1252
+ // batch start (paged defers writes; an unpaged key untouched so
1253
+ // far this batch reads its start state), so it is true only for a
1254
+ // genuinely-present key. Deleting a key that was already absent
1255
+ // must NOT enter `removed`: a later re-`set` of that key in the
1256
+ // same batch consults `removed.has(k)` to decide "re-add of a
1257
+ // pre-existing key" (skip the order append) vs "fresh add"
1258
+ // (append). A spurious `removed` entry makes the fresh add look
1259
+ // like a re-add, so the key lands in the record map but never in
1260
+ // `instance_order`, a silent divergence between the two.
1261
+ removed.add(k);
1262
+ }
1263
+ delete inner[k];
1264
+ return true;
1265
+ },
1266
+ has: (_t, key) => typeof key === 'symbol'
1267
+ ? false
1268
+ : key in inner,
1269
+ ownKeys: () => Reflect.ownKeys(inner),
1270
+ getOwnPropertyDescriptor: (_t, key) => Object.getOwnPropertyDescriptor(inner, key),
1271
+ });
1272
+ fn(tracking);
1273
+ }, batcher ?? undefined);
1274
+ reconcileOrder();
1275
+ this._owner.commitMutate();
1276
+ }
1277
+ subscribe(cb) {
1278
+ // Content (`set` / `deleted`) rides the map subscription. `order` is
1279
+ // attached ONLY when the order moved; a field edit must not pay for a
1280
+ // re-sequence. A move is detected in O(changed) by testing changed keys
1281
+ // against a cached membership set; the ordered keys (O(order)) are read
1282
+ // only on a move. `known` starts empty so the initial fire reads as an
1283
+ // all-new move and carries the starting `order`.
1284
+ //
1285
+ // A `mutate` / remote delta only moves the order by changing the key set,
1286
+ // which the cheap membership test catches. The exception is a
1287
+ // whole-container re-sync (registration, or a checkout / return / hydrate
1288
+ // swapping the source): it can present the SAME key set in a different
1289
+ // `instance_order`, which the membership test cannot see. The fire tells
1290
+ // us which it is (`resync`) directly, so on a re-sync we read the actual
1291
+ // order (O(order), and these fires are rare) and treat only a genuine
1292
+ // reorder as a move rather than every re-sync (else an empty registration
1293
+ // and an unchanged-order checkout would both deliver a redundant `order`).
1294
+ const known = new Set();
1295
+ let lastOrder = [];
1296
+ const sameOrder = (a, b) => a.length === b.length && a.every((k, i) => k === b[i]);
1297
+ return this._map._subscribeInternal((change, origin, resync) => {
1298
+ let moved = change.deleted.length > 0;
1299
+ for (const key of change.set.keys())
1300
+ if (!known.has(key))
1301
+ moved = true;
1302
+ let orderKeys = null;
1303
+ if (!moved && resync) {
1304
+ orderKeys = this.keys();
1305
+ if (!sameOrder(orderKeys, lastOrder))
1306
+ moved = true;
1307
+ }
1308
+ if (!moved) {
1309
+ cb(change, origin);
1310
+ return;
1311
+ }
1312
+ for (const key of change.deleted)
1313
+ known.delete(key);
1314
+ for (const key of change.set.keys())
1315
+ known.add(key);
1316
+ const order = orderKeys ?? this.keys();
1317
+ lastOrder = order;
1318
+ cb({ set: change.set, deleted: change.deleted, order }, origin);
1319
+ });
1320
+ }
1321
+ }
1322
+ // Factory functions called by the Document. App code reaches handles via
1323
+ // `doc.map(...)` / `doc.list(...)`.
1324
+ // `pageCount`, when supplied, lets the root map present a PAGED record store
1325
+ // transparently as a flat record map. The Document supplies it for every root
1326
+ // map; a non-paged map never consults it, so passing it to non-record roots
1327
+ // is harmless. Omitted means the handle only serves the plain unpaged shape.
1328
+ export function createRootMapHandle(root, owner, pageCount, pagingScheme) {
1329
+ return new MapHandleImpl(rootAnchor(root), owner, pageCount ?? null, pagingScheme ?? null);
1330
+ }
1331
+ export function createRootListHandle(root, owner) {
1332
+ return new ListHandleImpl(rootAnchor(root), owner);
1333
+ }
1334
+ // Re-point a ROOT handle to a container in a newly-active source doc and
1335
+ // resync its subscribers, in two phases. The Document calls these on a version
1336
+ // checkout / return-to-head so the handles an app already holds (its
1337
+ // `doc.map(key)` / `doc.list(key)` / `doc.orderedMap(...)` references) follow
1338
+ // the source swap and redraw, instead of being discarded and silently
1339
+ // detached. The cast is safe because this module is the only producer of these
1340
+ // interfaces.
1341
+ //
1342
+ // The two phases must be run across ALL root handles as two passes (rebind
1343
+ // every anchor, THEN resync every handle), not handle-by-handle, because a
1344
+ // composed handle can read a SIBLING root while firing: the ordered map reads
1345
+ // its order list during its map redraw. If the order list's anchor were still
1346
+ // on the old source when the map fired, the redraw would read a half-swapped
1347
+ // pair and drop the keys the order list had not caught up on.
1348
+ export function rebindRootMapAnchor(handle, next) {
1349
+ handle._rebindAnchor(next);
1350
+ }
1351
+ export function rebindRootListAnchor(handle, next) {
1352
+ handle._rebindAnchor(next);
1353
+ }
1354
+ // Re-fire + re-attach a root handle's subscribers against its current
1355
+ // container. Both concrete impls expose `_resync`, so one generic reaches
1356
+ // either kind; the cast is safe because this module is the only producer of
1357
+ // these handles.
1358
+ export function resyncRootHandle(handle) {
1359
+ handle._resync();
1360
+ }
1361
+ // Detach a handle's live subscriptions (its own and its children's). The
1362
+ // Document calls this when it suspends (parks) or closes, so a subscription
1363
+ // registered before `close()` stops firing against the still-live containers
1364
+ // a parked doc keeps applying remote ops to. As with `resyncRootHandle`, one
1365
+ // generic reaches either kind.
1366
+ export function teardownRootHandle(handle) {
1367
+ handle._teardownSubs();
1368
+ }
1369
+ // Bind a map handle and a list handle into an ordered map. The cast to the
1370
+ // concrete impls is safe because this module is the only producer of those
1371
+ // interfaces, and it lets the ordered map drive their no-commit seams.
1372
+ export function createOrderedMapHandle(map, order, owner) {
1373
+ return new OrderedMapHandleImpl(map, order, owner);
1374
+ }
1375
+ //# sourceMappingURL=handles.js.map