@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
@@ -0,0 +1,486 @@
1
+ // The handle surface over a static record store.
2
+ //
3
+ // A document's encoding is not something an app is told about, so the roots of
4
+ // a static document are reached through the same `MapHandle` / `ListHandle` /
5
+ // `OrderedMapHandle` types a CRDT document's roots are: a projection built in
6
+ // a subscriber, a record resolved by key, a scoped handle onto one record's
7
+ // body. One read type over both encodings is what makes the encoding the
8
+ // platform's decision rather than the app's, so these implement the published
9
+ // interfaces rather than a read-only cousin of them.
10
+ //
11
+ // What they do not carry is a write. A record set holds no history for an edit
12
+ // to become, and building the CRDT that would hold one is the platform's
13
+ // (`runRecordSetConversion`), so a document served as a record set is one the
14
+ // reader asked to read. The write methods the interfaces declare therefore
15
+ // refuse through the document's own read-only gate, which names the reader's
16
+ // actual limit rather than the encoding.
17
+ //
18
+ // Reads stay as lazy as the store is. A keyed read decodes the one page that
19
+ // holds the record; `keys()` costs the pages' key lists; only a subscriber,
20
+ // which is asking for the whole collection by definition, walks every page.
21
+
22
+ import type {
23
+ Change,
24
+ HandleUnsubscribe,
25
+ TextHandle,
26
+ TextSubscriber,
27
+ ListHandle,
28
+ ListSubscriber,
29
+ MapHandle,
30
+ MapSubscriber,
31
+ OrderedMapHandle,
32
+ Splice,
33
+ } from './handle-types.js';
34
+ import { pushBoundedSplice } from './subscriber-delivery.js';
35
+ import { SealedStore, type SealedContent, type SealedMapReader } from './sealed-store.js';
36
+
37
+ /**
38
+ * How a write through one of these handles is refused.
39
+ *
40
+ * The document's own gate, passed in rather than restated here, so the
41
+ * refusal names what the reader is actually up against (a snapshot they asked
42
+ * for, a view-only grant, a link) instead of describing how the platform
43
+ * happens to store the content. It always throws for a document served as a
44
+ * record set, because such a document is opened read-only; the throw that
45
+ * follows a gate that did not is the invariant stated where it is relied on.
46
+ */
47
+ type RefuseWrite = () => void;
48
+
49
+ function refuse(assertWritable: RefuseWrite): never {
50
+ assertWritable();
51
+ throw new Error('Document is read-only: it holds a stored record set.');
52
+ }
53
+
54
+ /** Whether a decoded body carries this entry itself. The prototype chain is
55
+ * not part of a record, so a key an app never wrote must not resolve through
56
+ * it. */
57
+ function ownEntry(object: Record<string, unknown>, key: string): boolean {
58
+ return Object.prototype.hasOwnProperty.call(object, key);
59
+ }
60
+
61
+ /** The subscribers of one handle, re-delivered when the content it reads is
62
+ * swapped. */
63
+ class RootSubscribers<C> {
64
+ private readonly _subs = new Set<C>();
65
+
66
+ add(cb: C, deliver: (cb: C) => void): HandleUnsubscribe {
67
+ this._subs.add(cb);
68
+ deliver(cb);
69
+ return () => {
70
+ this._subs.delete(cb);
71
+ };
72
+ }
73
+
74
+ each(visit: (cb: C) => void): void {
75
+ for (const cb of this._subs) {
76
+ try {
77
+ visit(cb);
78
+ } catch {
79
+ // A throwing subscriber cannot strand the others.
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ /** A stored map root as a map handle, and as the ordered map its stored
86
+ * order makes it. */
87
+ class SealedRecordMap<T> implements MapHandle<T>, OrderedMapHandle<T> {
88
+ private readonly _subs = new RootSubscribers<MapSubscriber<T>>();
89
+ private readonly _settled = new RootSubscribers<() => void>();
90
+ // The keys each subscriber was last handed, so a swap reports the ones the
91
+ // new content no longer holds as deleted.
92
+ private readonly _delivered = new Map<MapSubscriber<T>, string[]>();
93
+
94
+ /** `content` resolves the content this document shows now. A root that
95
+ * content does not hold reads as the empty collection a CRDT document
96
+ * reports for a container nothing has written.
97
+ *
98
+ * `_ordered` is which of the two interfaces this instance was taken as. A
99
+ * plain map's change carries no `order`, because that field is how an
100
+ * ordered map tells a subscriber to re-sequence; carrying it on a plain
101
+ * map's fire would ask for work the app never asked to do. */
102
+ constructor(
103
+ private readonly _content: () => SealedContent,
104
+ private readonly _name: string,
105
+ private readonly _refuse: RefuseWrite,
106
+ private readonly _ordered: boolean,
107
+ ) {}
108
+
109
+ private _map(): SealedMapReader | null {
110
+ return this._content().mapRoot(this._name);
111
+ }
112
+
113
+ get(entryKey: string): T | undefined {
114
+ return this._map()?.get(entryKey) as T | undefined;
115
+ }
116
+
117
+ has(entryKey: string): boolean {
118
+ return this._map()?.has(entryKey) ?? false;
119
+ }
120
+
121
+ keys(): string[] {
122
+ return this._map()?.keys() ?? [];
123
+ }
124
+
125
+ *entries(): IterableIterator<[string, T]> {
126
+ const map = this._map();
127
+ if (map !== null) yield* map.entries() as IterableIterator<[string, T]>;
128
+ }
129
+
130
+ /** @internal The whole collection at once. */
131
+ read(): Record<string, T> {
132
+ const out: Record<string, T> = {};
133
+ for (const [key, value] of this.entries()) out[key] = value;
134
+ return out;
135
+ }
136
+
137
+ ordered(): { key: string; value: T }[] {
138
+ const out: { key: string; value: T }[] = [];
139
+ for (const [key, value] of this.entries()) out.push({ key, value });
140
+ return out;
141
+ }
142
+
143
+ subscribe(cb: MapSubscriber<T>): HandleUnsubscribe {
144
+ const off = this._subs.add(cb, (sub) => this._deliver(sub));
145
+ return () => {
146
+ off();
147
+ this._delivered.delete(cb);
148
+ };
149
+ }
150
+
151
+ private _deliver(cb: MapSubscriber<T>): void {
152
+ const set = new Map<string, T>();
153
+ const order: string[] = [];
154
+ const map = this._map();
155
+ if (map !== null) {
156
+ for (const [key, value] of map.entries() as IterableIterator<[string, T]>) {
157
+ set.set(key, value);
158
+ order.push(key);
159
+ }
160
+ }
161
+ const before = this._delivered.get(cb) ?? [];
162
+ const deleted = before.filter((key) => !set.has(key));
163
+ this._delivered.set(cb, order);
164
+ const change: Change<T> = this._ordered ? { set, deleted, order } : { set, deleted };
165
+ cb(change);
166
+ }
167
+
168
+ onSettled(cb: () => void): HandleUnsubscribe {
169
+ return this._settled.add(cb, (sub) => sub());
170
+ }
171
+
172
+ /** @internal Re-deliver every subscriber against the content now shown. */
173
+ _resync(): void {
174
+ this._subs.each((cb) => this._deliver(cb));
175
+ this._settled.each((cb) => cb());
176
+ }
177
+
178
+ map<U>(entryKey: string): MapHandle<U> {
179
+ const value = this.get(entryKey);
180
+ return new SealedValueMap<U>(() => value, this._refuse);
181
+ }
182
+
183
+ list<U>(entryKey: string): ListHandle<U> {
184
+ const value = this.get(entryKey);
185
+ return new SealedValueList<U>(() => value, this._refuse);
186
+ }
187
+
188
+ mutate(): void {
189
+ refuse(this._refuse);
190
+ }
191
+
192
+ insert(): void {
193
+ refuse(this._refuse);
194
+ }
195
+
196
+ move(): void {
197
+ refuse(this._refuse);
198
+ }
199
+ }
200
+
201
+ /** A map handle over a plain value decoded out of the store: a map root that
202
+ * holds no records, a record, or a field inside one. A scoped handle onto an
203
+ * entry a CRDT document has not created yet reads as empty, and so does this
204
+ * one. `value` resolves at call time, so a root follows the content its
205
+ * document shows. */
206
+ class SealedValueMap<T> implements MapHandle<T> {
207
+ private readonly _subs = new RootSubscribers<MapSubscriber<T>>();
208
+ private readonly _settled = new RootSubscribers<() => void>();
209
+ private readonly _delivered = new Map<MapSubscriber<T>, string[]>();
210
+
211
+ constructor(
212
+ private readonly _value: () => unknown,
213
+ private readonly _refuse: RefuseWrite,
214
+ ) {}
215
+
216
+ private _object(): Record<string, unknown> {
217
+ const value = this._value();
218
+ return value && typeof value === 'object' && !Array.isArray(value)
219
+ ? (value as Record<string, unknown>)
220
+ : {};
221
+ }
222
+
223
+ get(entryKey: string): T | undefined {
224
+ const object = this._object();
225
+ return ownEntry(object, entryKey) ? (object[entryKey] as T) : undefined;
226
+ }
227
+
228
+ has(entryKey: string): boolean {
229
+ return ownEntry(this._object(), entryKey);
230
+ }
231
+
232
+ keys(): string[] {
233
+ return Object.keys(this._object());
234
+ }
235
+
236
+ *entries(): IterableIterator<[string, T]> {
237
+ const object = this._object();
238
+ for (const key of Object.keys(object)) yield [key, object[key] as T];
239
+ }
240
+
241
+ /** @internal The whole collection at once. */
242
+ read(): Record<string, T> {
243
+ return { ...this._object() } as Record<string, T>;
244
+ }
245
+
246
+ subscribe(cb: MapSubscriber<T>): HandleUnsubscribe {
247
+ const off = this._subs.add(cb, (sub) => this._deliver(sub));
248
+ return () => {
249
+ off();
250
+ this._delivered.delete(cb);
251
+ };
252
+ }
253
+
254
+ private _deliver(cb: MapSubscriber<T>): void {
255
+ const object = this._object();
256
+ const keys = Object.keys(object);
257
+ const before = this._delivered.get(cb) ?? [];
258
+ this._delivered.set(cb, keys);
259
+ cb({
260
+ set: new Map(keys.map((key) => [key, object[key] as T])),
261
+ deleted: before.filter((key) => !ownEntry(object, key)),
262
+ });
263
+ }
264
+
265
+ onSettled(cb: () => void): HandleUnsubscribe {
266
+ return this._settled.add(cb, (sub) => sub());
267
+ }
268
+
269
+ /** @internal */
270
+ _resync(): void {
271
+ this._subs.each((cb) => this._deliver(cb));
272
+ this._settled.each((cb) => cb());
273
+ }
274
+
275
+ map<U>(entryKey: string): MapHandle<U> {
276
+ const value = this._object()[entryKey];
277
+ return new SealedValueMap<U>(() => value, this._refuse);
278
+ }
279
+
280
+ list<U>(entryKey: string): ListHandle<U> {
281
+ const value = this._object()[entryKey];
282
+ return new SealedValueList<U>(() => value, this._refuse);
283
+ }
284
+
285
+ mutate(): void {
286
+ refuse(this._refuse);
287
+ }
288
+ }
289
+
290
+ class SealedValueList<T> implements ListHandle<T> {
291
+ private readonly _subs = new RootSubscribers<ListSubscriber<T>>();
292
+ private readonly _settled = new RootSubscribers<() => void>();
293
+ private readonly _delivered = new Map<ListSubscriber<T>, number>();
294
+
295
+ constructor(
296
+ private readonly _value: () => unknown,
297
+ private readonly _refuse: RefuseWrite,
298
+ ) {}
299
+
300
+ private _array(): T[] {
301
+ const value = this._value();
302
+ return Array.isArray(value) ? (value as T[]) : [];
303
+ }
304
+
305
+ length(): number {
306
+ return this._array().length;
307
+ }
308
+
309
+ get(index: number): T | undefined {
310
+ if (!Number.isInteger(index) || index < 0) return undefined;
311
+ return this._array()[index];
312
+ }
313
+
314
+ *values(): IterableIterator<T> {
315
+ yield* this._array();
316
+ }
317
+
318
+ /** @internal The whole list at once. */
319
+ read(): T[] {
320
+ return [...this._array()];
321
+ }
322
+
323
+ subscribe(cb: ListSubscriber<T>): HandleUnsubscribe {
324
+ const off = this._subs.add(cb, (sub) => this._deliver(sub));
325
+ return () => {
326
+ off();
327
+ this._delivered.delete(cb);
328
+ };
329
+ }
330
+
331
+ private _deliver(cb: ListSubscriber<T>): void {
332
+ const items = this._array();
333
+ const removed = this._delivered.get(cb) ?? 0;
334
+ this._delivered.set(cb, items.length);
335
+ const splices: Splice<T>[] = [];
336
+ pushBoundedSplice(splices, { at: 0, removed, inserted: items });
337
+ cb({ splices });
338
+ }
339
+
340
+ onSettled(cb: () => void): HandleUnsubscribe {
341
+ return this._settled.add(cb, (sub) => sub());
342
+ }
343
+
344
+ /** @internal */
345
+ _resync(): void {
346
+ this._subs.each((cb) => this._deliver(cb));
347
+ this._settled.each((cb) => cb());
348
+ }
349
+
350
+ mutate(): void {
351
+ refuse(this._refuse);
352
+ }
353
+ }
354
+
355
+ /** A text root. */
356
+ class SealedText implements TextHandle {
357
+ private readonly _subs = new RootSubscribers<TextSubscriber>();
358
+
359
+ constructor(
360
+ private readonly _value: () => string,
361
+ private readonly _refuse: RefuseWrite,
362
+ ) {}
363
+
364
+ read(): string {
365
+ return this._value();
366
+ }
367
+
368
+ update(): void {
369
+ refuse(this._refuse);
370
+ }
371
+
372
+ subscribe(cb: TextSubscriber): HandleUnsubscribe {
373
+ return this._subs.add(cb, (sub) => sub(this._value()));
374
+ }
375
+
376
+ /** @internal */
377
+ _resync(): void {
378
+ this._subs.each((cb) => cb(this._value()));
379
+ }
380
+ }
381
+
382
+ /** The roots of a document over frozen content. */
383
+ export type SealedRoots = ReturnType<typeof sealedRoots>;
384
+
385
+ /**
386
+ * The roots of a document over frozen content, whatever the content declares.
387
+ *
388
+ * `stored` may resolve the content the document shows now, so a version
389
+ * checkout swaps what every root reads and `resync` re-delivers the handles an
390
+ * app already holds. Each root handle is made once per key, which is what lets
391
+ * that reach them.
392
+ *
393
+ * A root the content does not hold reads as an empty map, list or text rather
394
+ * than throwing, which is what a CRDT document does for a container nothing
395
+ * has written: an app that reaches for a root of its own on an imported
396
+ * document gets the same empty answer either way.
397
+ */
398
+ export function sealedRoots(
399
+ stored: SealedContent | SealedStore | (() => SealedContent),
400
+ refuseWrite: RefuseWrite,
401
+ ) {
402
+ const content: () => SealedContent =
403
+ typeof stored === 'function'
404
+ ? stored
405
+ : ((fixed) => () => fixed)(stored instanceof SealedStore ? stored.content : stored);
406
+ // Decoded lists, per content, so a checkout never reads a stale memo.
407
+ const lists = new WeakMap<SealedContent, Map<string, unknown[]>>();
408
+ const listOf = (key: string): unknown[] | undefined => {
409
+ const current = content();
410
+ if (current.kindOf(key) !== 'list') return undefined;
411
+ let memo = lists.get(current);
412
+ if (memo === undefined) {
413
+ memo = new Map();
414
+ lists.set(current, memo);
415
+ }
416
+ let items = memo.get(key);
417
+ if (items === undefined) {
418
+ items = current.list(key) ?? [];
419
+ memo.set(key, items);
420
+ }
421
+ return items;
422
+ };
423
+ const roots = new Map<string, { _resync(): void }>();
424
+ const once = <H extends { _resync(): void }>(cacheKey: string, make: () => H): H => {
425
+ let handle = roots.get(cacheKey) as H | undefined;
426
+ if (handle === undefined) {
427
+ handle = make();
428
+ roots.set(cacheKey, handle);
429
+ }
430
+ return handle;
431
+ };
432
+ return {
433
+ map<T>(key: string): MapHandle<T> {
434
+ return once(
435
+ `map:${key}`,
436
+ () => new SealedRecordMap<unknown>(content, key, refuseWrite, false),
437
+ ) as MapHandle<T>;
438
+ },
439
+ list<T>(key: string): ListHandle<T> {
440
+ return once(
441
+ `list:${key}`,
442
+ () => new SealedValueList<unknown>(() => listOf(key), refuseWrite),
443
+ ) as ListHandle<T>;
444
+ },
445
+ text(key: string): TextHandle {
446
+ return once(`text:${key}`, () => new SealedText(() => content().text(key) ?? '', refuseWrite));
447
+ },
448
+ orderedMap<T>(mapKey: string, _orderKey: string): OrderedMapHandle<T> {
449
+ // A stored map keeps its entries in the order its order list gave
450
+ // them, so the map's own walk is the ordered map's order.
451
+ return once(
452
+ `ordered:${mapKey}`,
453
+ () => new SealedRecordMap<unknown>(content, mapKey, refuseWrite, true),
454
+ ) as OrderedMapHandle<T>;
455
+ },
456
+ /** Re-deliver every root handle's subscribers against the content now
457
+ * shown. */
458
+ resync(): void {
459
+ for (const handle of roots.values()) handle._resync();
460
+ },
461
+ /** The kind of every root that holds something, in key order, the way a
462
+ * CRDT document's `roots()` reports them. */
463
+ roots(): Record<string, 'map' | 'list' | 'text'> {
464
+ const current = content();
465
+ const found: [string, 'map' | 'list' | 'text'][] = [];
466
+ for (const name of current.rootNames()) {
467
+ const kind = current.kindOf(name);
468
+ if (kind === undefined) continue;
469
+ const empty =
470
+ kind === 'map'
471
+ ? (current.mapRoot(name)?.size() ?? 0) === 0
472
+ : kind === 'list'
473
+ ? (listOf(name)?.length ?? 0) === 0
474
+ : (current.text(name) ?? '') === '';
475
+ if (!empty) found.push([name, kind]);
476
+ }
477
+ found.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
478
+ return Object.fromEntries(found);
479
+ },
480
+ /** The whole document as plain JSON, empty roots included; the
481
+ * Document leaves those out. */
482
+ toJSON(): Record<string, unknown> {
483
+ return content().toJSON();
484
+ },
485
+ };
486
+ }