@brass-build/client 0.4.1 → 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 (323) hide show
  1. package/AGENTS.md +124 -45
  2. package/CHANGELOG.md +80 -4
  3. package/README.md +666 -179
  4. package/dist/api-fetch.d.ts +8 -0
  5. package/dist/api-fetch.d.ts.map +1 -1
  6. package/dist/api-fetch.js +35 -8
  7. package/dist/api-fetch.js.map +1 -1
  8. package/dist/batched-get.d.ts +11 -0
  9. package/dist/batched-get.d.ts.map +1 -0
  10. package/dist/batched-get.js +92 -0
  11. package/dist/batched-get.js.map +1 -0
  12. package/dist/call-service.d.ts +4 -0
  13. package/dist/call-service.d.ts.map +1 -1
  14. package/dist/call-service.js +66 -3
  15. package/dist/call-service.js.map +1 -1
  16. package/dist/client.d.ts +48 -2
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +918 -115
  19. package/dist/client.js.map +1 -1
  20. package/dist/document-checkpoints.d.ts +25 -2
  21. package/dist/document-checkpoints.d.ts.map +1 -1
  22. package/dist/document-checkpoints.js +258 -61
  23. package/dist/document-checkpoints.js.map +1 -1
  24. package/dist/document-commands.d.ts.map +1 -1
  25. package/dist/document-commands.js +5 -3
  26. package/dist/document-commands.js.map +1 -1
  27. package/dist/document-contents.d.ts +10 -0
  28. package/dist/document-contents.d.ts.map +1 -1
  29. package/dist/document-contents.js +23 -1
  30. package/dist/document-contents.js.map +1 -1
  31. package/dist/document-set-subscription.d.ts +56 -0
  32. package/dist/document-set-subscription.d.ts.map +1 -0
  33. package/dist/document-set-subscription.js +240 -0
  34. package/dist/document-set-subscription.js.map +1 -0
  35. package/dist/document-signals.d.ts +6 -0
  36. package/dist/document-signals.d.ts.map +1 -1
  37. package/dist/document-signals.js +31 -1
  38. package/dist/document-signals.js.map +1 -1
  39. package/dist/document-streams.d.ts +36 -2
  40. package/dist/document-streams.d.ts.map +1 -1
  41. package/dist/document-streams.js +69 -18
  42. package/dist/document-streams.js.map +1 -1
  43. package/dist/document-thumbnail-capture.d.ts +12 -2
  44. package/dist/document-thumbnail-capture.d.ts.map +1 -1
  45. package/dist/document-thumbnail-capture.js +103 -27
  46. package/dist/document-thumbnail-capture.js.map +1 -1
  47. package/dist/documents-namespace.d.ts +92 -10
  48. package/dist/documents-namespace.d.ts.map +1 -1
  49. package/dist/documents-namespace.js +285 -39
  50. package/dist/documents-namespace.js.map +1 -1
  51. package/dist/documents.d.ts +173 -8
  52. package/dist/documents.d.ts.map +1 -1
  53. package/dist/documents.js +1778 -310
  54. package/dist/documents.js.map +1 -1
  55. package/dist/errors.d.ts +3 -3
  56. package/dist/errors.d.ts.map +1 -1
  57. package/dist/errors.js.map +1 -1
  58. package/dist/fetch-retry.d.ts +1 -0
  59. package/dist/fetch-retry.d.ts.map +1 -1
  60. package/dist/fetch-retry.js +2 -0
  61. package/dist/fetch-retry.js.map +1 -1
  62. package/dist/file-choice.d.ts +2 -0
  63. package/dist/file-choice.d.ts.map +1 -0
  64. package/dist/file-choice.js +27 -0
  65. package/dist/file-choice.js.map +1 -0
  66. package/dist/guest-link.d.ts +18 -1
  67. package/dist/guest-link.d.ts.map +1 -1
  68. package/dist/guest-link.js +329 -1
  69. package/dist/guest-link.js.map +1 -1
  70. package/dist/handle-types.d.ts +67 -5
  71. package/dist/handle-types.d.ts.map +1 -1
  72. package/dist/handles.d.ts +15 -19
  73. package/dist/handles.d.ts.map +1 -1
  74. package/dist/handles.js +231 -76
  75. package/dist/handles.js.map +1 -1
  76. package/dist/hosted-ui.d.ts +61 -0
  77. package/dist/hosted-ui.d.ts.map +1 -1
  78. package/dist/hosted-ui.js +94 -42
  79. package/dist/hosted-ui.js.map +1 -1
  80. package/dist/import-contract.d.ts +74 -0
  81. package/dist/import-contract.d.ts.map +1 -0
  82. package/dist/import-contract.js +163 -0
  83. package/dist/import-contract.js.map +1 -0
  84. package/dist/import-guest.d.ts +9 -0
  85. package/dist/import-guest.d.ts.map +1 -0
  86. package/dist/import-guest.js +155 -0
  87. package/dist/import-guest.js.map +1 -0
  88. package/dist/import-report.d.ts +17 -0
  89. package/dist/import-report.d.ts.map +1 -0
  90. package/dist/import-report.js +7 -0
  91. package/dist/import-report.js.map +1 -0
  92. package/dist/importer-wait.d.ts +2 -0
  93. package/dist/importer-wait.d.ts.map +1 -0
  94. package/dist/importer-wait.js +5 -0
  95. package/dist/importer-wait.js.map +1 -0
  96. package/dist/importer.d.ts +18 -4
  97. package/dist/importer.d.ts.map +1 -1
  98. package/dist/importer.js +322 -105
  99. package/dist/importer.js.map +1 -1
  100. package/dist/in-development.d.ts +2 -0
  101. package/dist/in-development.d.ts.map +1 -0
  102. package/dist/in-development.js +10 -0
  103. package/dist/in-development.js.map +1 -0
  104. package/dist/index.d.ts +16 -8
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +24 -8
  107. package/dist/index.js.map +1 -1
  108. package/dist/init-keys.d.ts +1 -1
  109. package/dist/init-keys.d.ts.map +1 -1
  110. package/dist/init-keys.js +6 -1
  111. package/dist/init-keys.js.map +1 -1
  112. package/dist/input-documents.d.ts +67 -0
  113. package/dist/input-documents.d.ts.map +1 -0
  114. package/dist/input-documents.js +693 -0
  115. package/dist/input-documents.js.map +1 -0
  116. package/dist/inputs-status.d.ts +80 -0
  117. package/dist/inputs-status.d.ts.map +1 -0
  118. package/dist/inputs-status.js +460 -0
  119. package/dist/inputs-status.js.map +1 -0
  120. package/dist/inputs.d.ts +2 -0
  121. package/dist/inputs.d.ts.map +1 -0
  122. package/dist/inputs.js +793 -0
  123. package/dist/inputs.js.map +1 -0
  124. package/dist/internal.d.ts +6 -1
  125. package/dist/internal.d.ts.map +1 -1
  126. package/dist/internal.js +4 -1
  127. package/dist/internal.js.map +1 -1
  128. package/dist/join-content.d.ts +33 -0
  129. package/dist/join-content.d.ts.map +1 -0
  130. package/dist/join-content.js +114 -0
  131. package/dist/join-content.js.map +1 -0
  132. package/dist/join-stamp.d.ts +3 -0
  133. package/dist/join-stamp.d.ts.map +1 -0
  134. package/dist/join-stamp.js +12 -0
  135. package/dist/join-stamp.js.map +1 -0
  136. package/dist/manifest-drift.d.ts +9 -0
  137. package/dist/manifest-drift.d.ts.map +1 -0
  138. package/dist/manifest-drift.js +46 -0
  139. package/dist/manifest-drift.js.map +1 -0
  140. package/dist/open-document.d.ts +109 -2
  141. package/dist/open-document.d.ts.map +1 -1
  142. package/dist/open-document.js +1822 -811
  143. package/dist/open-document.js.map +1 -1
  144. package/dist/page-restore.d.ts +12 -0
  145. package/dist/page-restore.d.ts.map +1 -0
  146. package/dist/page-restore.js +23 -0
  147. package/dist/page-restore.js.map +1 -0
  148. package/dist/placement.d.ts +13 -0
  149. package/dist/placement.d.ts.map +1 -0
  150. package/dist/placement.js +34 -0
  151. package/dist/placement.js.map +1 -0
  152. package/dist/presence.d.ts +1 -0
  153. package/dist/presence.d.ts.map +1 -1
  154. package/dist/presence.js +16 -4
  155. package/dist/presence.js.map +1 -1
  156. package/dist/read-only-document.d.ts +87 -0
  157. package/dist/read-only-document.d.ts.map +1 -0
  158. package/dist/read-only-document.js +4 -0
  159. package/dist/read-only-document.js.map +1 -0
  160. package/dist/record-paging.d.ts +63 -6
  161. package/dist/record-paging.d.ts.map +1 -1
  162. package/dist/record-paging.js +122 -29
  163. package/dist/record-paging.js.map +1 -1
  164. package/dist/references.d.ts +40 -0
  165. package/dist/references.d.ts.map +1 -0
  166. package/dist/references.js +135 -0
  167. package/dist/references.js.map +1 -0
  168. package/dist/request-lane.d.ts +5 -0
  169. package/dist/request-lane.d.ts.map +1 -0
  170. package/dist/request-lane.js +48 -0
  171. package/dist/request-lane.js.map +1 -0
  172. package/dist/sealed-arrow.d.ts +170 -0
  173. package/dist/sealed-arrow.d.ts.map +1 -0
  174. package/dist/sealed-arrow.js +769 -0
  175. package/dist/sealed-arrow.js.map +1 -0
  176. package/dist/sealed-cache.d.ts +21 -0
  177. package/dist/sealed-cache.d.ts.map +1 -0
  178. package/dist/sealed-cache.js +247 -0
  179. package/dist/sealed-cache.js.map +1 -0
  180. package/dist/sealed-encode.d.ts +87 -0
  181. package/dist/sealed-encode.d.ts.map +1 -0
  182. package/dist/sealed-encode.js +876 -0
  183. package/dist/sealed-encode.js.map +1 -0
  184. package/dist/sealed-handles.d.ts +45 -0
  185. package/dist/sealed-handles.d.ts.map +1 -0
  186. package/dist/sealed-handles.js +408 -0
  187. package/dist/sealed-handles.js.map +1 -0
  188. package/dist/sealed-store.d.ts +242 -0
  189. package/dist/sealed-store.d.ts.map +1 -0
  190. package/dist/sealed-store.js +1155 -0
  191. package/dist/sealed-store.js.map +1 -0
  192. package/dist/serializer.d.ts +6 -15
  193. package/dist/serializer.d.ts.map +1 -1
  194. package/dist/session-toolbar.d.ts +20 -5
  195. package/dist/session-toolbar.d.ts.map +1 -1
  196. package/dist/session-toolbar.js +419 -67
  197. package/dist/session-toolbar.js.map +1 -1
  198. package/dist/session.d.ts +38 -1
  199. package/dist/session.d.ts.map +1 -1
  200. package/dist/session.js +562 -79
  201. package/dist/session.js.map +1 -1
  202. package/dist/sha256.d.ts +3 -0
  203. package/dist/sha256.d.ts.map +1 -0
  204. package/dist/sha256.js +74 -0
  205. package/dist/sha256.js.map +1 -0
  206. package/dist/stable-handles.d.ts +19 -0
  207. package/dist/stable-handles.d.ts.map +1 -0
  208. package/dist/stable-handles.js +193 -0
  209. package/dist/stable-handles.js.map +1 -0
  210. package/dist/staged-seal.d.ts +2 -0
  211. package/dist/staged-seal.d.ts.map +1 -0
  212. package/dist/staged-seal.js +76 -0
  213. package/dist/staged-seal.js.map +1 -0
  214. package/dist/subscriber-delivery.d.ts +8 -0
  215. package/dist/subscriber-delivery.d.ts.map +1 -0
  216. package/dist/subscriber-delivery.js +31 -0
  217. package/dist/subscriber-delivery.js.map +1 -0
  218. package/dist/tail-op-import.d.ts +10 -0
  219. package/dist/tail-op-import.d.ts.map +1 -0
  220. package/dist/tail-op-import.js +33 -0
  221. package/dist/tail-op-import.js.map +1 -0
  222. package/dist/topic-lifecycle.d.ts +6 -0
  223. package/dist/topic-lifecycle.d.ts.map +1 -1
  224. package/dist/topic-lifecycle.js +32 -2
  225. package/dist/topic-lifecycle.js.map +1 -1
  226. package/dist/trial-replace-dialog.d.ts +10 -0
  227. package/dist/trial-replace-dialog.d.ts.map +1 -0
  228. package/dist/trial-replace-dialog.js +153 -0
  229. package/dist/trial-replace-dialog.js.map +1 -0
  230. package/dist/types.d.ts +8 -1
  231. package/dist/types.d.ts.map +1 -1
  232. package/dist/undo.d.ts +60 -5
  233. package/dist/undo.d.ts.map +1 -1
  234. package/dist/undo.js +344 -75
  235. package/dist/undo.js.map +1 -1
  236. package/dist/version-bar.d.ts +1 -0
  237. package/dist/version-bar.d.ts.map +1 -1
  238. package/dist/version-bar.js +2 -1
  239. package/dist/version-bar.js.map +1 -1
  240. package/dist/version.d.ts +2 -2
  241. package/dist/version.js +1 -1
  242. package/dist/ws-client.d.ts +21 -0
  243. package/dist/ws-client.d.ts.map +1 -1
  244. package/dist/ws-client.js +192 -8
  245. package/dist/ws-client.js.map +1 -1
  246. package/dist/ws-frame-limit.d.ts +1 -0
  247. package/dist/ws-frame-limit.d.ts.map +1 -1
  248. package/dist/ws-frame-limit.js.map +1 -1
  249. package/docs/app-manifest.md +121 -17
  250. package/docs/custom-chrome.md +24 -16
  251. package/docs/hosted-ui-advanced.md +38 -6
  252. package/docs/imported-documents.md +104 -51
  253. package/docs/presence.md +16 -4
  254. package/docs/schemas.md +8 -5
  255. package/docs/services.md +6 -6
  256. package/docs/streams.md +139 -150
  257. package/docs/text-fields.md +26 -19
  258. package/package.json +5 -2
  259. package/src/api-fetch.ts +53 -3
  260. package/src/auth-flow.ts +1 -1
  261. package/src/batched-get.ts +117 -0
  262. package/src/call-service.ts +78 -3
  263. package/src/client.ts +1022 -112
  264. package/src/document-checkpoints.ts +307 -65
  265. package/src/document-commands.ts +5 -5
  266. package/src/document-contents.ts +27 -1
  267. package/src/document-set-subscription.ts +274 -0
  268. package/src/document-signals.ts +47 -5
  269. package/src/document-streams.ts +85 -16
  270. package/src/document-thumbnail-capture.ts +118 -32
  271. package/src/documents-namespace.ts +539 -84
  272. package/src/documents.ts +2017 -348
  273. package/src/errors.ts +17 -6
  274. package/src/fetch-retry.ts +5 -0
  275. package/src/file-choice.ts +26 -0
  276. package/src/guest-link.ts +362 -1
  277. package/src/handle-types.ts +73 -47
  278. package/src/handles.ts +253 -83
  279. package/src/hosted-ui.ts +211 -50
  280. package/src/import-contract.ts +227 -0
  281. package/src/import-guest.ts +193 -0
  282. package/src/import-report.ts +24 -0
  283. package/src/importer-wait.ts +4 -0
  284. package/src/importer.ts +386 -131
  285. package/src/in-development.ts +10 -0
  286. package/src/index.ts +76 -16
  287. package/src/init-keys.ts +6 -1
  288. package/src/input-documents.ts +819 -0
  289. package/src/inputs-status.ts +598 -0
  290. package/src/inputs.ts +1090 -0
  291. package/src/internal.ts +7 -0
  292. package/src/join-content.ts +135 -0
  293. package/src/join-stamp.ts +12 -0
  294. package/src/manifest-drift.ts +54 -0
  295. package/src/open-document.ts +2346 -930
  296. package/src/page-restore.ts +38 -0
  297. package/src/placement.ts +54 -0
  298. package/src/presence.ts +21 -5
  299. package/src/read-only-document.ts +133 -0
  300. package/src/record-paging.ts +147 -25
  301. package/src/references.ts +207 -0
  302. package/src/request-lane.ts +59 -0
  303. package/src/sealed-arrow.ts +843 -0
  304. package/src/sealed-cache.ts +267 -0
  305. package/src/sealed-encode.ts +1015 -0
  306. package/src/sealed-handles.ts +486 -0
  307. package/src/sealed-store.ts +1330 -0
  308. package/src/serializer.ts +8 -20
  309. package/src/session-toolbar.ts +463 -72
  310. package/src/session.ts +740 -99
  311. package/src/sha256.ts +65 -0
  312. package/src/stable-handles.ts +235 -0
  313. package/src/staged-seal.ts +90 -0
  314. package/src/subscriber-delivery.ts +37 -0
  315. package/src/tail-op-import.ts +30 -0
  316. package/src/topic-lifecycle.ts +38 -2
  317. package/src/trial-replace-dialog.ts +163 -0
  318. package/src/types.ts +6 -6
  319. package/src/undo.ts +386 -73
  320. package/src/version-bar.ts +5 -1
  321. package/src/version.ts +1 -1
  322. package/src/ws-client.ts +208 -16
  323. package/src/ws-frame-limit.ts +1 -0
package/dist/inputs.js ADDED
@@ -0,0 +1,793 @@
1
+ // The shape of a document that references other documents: the references a
2
+ // key holds, the reserved CRDT roots they live in, the status the platform
3
+ // reports for a key, and the frames and bodies that carry it. This is the one
4
+ // definition; the platform's `@brass-build/contracts` re-exports it.
5
+ //
6
+ // The document's CRDT is the source of truth for each key's references and
7
+ // read marks. Nothing read out of the roots is authority: a reference's
8
+ // `organization_id` is a hint for resolving it and grants nothing.
9
+ import { sha256Hex } from './sha256.js';
10
+ const DEFAULT_STREAM_NAME = 'default';
11
+ const NAME_TOKEN_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
12
+ function isNameToken(value) {
13
+ return NAME_TOKEN_PATTERN.test(value);
14
+ }
15
+ // A stored reference may carry its families as `types`, which reads the same.
16
+ function parseSelectionAccept(value) {
17
+ if (!value || typeof value !== 'object')
18
+ return null;
19
+ const raw = value;
20
+ const list = (v) => {
21
+ if (!Array.isArray(v))
22
+ return null;
23
+ const items = v.filter((x) => typeof x === 'string' && x !== '');
24
+ return items.length > 0 ? items : null;
25
+ };
26
+ const names = list(raw['names']);
27
+ const extensions = list(raw['extensions']);
28
+ const families = list(raw['families']) ?? list(raw['types']);
29
+ if (!names && !extensions && !families)
30
+ return null;
31
+ return {
32
+ ...(names ? { names } : {}),
33
+ ...(extensions ? { extensions } : {}),
34
+ ...(families ? { families } : {}),
35
+ };
36
+ }
37
+ function documentTopic(docId, branchId) {
38
+ return `doc:${docId}:${branchId}`;
39
+ }
40
+ function parseDocTopic(topic) {
41
+ const m = /^doc:(doc_[A-Za-z0-9_-]{32}):(b_[A-Za-z0-9_-]{32})$/.exec(topic);
42
+ return m && m[1] && m[2] ? { docId: m[1], branchId: m[2] } : null;
43
+ }
44
+ // --- Identifiers -----------------------------------------------------------
45
+ const DOC_ID_PATTERN = /^doc_[A-Za-z0-9_-]{32}$/;
46
+ const LINE_ID_PATTERN = /^b_[A-Za-z0-9_-]{32}$/;
47
+ // Organization and project ids are opaque here; the shape only has to keep
48
+ // them safe inside a key or an identity string.
49
+ const ENTITY_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
50
+ /** @internal */
51
+ export function isInputDocId(value) {
52
+ return typeof value === 'string' && DOC_ID_PATTERN.test(value);
53
+ }
54
+ /** @internal */
55
+ export function isInputLineId(value) {
56
+ return typeof value === 'string' && LINE_ID_PATTERN.test(value);
57
+ }
58
+ // --- Keys ------------------------------------------------------------------
59
+ /** @internal The longest key an app may record references under. */
60
+ export const INPUT_KEY_MAX_CHARS = 128;
61
+ /**
62
+ * @internal
63
+ * An app key: a letter or digit, then letters, digits and `._:@/+=~-`. `#`
64
+ * is excluded because the key is the tail of a row's sort key, and so is
65
+ * whitespace, which an app writing `stamp:${id}` never produces on purpose.
66
+ */
67
+ export const INPUT_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/+=~-]{0,127}$/;
68
+ /** @internal Keys under this prefix are the platform's own, refused from an app. */
69
+ export const INPUT_KEY_RESERVED_PREFIX = 'brass:';
70
+ /** @internal */
71
+ export function isInputKey(value) {
72
+ return (typeof value === 'string' &&
73
+ INPUT_KEY_PATTERN.test(value) &&
74
+ !value.toLowerCase().startsWith(INPUT_KEY_RESERVED_PREFIX));
75
+ }
76
+ /** @internal */
77
+ export const INPUT_REFERENCE_LABEL_MAX_CHARS = 256;
78
+ /**
79
+ * @internal
80
+ * Reads a reference off an untrusted value: the wire, or an entry a peer
81
+ * wrote into the reserved roots. Null when it is not one.
82
+ */
83
+ export function parseInputReference(raw) {
84
+ if (!isRecord(raw))
85
+ return null;
86
+ const extent = raw['extent'];
87
+ const organizationId = raw['organization_id'];
88
+ if (typeof organizationId !== 'string' || !ENTITY_ID_PATTERN.test(organizationId))
89
+ return null;
90
+ const ref = { organization_id: organizationId, extent: 'organization' };
91
+ if (extent === 'document') {
92
+ if (!isInputDocId(raw['doc_id']))
93
+ return null;
94
+ ref.extent = 'document';
95
+ ref.doc_id = raw['doc_id'];
96
+ const stream = raw['stream'];
97
+ if (stream !== undefined) {
98
+ if (typeof stream !== 'string' || !isNameToken(stream))
99
+ return null;
100
+ if (stream !== DEFAULT_STREAM_NAME)
101
+ ref.stream = stream;
102
+ }
103
+ }
104
+ else if (extent === 'project') {
105
+ const projectId = raw['project_id'];
106
+ if (typeof projectId !== 'string' || !ENTITY_ID_PATTERN.test(projectId))
107
+ return null;
108
+ if (raw['stream'] !== undefined)
109
+ return null;
110
+ ref.extent = 'project';
111
+ ref.project_id = projectId;
112
+ }
113
+ else if (extent === 'organization') {
114
+ if (raw['stream'] !== undefined)
115
+ return null;
116
+ }
117
+ else {
118
+ return null;
119
+ }
120
+ // A project reference may also say which project a document sits in; the
121
+ // identity reads only the field its extent names.
122
+ if (ref.extent === 'document' && typeof raw['project_id'] === 'string') {
123
+ if (!ENTITY_ID_PATTERN.test(raw['project_id']))
124
+ return null;
125
+ ref.project_id = raw['project_id'];
126
+ }
127
+ const label = raw['label'];
128
+ if (label !== undefined) {
129
+ if (typeof label !== 'string' || label.length > INPUT_REFERENCE_LABEL_MAX_CHARS)
130
+ return null;
131
+ ref.label = label;
132
+ }
133
+ if (raw['accept'] !== undefined) {
134
+ if (!isRecord(raw['accept']))
135
+ return null;
136
+ // A narrowing with no clause narrows nothing, so it is the same
137
+ // reference as one carrying none.
138
+ const accept = parseSelectionAccept(raw['accept']);
139
+ if (accept !== null)
140
+ ref.accept = accept;
141
+ }
142
+ return ref;
143
+ }
144
+ function isEmptyAccept(accept) {
145
+ return !accept.families?.length && !accept.names?.length && !accept.extensions?.length;
146
+ }
147
+ // The narrowing as one canonical string: families and extensions compared
148
+ // case-insensitively, extensions without their dot, names case-insensitively
149
+ // (the match is), each list sorted and de-duplicated.
150
+ function canonicalAccept(accept) {
151
+ const norm = (xs, strip = false) => [...new Set((xs ?? []).map((x) => (strip ? x.replace(/^\./, '') : x).toLowerCase()))].sort();
152
+ return JSON.stringify({
153
+ t: norm(accept.families),
154
+ e: norm(accept.extensions, true),
155
+ n: norm(accept.names),
156
+ });
157
+ }
158
+ /**
159
+ * @internal
160
+ * The reference's stable identity: the key it is stored under in the
161
+ * references root, and what the platform's rows name it by.
162
+ *
163
+ * `d:<doc_id>`, `d:<doc_id>/<stream>`, `p:<project_id>` or
164
+ * `o:<organization_id>`, followed by `~<16 hex>` when the reference carries a
165
+ * narrowing. The label is not part of it, so renaming a choice does not make
166
+ * it a different reference.
167
+ */
168
+ export function inputReferenceId(ref) {
169
+ let id;
170
+ if (ref.extent === 'document') {
171
+ id = `d:${ref.doc_id ?? ''}`;
172
+ if (ref.stream !== undefined && ref.stream !== DEFAULT_STREAM_NAME)
173
+ id += `/${ref.stream}`;
174
+ }
175
+ else if (ref.extent === 'project') {
176
+ id = `p:${ref.project_id ?? ''}`;
177
+ }
178
+ else {
179
+ id = `o:${ref.organization_id}`;
180
+ }
181
+ if (ref.accept && !isEmptyAccept(ref.accept)) {
182
+ id += `~${sha256Hex(canonicalAccept(ref.accept)).slice(0, 16)}`;
183
+ }
184
+ return id;
185
+ }
186
+ const INPUT_REFERENCE_ID_PATTERN = /^(?:d:doc_[A-Za-z0-9_-]{32}(?:\/[a-z0-9][a-z0-9._-]{0,63})?|[po]:[A-Za-z0-9_-]{1,64})(?:~[0-9a-f]{16})?$/;
187
+ /** @internal */
188
+ export function isInputReferenceId(value) {
189
+ return typeof value === 'string' && INPUT_REFERENCE_ID_PATTERN.test(value);
190
+ }
191
+ /**
192
+ * @internal
193
+ * The hash of a reference set: what keys with identical references share one
194
+ * resolution under. Order and duplicates do not move it, and neither does a
195
+ * label.
196
+ */
197
+ export function inputReferenceSetHash(refs) {
198
+ const ids = [...new Set(refs.map(inputReferenceId))].sort();
199
+ return sha256Hex(`inputs-v1\n${ids.join('\n')}`);
200
+ }
201
+ const SET_HASH_PATTERN = /^[0-9a-f]{64}$/;
202
+ /** @internal */
203
+ export function isInputReferenceSetHash(value) {
204
+ return typeof value === 'string' && SET_HASH_PATTERN.test(value);
205
+ }
206
+ // --- Limits ----------------------------------------------------------------
207
+ /**
208
+ * @internal
209
+ * The most distinct documents a document's keys report across all of them.
210
+ * Past it the status says `truncated`; it is never a refusal.
211
+ */
212
+ export const INPUT_DOCS_MAX = 2000;
213
+ /** @internal The most references one key may hold in a provisional body. */
214
+ export const INPUT_REFS_MAX_PER_KEY = 2000;
215
+ /** @internal The most read marks one key may carry in a provisional body. */
216
+ export const INPUT_READ_MARKS_MAX_PER_KEY = INPUT_DOCS_MAX;
217
+ /** @internal The longest version stamp a read mark or a file may carry. */
218
+ export const INPUT_STAMP_MAX_CHARS = 64;
219
+ // --- The reserved CRDT roots -------------------------------------------------
220
+ /**
221
+ * @internal
222
+ * The document roots holding every key's references and read marks. Both are
223
+ * flat maps at the root: a root container's id is fixed by its name, so every
224
+ * peer writes into the same container. A container created under a map entry
225
+ * is not: two peers creating one concurrently make two, and the entry keeps
226
+ * one of them, dropping the other's writes. So nothing here nests.
227
+ *
228
+ * The names sit outside both the `brass:` record-store key space and any name
229
+ * an app passes to `doc.map`, so record paging and the frozen-to-CRDT
230
+ * conversion, which walk app roots, never reach them.
231
+ *
232
+ * `INPUTS_REFS_ROOT`: `<key>␟<referenceId>` -> the reference.
233
+ * `INPUTS_READ_ROOT`: `<key>␟<docId>␟<stamp>␟<kind>` -> true.
234
+ */
235
+ export const INPUTS_REFS_ROOT = '__brass_input_refs';
236
+ /** @internal */
237
+ export const INPUTS_READ_ROOT = '__brass_input_read';
238
+ /**
239
+ * @internal
240
+ * Joins the parts of an entry key: U+241F SYMBOL FOR UNIT SEPARATOR, which no
241
+ * app key, reference id, doc id or stamp can contain.
242
+ */
243
+ export const INPUTS_ENTRY_SEPARATOR = '␟';
244
+ const SEP = INPUTS_ENTRY_SEPARATOR;
245
+ /** @internal */
246
+ export function inputRefEntryKey(key, refId) {
247
+ return `${key}${SEP}${refId}`;
248
+ }
249
+ /** @internal */
250
+ export function parseInputRefEntryKey(entryKey) {
251
+ const parts = entryKey.split(SEP);
252
+ if (parts.length !== 2)
253
+ return null;
254
+ const [key, refId] = parts;
255
+ return isInputKey(key) && isInputReferenceId(refId) ? { key, refId } : null;
256
+ }
257
+ /** @internal Digits a stamp is zero-padded to in an entry key, so the keys sort by stamp. */
258
+ export const INPUT_STAMP_DIGITS = 16;
259
+ /**
260
+ * @internal
261
+ * A version of a document, as a read mark records it: the milliseconds since
262
+ * the epoch of the `updated_at` the reader read the document at.
263
+ */
264
+ export function inputStampFromUpdatedAt(updatedAt) {
265
+ const ms = Date.parse(updatedAt);
266
+ return Number.isSafeInteger(ms) && ms >= 0 && ms < 10 ** INPUT_STAMP_DIGITS ? ms : null;
267
+ }
268
+ function isStamp(value) {
269
+ return (typeof value === 'number' &&
270
+ Number.isSafeInteger(value) &&
271
+ value >= 0 &&
272
+ value < 10 ** INPUT_STAMP_DIGITS);
273
+ }
274
+ const READ_KIND = { removed: '0', read: '1' };
275
+ /** @internal */
276
+ export function inputReadEntryKey(key, docId, mark) {
277
+ const stamp = String(mark.stamp).padStart(INPUT_STAMP_DIGITS, '0');
278
+ const kind = mark.removed ? READ_KIND.removed : READ_KIND.read;
279
+ return `${key}${SEP}${docId}${SEP}${stamp}${SEP}${kind}`;
280
+ }
281
+ /** @internal The prefix every read mark of one key shares, or of one document under it. */
282
+ export function inputReadEntryPrefix(key, docId) {
283
+ return docId === undefined ? `${key}${SEP}` : `${key}${SEP}${docId}${SEP}`;
284
+ }
285
+ /** @internal */
286
+ export function parseInputReadEntryKey(entryKey) {
287
+ const parts = entryKey.split(SEP);
288
+ if (parts.length !== 4)
289
+ return null;
290
+ const [key, docId, stamp, kind] = parts;
291
+ if (!isInputKey(key) || !isInputDocId(docId))
292
+ return null;
293
+ if (!new RegExp(`^[0-9]{${INPUT_STAMP_DIGITS}}$`).test(stamp))
294
+ return null;
295
+ if (kind !== READ_KIND.removed && kind !== READ_KIND.read)
296
+ return null;
297
+ return { key, docId, mark: { stamp: Number(stamp), removed: kind === READ_KIND.removed } };
298
+ }
299
+ /**
300
+ * @internal
301
+ * The mark that acknowledges a file leaving a key, given the latest mark the
302
+ * reader holds for it and the version it left at (`updatedAt` on the removed
303
+ * entry; null when the entry carries none). The mark sits at that version, so
304
+ * a read of the file returning at it follows the removal. When the version
305
+ * did not move past the reader's last read, the removal has to sit one stamp
306
+ * past that read to follow it, and `inputReadMarkFor` lifts a later read of
307
+ * the returning file to the removal's stamp. A removal already acknowledged
308
+ * keeps its mark.
309
+ */
310
+ export function inputRemovalMark(latest, updatedAt) {
311
+ if (latest?.removed === true)
312
+ return latest;
313
+ const leftAt = updatedAt === null ? null : inputStampFromUpdatedAt(updatedAt);
314
+ const floor = latest === null ? 0 : latest.stamp + 1;
315
+ return { stamp: Math.max(leftAt ?? 0, floor), removed: true };
316
+ }
317
+ /**
318
+ * @internal
319
+ * The mark that records reading a file at `stamp`, given the latest mark the
320
+ * reader holds for it: at `stamp`, or at a removal's stamp when that removal
321
+ * sits past it, which is the one stamp a read can take to follow it.
322
+ */
323
+ export function inputReadMarkFor(latest, stamp) {
324
+ if (latest?.removed === true && latest.stamp > stamp)
325
+ return { stamp: latest.stamp, removed: false };
326
+ return { stamp, removed: false };
327
+ }
328
+ /** @internal Whether `a` is a later acknowledgement than `b`. */
329
+ export function inputReadMarkAfter(a, b) {
330
+ if (a.stamp !== b.stamp)
331
+ return a.stamp > b.stamp;
332
+ return !a.removed && b.removed;
333
+ }
334
+ /** @internal */
335
+ export function parseInputReadMark(raw) {
336
+ if (!isRecord(raw) || !isStamp(raw['stamp']) || typeof raw['removed'] !== 'boolean')
337
+ return null;
338
+ return { stamp: raw['stamp'], removed: raw['removed'] };
339
+ }
340
+ /**
341
+ * @internal
342
+ * Every key holding references, from the two roots' plain-object forms.
343
+ * Tolerant, since peers write the roots: an entry whose key does not parse,
344
+ * whose value is not a reference, or whose reference id does not match the
345
+ * one re-derived from its value is dropped rather than failing the read. A
346
+ * read mark is kept only when present (`true`), and only the latest per
347
+ * document counts. Keys with no references are absent, along with their
348
+ * marks.
349
+ */
350
+ export function decodeInputsRoots(refsRoot, readRoot) {
351
+ const refsByKey = new Map();
352
+ if (isRecord(refsRoot)) {
353
+ for (const [entryKey, value] of Object.entries(refsRoot)) {
354
+ const parsed = parseInputRefEntryKey(entryKey);
355
+ const ref = parseInputReference(value);
356
+ if (parsed === null || ref === null || inputReferenceId(ref) !== parsed.refId)
357
+ continue;
358
+ const held = refsByKey.get(parsed.key) ?? new Map();
359
+ held.set(parsed.refId, ref);
360
+ refsByKey.set(parsed.key, held);
361
+ }
362
+ }
363
+ const out = new Map();
364
+ for (const key of [...refsByKey.keys()].sort()) {
365
+ const held = refsByKey.get(key);
366
+ const refs = [...held.keys()].sort().map((id) => held.get(id));
367
+ out.set(key, { refs, read: {} });
368
+ }
369
+ if (isRecord(readRoot)) {
370
+ for (const [entryKey, value] of Object.entries(readRoot)) {
371
+ if (value !== true)
372
+ continue;
373
+ const parsed = parseInputReadEntryKey(entryKey);
374
+ const state = parsed ? out.get(parsed.key) : undefined;
375
+ if (!parsed || !state)
376
+ continue;
377
+ const prior = state.read[parsed.docId];
378
+ if (!prior || inputReadMarkAfter(parsed.mark, prior))
379
+ state.read[parsed.docId] = parsed.mark;
380
+ }
381
+ }
382
+ return out;
383
+ }
384
+ /**
385
+ * @internal
386
+ * The writes that make `key`'s references `next` (null clears the key), given
387
+ * the refs root as this writer sees it. Only the difference is written:
388
+ * entries this writer did not see are not touched, so a reference another
389
+ * peer adds concurrently survives. Throws on a reference that would not
390
+ * decode.
391
+ */
392
+ export function inputRefsWrites(refsRoot, key, next) {
393
+ if (!isInputKey(key))
394
+ throw new TypeError('Not a valid input key');
395
+ const wanted = new Map();
396
+ for (const ref of next ?? []) {
397
+ const parsed = parseInputReference(ref);
398
+ if (parsed === null)
399
+ throw new TypeError('Not a valid input reference');
400
+ wanted.set(inputRefEntryKey(key, inputReferenceId(parsed)), parsed);
401
+ }
402
+ const writes = { set: {}, delete: [] };
403
+ for (const entryKey of Object.keys(refsRoot).sort()) {
404
+ if (parseInputRefEntryKey(entryKey)?.key === key && !wanted.has(entryKey)) {
405
+ writes.delete.push(entryKey);
406
+ }
407
+ }
408
+ for (const entryKey of [...wanted.keys()].sort()) {
409
+ const ref = wanted.get(entryKey);
410
+ if (!sameJson(refsRoot[entryKey], ref))
411
+ writes.set[entryKey] = ref;
412
+ }
413
+ return writes;
414
+ }
415
+ /**
416
+ * @internal
417
+ * The writes that record `mark` for `docId` under `key`, given the read root
418
+ * as this writer sees it: the mark's entry, and the deletion of every earlier
419
+ * mark for that document. A later mark already present is left in place, and
420
+ * still wins.
421
+ */
422
+ export function inputReadMarkWrites(readRoot, key, docId, mark) {
423
+ if (!isInputKey(key) || !isInputDocId(docId) || parseInputReadMark(mark) === null) {
424
+ throw new TypeError('Not a valid read mark');
425
+ }
426
+ const own = inputReadEntryKey(key, docId, mark);
427
+ const writes = { set: {}, delete: [] };
428
+ for (const entryKey of Object.keys(readRoot).sort()) {
429
+ const parsed = parseInputReadEntryKey(entryKey);
430
+ if (parsed?.key === key && parsed.docId === docId && inputReadMarkAfter(mark, parsed.mark)) {
431
+ writes.delete.push(entryKey);
432
+ }
433
+ }
434
+ if (readRoot[own] !== true)
435
+ writes.set[own] = true;
436
+ return writes;
437
+ }
438
+ /** @internal The writes that clear every read mark under `key`, as when the key is cleared. */
439
+ export function inputReadClearWrites(readRoot, key) {
440
+ const prefix = inputReadEntryPrefix(key);
441
+ return { set: {}, delete: Object.keys(readRoot).filter((k) => k.startsWith(prefix)).sort() };
442
+ }
443
+ function sameJson(a, b) {
444
+ return JSON.stringify(a) === JSON.stringify(b);
445
+ }
446
+ /** @internal */
447
+ export const INPUT_FILE_CHANGES = ['added', 'modified', 'removed'];
448
+ /** @internal The most keys one page of the keyless status carries. */
449
+ export const INPUT_KEYS_PAGE_SIZE = 50;
450
+ /** @internal The most changed entries one page of the keyless status carries. */
451
+ export const INPUT_KEYS_CHANGED_MAX = INPUT_DOCS_MAX;
452
+ /** @internal Query parameters on `GET` of a key's status. */
453
+ export const INPUT_STATUS_SINCE_PARAM = 'since_rev';
454
+ /** @internal */
455
+ export const INPUT_STATUS_CURSOR_PARAM = 'cursor';
456
+ /**
457
+ * @internal
458
+ * The line whose keys a `GET` reads: a branch or a stream of the document,
459
+ * named as a document join names it. Absent reads the document's default
460
+ * branch.
461
+ */
462
+ export const INPUT_STATUS_LINE_PARAM = 'branch';
463
+ /** @internal The most documents one page or one delta carries. */
464
+ export const INPUT_STATUS_PAGE_SIZE = 500;
465
+ /**
466
+ * @internal
467
+ * A page cursor: the revision the first page answered at and the last doc id
468
+ * the previous page held. Pinning the revision is what keeps a paged read
469
+ * consistent; a server whose status moved since answers a resync, and the
470
+ * reader starts again.
471
+ */
472
+ export function encodeInputStatusCursor(cursor) {
473
+ return `${cursor.rev}.${cursor.after}`;
474
+ }
475
+ /**
476
+ * @internal
477
+ * A keyless status cursor: `encodeInputStatusCursor` over the revision the
478
+ * first page answered at and the last key the previous page held.
479
+ */
480
+ export function decodeInputKeysCursor(raw) {
481
+ if (typeof raw !== 'string')
482
+ return null;
483
+ const m = /^(0|[1-9][0-9]{0,14})\.(.+)$/s.exec(raw);
484
+ if (!m || !m[1] || !m[2] || !isInputKey(m[2]))
485
+ return null;
486
+ return { rev: Number(m[1]), after: m[2] };
487
+ }
488
+ /** @internal */
489
+ export function decodeInputStatusCursor(raw) {
490
+ if (typeof raw !== 'string')
491
+ return null;
492
+ const m = /^(0|[1-9][0-9]{0,14})\.(doc_[A-Za-z0-9_-]{32})$/.exec(raw);
493
+ if (!m || !m[1] || !m[2])
494
+ return null;
495
+ return { rev: Number(m[1]), after: m[2] };
496
+ }
497
+ /**
498
+ * @internal
499
+ * The status a delta moves `status` to, or null when the delta does not start
500
+ * at `status.rev` (a gap, which the reader closes by fetching again).
501
+ */
502
+ export function applyInputStatusDelta(status, delta) {
503
+ if (delta.key !== status.key || delta.since_rev !== status.rev)
504
+ return null;
505
+ return {
506
+ key: status.key,
507
+ refs: delta.refs ?? status.refs,
508
+ files: applyUpserts(status.files, delta.files),
509
+ changed: applyUpserts(status.changed, delta.changed),
510
+ hidden_count: delta.hidden_count,
511
+ truncated: delta.truncated,
512
+ rev: delta.rev,
513
+ ...(delta.stale === true ? { stale: true } : {}),
514
+ ...(delta.arriving === true ? { arriving: true } : {}),
515
+ };
516
+ }
517
+ function applyUpserts(current, diff) {
518
+ const byId = new Map(current.map((e) => [e.doc_id, e]));
519
+ for (const id of diff.remove)
520
+ byId.delete(id);
521
+ for (const e of diff.upsert)
522
+ byId.set(e.doc_id, e);
523
+ return [...byId.values()].sort((a, b) => (a.doc_id < b.doc_id ? -1 : a.doc_id > b.doc_id ? 1 : 0));
524
+ }
525
+ /** @internal Reads a key's status (or a page of one) off the wire. Null when malformed. */
526
+ export function parseInputStatus(raw) {
527
+ if (!isRecord(raw) || !isInputKey(raw['key']))
528
+ return null;
529
+ const refs = parseList(raw['refs'], parseInputReference);
530
+ const files = parseList(raw['files'], parseInputStatusFile);
531
+ const changed = parseList(raw['changed'], parseInputStatusChange);
532
+ const counts = parseCounts(raw);
533
+ const stale = parseStale(raw);
534
+ const arriving = parseArriving(raw);
535
+ if (refs === null || files === null || changed === null || counts === null || stale === null || arriving === null) {
536
+ return null;
537
+ }
538
+ return { key: raw['key'], refs, files, changed, ...counts, ...stale, ...arriving };
539
+ }
540
+ /** @internal */
541
+ export function parseInputStatusPage(raw) {
542
+ const status = parseInputStatus(raw);
543
+ if (status === null || !isRecord(raw))
544
+ return null;
545
+ const cursor = raw['cursor'];
546
+ if (cursor !== null && decodeInputStatusCursor(cursor) === null)
547
+ return null;
548
+ return { ...status, cursor: cursor };
549
+ }
550
+ /** @internal Reads a keyless status page, or the resync a moved cursor draws. Null when malformed. */
551
+ export function parseInputStatusAll(raw) {
552
+ if (!isRecord(raw))
553
+ return null;
554
+ if (raw['resync'] === true)
555
+ return isRev(raw['rev']) ? { rev: raw['rev'], resync: true } : null;
556
+ const cursor = raw['cursor'] ?? null;
557
+ if (cursor !== null && decodeInputKeysCursor(cursor) === null)
558
+ return null;
559
+ const keys = raw['keys'];
560
+ if (!Array.isArray(keys) || !keys.every(isInputKey))
561
+ return null;
562
+ const changed = parseList(raw['changed'], parseInputStatusChange);
563
+ const counts = parseCounts(raw);
564
+ const stale = parseStale(raw);
565
+ if (changed === null || counts === null || stale === null)
566
+ return null;
567
+ const keyed = [];
568
+ for (const c of changed) {
569
+ if (c.key === undefined)
570
+ return null;
571
+ keyed.push({ ...c, key: c.key });
572
+ }
573
+ return { keys: [...keys], changed: keyed, ...counts, ...stale, cursor: cursor };
574
+ }
575
+ /** @internal Reads a `since_rev` answer: a delta, a resync, or null when malformed. */
576
+ export function parseInputStatusDelta(raw) {
577
+ if (!isRecord(raw) || !isInputKey(raw['key']))
578
+ return null;
579
+ const rev = raw['rev'];
580
+ if (!isRev(rev))
581
+ return null;
582
+ if (raw['resync'] === true)
583
+ return { key: raw['key'], rev, resync: true };
584
+ const since = raw['since_rev'];
585
+ if (!isRev(since) || since > rev)
586
+ return null;
587
+ const files = parseDiff(raw['files'], parseInputStatusFile);
588
+ const changed = parseDiff(raw['changed'], parseInputStatusChange);
589
+ const counts = parseCounts(raw);
590
+ const stale = parseStale(raw);
591
+ const arriving = parseArriving(raw);
592
+ if (files === null || changed === null || counts === null || stale === null || arriving === null)
593
+ return null;
594
+ const delta = {
595
+ key: raw['key'],
596
+ since_rev: since,
597
+ rev,
598
+ files,
599
+ changed,
600
+ hidden_count: counts.hidden_count,
601
+ truncated: counts.truncated,
602
+ ...stale,
603
+ ...arriving,
604
+ };
605
+ if (raw['refs'] !== undefined) {
606
+ const refs = parseList(raw['refs'], parseInputReference);
607
+ if (refs === null)
608
+ return null;
609
+ delta.refs = refs;
610
+ }
611
+ return delta;
612
+ }
613
+ function parseInputStatusFile(raw) {
614
+ if (!isRecord(raw) || !isInputDocId(raw['doc_id']))
615
+ return null;
616
+ const updatedAt = raw['updated_at'];
617
+ if (typeof updatedAt !== 'string' || updatedAt.length > INPUT_STAMP_MAX_CHARS)
618
+ return null;
619
+ const name = raw['name'];
620
+ if (typeof name !== 'string')
621
+ return null;
622
+ const from = parseFrom(raw['from']);
623
+ const ref = parseInputReference(raw['ref']);
624
+ if (from === null || ref === null)
625
+ return null;
626
+ const schemaType = raw['schema_type'];
627
+ if (schemaType !== undefined && typeof schemaType !== 'string')
628
+ return null;
629
+ const awaitsImport = raw['awaits_import'];
630
+ if (awaitsImport !== undefined && awaitsImport !== true)
631
+ return null;
632
+ return {
633
+ doc_id: raw['doc_id'],
634
+ name,
635
+ ...(schemaType !== undefined ? { schema_type: schemaType } : {}),
636
+ updated_at: updatedAt,
637
+ from,
638
+ ref,
639
+ ...(awaitsImport === true ? { awaits_import: true } : {}),
640
+ };
641
+ }
642
+ function parseInputStatusChange(raw) {
643
+ if (!isRecord(raw) || !isInputDocId(raw['doc_id']))
644
+ return null;
645
+ const change = raw['change'];
646
+ if (!INPUT_FILE_CHANGES.includes(change))
647
+ return null;
648
+ const from = parseFrom(raw['from']);
649
+ if (from === null)
650
+ return null;
651
+ const out = { doc_id: raw['doc_id'], change: change, from };
652
+ if (raw['key'] !== undefined) {
653
+ if (!isInputKey(raw['key']))
654
+ return null;
655
+ out.key = raw['key'];
656
+ }
657
+ if (raw['updated_at'] !== undefined) {
658
+ const at = raw['updated_at'];
659
+ if (typeof at !== 'string' || at.length > INPUT_STAMP_MAX_CHARS)
660
+ return null;
661
+ out.updated_at = at;
662
+ }
663
+ return out;
664
+ }
665
+ function parseFrom(raw) {
666
+ return Array.isArray(raw) && raw.every(isInputReferenceId) ? [...raw] : null;
667
+ }
668
+ function parseCounts(raw) {
669
+ const hidden = raw['hidden_count'];
670
+ const truncated = raw['truncated'];
671
+ const rev = raw['rev'];
672
+ if (!isRev(hidden) || typeof truncated !== 'boolean' || !isRev(rev))
673
+ return null;
674
+ return { hidden_count: hidden, truncated, rev };
675
+ }
676
+ // `stale` is optional on the wire and carried only when true.
677
+ function parseStale(raw) {
678
+ const stale = raw['stale'];
679
+ if (stale === undefined || stale === false)
680
+ return {};
681
+ return stale === true ? { stale: true } : null;
682
+ }
683
+ // `arriving` is optional on the wire and carried only when true.
684
+ function parseArriving(raw) {
685
+ const arriving = raw['arriving'];
686
+ if (arriving === undefined || arriving === false)
687
+ return {};
688
+ return arriving === true ? { arriving: true } : null;
689
+ }
690
+ function parseDiff(raw, parse) {
691
+ if (!isRecord(raw))
692
+ return null;
693
+ const upsert = parseList(raw['upsert'], parse);
694
+ const remove = raw['remove'];
695
+ if (upsert === null || !Array.isArray(remove) || !remove.every(isInputDocId))
696
+ return null;
697
+ return { upsert, remove: [...remove] };
698
+ }
699
+ function parseList(raw, parse) {
700
+ if (!Array.isArray(raw))
701
+ return null;
702
+ const out = [];
703
+ for (const entry of raw) {
704
+ const parsed = parse(entry);
705
+ if (parsed === null)
706
+ return null;
707
+ out.push(parsed);
708
+ }
709
+ return out;
710
+ }
711
+ function isRev(value) {
712
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
713
+ }
714
+ // --- Live nudge (WS) --------------------------------------------------------
715
+ /** @internal */
716
+ export const INPUT_STATUS_FRAME_TYPE = 'doc_input_status';
717
+ /** @internal */
718
+ export function inputStatusNudgeFrame(docId, lineId, key, rev) {
719
+ return { type: INPUT_STATUS_FRAME_TYPE, topic: documentTopic(docId, lineId), key, rev };
720
+ }
721
+ /** @internal Reads a nudge frame. Null for any other frame or a malformed one. */
722
+ export function parseInputStatusNudge(raw) {
723
+ if (!isRecord(raw) || raw['type'] !== INPUT_STATUS_FRAME_TYPE)
724
+ return null;
725
+ const topic = typeof raw['topic'] === 'string' ? parseDocTopic(raw['topic']) : null;
726
+ if (topic === null || !isInputKey(raw['key']) || !isRev(raw['rev']))
727
+ return null;
728
+ return { docId: topic.docId, lineId: topic.branchId, key: raw['key'], rev: raw['rev'] };
729
+ }
730
+ /** @internal */
731
+ export function parseProvisionalInputsAnswer(raw) {
732
+ if (!isRecord(raw) || !isInputKey(raw['key']) || !isRev(raw['rev']))
733
+ return null;
734
+ if (typeof raw['written'] !== 'boolean')
735
+ return null;
736
+ return { key: raw['key'], rev: raw['rev'], written: raw['written'] };
737
+ }
738
+ /**
739
+ * @internal
740
+ * Reads a provisional body for document `docId`. Refuses a reference to any
741
+ * stream of `docId` itself, and references that repeat one identity.
742
+ */
743
+ export function parseProvisionalInputsBody(raw, opts) {
744
+ if (!isRecord(raw))
745
+ return fail('Body must be an object');
746
+ const { branch_id: lineId, op_seq: opSeq } = raw;
747
+ if (!isInputLineId(lineId))
748
+ return fail('branch_id is not a valid line id');
749
+ if (typeof opSeq !== 'number' || !Number.isSafeInteger(opSeq) || opSeq < 1) {
750
+ return fail('op_seq must be a positive integer');
751
+ }
752
+ const refsRaw = raw['refs'];
753
+ if (!Array.isArray(refsRaw))
754
+ return fail('refs must be an array');
755
+ if (refsRaw.length > INPUT_REFS_MAX_PER_KEY)
756
+ return fail('Too many references');
757
+ const refs = [];
758
+ const seen = new Set();
759
+ for (const entry of refsRaw) {
760
+ const ref = parseInputReference(entry);
761
+ if (ref === null)
762
+ return fail('refs holds an invalid reference');
763
+ if (ref.extent === 'document' && ref.doc_id === opts.docId) {
764
+ return fail('A document cannot reference itself');
765
+ }
766
+ const id = inputReferenceId(ref);
767
+ if (seen.has(id))
768
+ return fail('refs repeats a reference');
769
+ seen.add(id);
770
+ refs.push(ref);
771
+ }
772
+ const readRaw = raw['read'];
773
+ if (!isRecord(readRaw))
774
+ return fail('read must be an object');
775
+ const docIds = Object.keys(readRaw);
776
+ if (docIds.length > INPUT_READ_MARKS_MAX_PER_KEY)
777
+ return fail('Too many read marks');
778
+ const read = {};
779
+ for (const docId of docIds) {
780
+ const mark = parseInputReadMark(readRaw[docId]);
781
+ if (!isInputDocId(docId) || mark === null)
782
+ return fail('read holds an invalid mark');
783
+ read[docId] = mark;
784
+ }
785
+ return { ok: true, value: { branch_id: lineId, op_seq: opSeq, refs, read } };
786
+ }
787
+ function fail(error) {
788
+ return { ok: false, error };
789
+ }
790
+ function isRecord(value) {
791
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
792
+ }
793
+ //# sourceMappingURL=inputs.js.map