@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
@@ -0,0 +1,693 @@
1
+ // `subscribeInputDocuments`: calls an app's handlers with the input documents
2
+ // of a key, each as a handle the app may open in any mode. `loaded` gets every
3
+ // input wherever the document is open; `changed` gets the inputs changed since
4
+ // the document last read them, where it can be edited, and records each as
5
+ // read once its handler resolves; `removed` gets each input that left.
6
+ import { inputStampFromUpdatedAt } from './inputs.js';
7
+ import { inDevelopment } from './in-development.js';
8
+ import { importerWaitRejections } from './importer-wait.js';
9
+ /** A failed file waits this long before its first retry at the same
10
+ * version, doubling up to the ceiling. A new version is tried at once. A file
11
+ * refused `not_imported` is tried again when its import lands instead. */
12
+ const RETRY_FIRST_MS = 2_000;
13
+ const RETRY_CEILING_MS = 5 * 60_000;
14
+ /** `settled` waits for this long with no call starting or finishing and no
15
+ * change to the set, so a burst of arrivals reaches it once. */
16
+ const QUIET_MS = 2_000;
17
+ /** The handlers that run at once when the app sets no `concurrency`. Each
18
+ * handler usually opens its input, and a few hundred opens started together
19
+ * are refused as throttled, for this app and every other caller. */
20
+ const DEFAULT_CONCURRENCY = 8;
21
+ /** A call pending this long is reported in development builds: the input
22
+ * gets no further call until it settles, and it keeps its concurrency slot. */
23
+ const LONG_PENDING_MS = 60_000;
24
+ /** On the input a shared follower hands out: `open` on behalf of one
25
+ * subscription, so a copy another subscription is using in the same call is
26
+ * not replaced. */
27
+ const OPEN_FOR = Symbol('openFor');
28
+ /** Options that open the same copy compare equal. */
29
+ const optionsKey = (options) => JSON.stringify(Object.entries(options ?? {}).filter(([, v]) => v !== undefined).sort(([a], [b]) => (a < b ? -1 : 1)));
30
+ const failKeyOf = (w) => `${w.kind}:${w.docId}@${w.version}`;
31
+ const newer = (version, than) => {
32
+ if (than === null)
33
+ return true;
34
+ const a = inputStampFromUpdatedAt(version);
35
+ const b = inputStampFromUpdatedAt(than);
36
+ return a !== null && b !== null ? a > b : version !== than;
37
+ };
38
+ /** Whether a copy was loaded at a later version than the call it was opened for. */
39
+ const openedAhead = (copyAt, callAt) => {
40
+ if (copyAt === null)
41
+ return false;
42
+ const a = inputStampFromUpdatedAt(copyAt);
43
+ const b = inputStampFromUpdatedAt(callAt);
44
+ return a !== null && b !== null && a > b;
45
+ };
46
+ /** @internal */
47
+ export function followInputDocuments(host, key, handlers, options = {}) {
48
+ checkOptions(options);
49
+ return follow(host, key, handlers, () => options).stop;
50
+ }
51
+ const checkOptions = (options) => {
52
+ if (options.concurrency !== undefined && !(options.concurrency >= 1)) {
53
+ throw new TypeError('subscribeInputDocuments: concurrency must be at least 1');
54
+ }
55
+ };
56
+ function follow(host, key, handlers, options) {
57
+ const has = (name) => typeof handlers[name] === 'function';
58
+ // With none of the file handlers, the list alone decides `settled`.
59
+ const followsChanges = () => has('changed') || has('removed');
60
+ let stopped = false;
61
+ let latest = null;
62
+ // One call per file at a time; the file's next work is taken up once it
63
+ // settles.
64
+ const running = new Map();
65
+ // `<kind>:<doc_id>@<version>` -> the failed attempts at that version and
66
+ // when the next may run, or that it waits for the file's import. A call
67
+ // that failed has finished, so it does not hold `settled`.
68
+ const failed = new Map();
69
+ let retryTimer = null;
70
+ let quietTimer = null;
71
+ // `<kind>:<doc_id>` -> the versions it was last handled at. For `changed`
72
+ // this stands in for the read record until the status catches up with it.
73
+ const done = new Map();
74
+ // Files `loaded` was given this session, so their removal reaches `removed`.
75
+ const seen = new Set();
76
+ // The copies open per file.
77
+ const copies = new Map();
78
+ // Moves when a handler set joins, so a `loaded` call that started before
79
+ // it does not count as given to the newcomer.
80
+ let joins = 0;
81
+ const isDone = (w) => {
82
+ const versions = done.get(`${w.kind}:${w.docId}`);
83
+ if (versions === undefined)
84
+ return false;
85
+ if (versions.includes(w.version))
86
+ return true;
87
+ const stamp = inputStampFromUpdatedAt(w.version);
88
+ return (stamp !== null &&
89
+ versions.some((v) => {
90
+ const at = inputStampFromUpdatedAt(v);
91
+ return at !== null && stamp <= at;
92
+ }));
93
+ };
94
+ const closeCopy = (docId, by) => {
95
+ const opened = copies.get(docId);
96
+ if (opened === undefined)
97
+ return;
98
+ copies.delete(docId);
99
+ opened.closedBy = by;
100
+ try {
101
+ opened.copy.close();
102
+ }
103
+ catch {
104
+ // Already closed by the app.
105
+ }
106
+ };
107
+ // A handle that warns, once, when it is used after its copy closed.
108
+ const guard = (docId, copy, opened) => {
109
+ let warned = false;
110
+ const bound = new Map();
111
+ return new Proxy(copy, {
112
+ get(target, prop) {
113
+ const by = opened().closedBy;
114
+ if (by !== null && !warned && typeof prop === 'string' && prop !== 'then' && prop !== 'close') {
115
+ warned = true;
116
+ console.warn(`[brass] input ${docId}: the document open returned was closed ${by}, so it no longer follows the input.`);
117
+ }
118
+ if (prop === 'close') {
119
+ return () => {
120
+ if (copies.get(docId) === opened())
121
+ closeCopy(docId, 'by close()');
122
+ else
123
+ target.close();
124
+ };
125
+ }
126
+ const value = Reflect.get(target, prop, target);
127
+ if (typeof value !== 'function')
128
+ return value;
129
+ let fn = bound.get(prop);
130
+ if (fn === undefined) {
131
+ fn = value.bind(target);
132
+ bound.set(prop, fn);
133
+ }
134
+ return fn;
135
+ },
136
+ });
137
+ };
138
+ // The work a status asks for, before subtracting what is done or running.
139
+ const workOf = (status) => {
140
+ const out = [];
141
+ const listed = new Set();
142
+ if (has('loaded')) {
143
+ for (const f of status.inputs) {
144
+ listed.add(f.doc_id);
145
+ out.push({ docId: f.doc_id, kind: 'loaded', version: f.updated_at, from: f.from, entry: null });
146
+ }
147
+ }
148
+ else {
149
+ for (const f of status.inputs)
150
+ listed.add(f.doc_id);
151
+ }
152
+ const removing = new Set();
153
+ if (followsChanges()) {
154
+ const writer = host.canWrite();
155
+ for (const c of status.changed) {
156
+ if (c.change === 'removed') {
157
+ // A copy that cannot record the removal still reports a file
158
+ // `loaded` was given, below.
159
+ if (!writer && seen.has(c.doc_id))
160
+ continue;
161
+ removing.add(c.doc_id);
162
+ out.push({ docId: c.doc_id, kind: 'removed', version: `removed:${c.updated_at ?? ''}`, from: c.from, entry: c });
163
+ }
164
+ else if (has('changed')) {
165
+ const version = status.inputs.find((f) => f.doc_id === c.doc_id)?.updated_at ?? '';
166
+ out.push({ docId: c.doc_id, kind: 'changed', version, from: c.from, entry: c });
167
+ }
168
+ }
169
+ }
170
+ for (const docId of seen) {
171
+ if (!listed.has(docId) && !removing.has(docId)) {
172
+ out.push({ docId, kind: 'removed', version: 'gone', from: [], entry: null });
173
+ }
174
+ }
175
+ return out;
176
+ };
177
+ // `loaded` and a removal of a file it was given run on any copy; work that
178
+ // records a read runs where the document can be written.
179
+ const canRun = (w) => !stopped && (w.entry === null || host.canWrite());
180
+ const scheduleRetry = () => {
181
+ if (stopped)
182
+ return;
183
+ let next = Infinity;
184
+ for (const f of failed.values())
185
+ next = Math.min(next, f.retryAt);
186
+ if (next === Infinity)
187
+ return;
188
+ if (retryTimer !== null)
189
+ clearTimeout(retryTimer);
190
+ retryTimer = setTimeout(() => {
191
+ retryTimer = null;
192
+ if (latest !== null)
193
+ start(latest);
194
+ }, Math.max(0, next - Date.now()));
195
+ };
196
+ // Whether the status lists the file as not imported yet.
197
+ const awaitsImport = (status, docId) => status.inputs.find((f) => f.doc_id === docId)?.awaits_import === true;
198
+ const fail = (failKey, untilImported, untilImporter = false) => {
199
+ const attempts = (failed.get(failKey)?.attempts ?? 0) + 1;
200
+ if (untilImporter) {
201
+ failed.set(failKey, { attempts, retryAt: Infinity, awaitsImporter: true });
202
+ return;
203
+ }
204
+ if (untilImported) {
205
+ failed.set(failKey, { attempts, retryAt: Infinity, awaitsImport: true });
206
+ return;
207
+ }
208
+ const wait = Math.min(RETRY_FIRST_MS * 2 ** (attempts - 1), RETRY_CEILING_MS);
209
+ failed.set(failKey, { attempts, retryAt: Date.now() + wait });
210
+ scheduleRetry();
211
+ };
212
+ const forget = (docId) => {
213
+ seen.delete(docId);
214
+ done.delete(`loaded:${docId}`);
215
+ done.delete(`changed:${docId}`);
216
+ closeCopy(docId, 'because the input was removed');
217
+ };
218
+ // `retry` is a further call at a version whose call already finished: it
219
+ // changes what `settled` reports only if it resolves.
220
+ const run = (status, work, failKey, retry) => {
221
+ const { docId, kind } = work;
222
+ const abort = new AbortController();
223
+ const joinsAtStart = joins;
224
+ // Reported whether or not a newer version abandoned the call, since an
225
+ // abandoned call still holds the input and its slot.
226
+ const pendingTimer = inDevelopment()
227
+ ? setTimeout(() => {
228
+ console.warn(`[brass] subscribeInputDocuments("${key}"): the ${kind} handler for input ${docId} has not settled after ${LONG_PENDING_MS / 1000}s. The input gets no other call until it resolves or rejects.`);
229
+ }, LONG_PENDING_MS)
230
+ : null;
231
+ running.set(docId, { work, abort, pendingTimer });
232
+ if (!retry)
233
+ touch();
234
+ let resolved = false;
235
+ // The copies this call opened, which it records the read at, and which
236
+ // subscriptions each was handed to.
237
+ const openedHere = [];
238
+ const users = new Map();
239
+ // Copies opened for this call alone, beside one another subscription uses.
240
+ const ownCopies = [];
241
+ const use = (opened, owner) => {
242
+ if (!openedHere.includes(opened))
243
+ openedHere.push(opened);
244
+ let by = users.get(opened);
245
+ if (by === undefined)
246
+ users.set(opened, (by = new Set()));
247
+ by.add(owner);
248
+ return opened;
249
+ };
250
+ let opening = null;
251
+ let settled = false;
252
+ let result;
253
+ let callAt = work.version;
254
+ try {
255
+ if (kind === 'removed') {
256
+ result = handlers.removed?.(docId);
257
+ }
258
+ else {
259
+ const handler = kind === 'loaded' ? handlers.loaded : handlers.changed;
260
+ const file = status.inputs.find((f) => f.doc_id === docId);
261
+ const updatedAt = file?.updated_at ?? work.version;
262
+ callAt = updatedAt;
263
+ if (kind === 'loaded')
264
+ seen.add(docId);
265
+ const refuseLate = () => {
266
+ if (stopped)
267
+ throw new Error('open: the subscription ended');
268
+ if (settled)
269
+ throw new Error(`open: call it before the ${kind} handler resolves`);
270
+ };
271
+ const openFor = (owner) => (async (openOptions) => {
272
+ const openById = host.open;
273
+ if (openById === null)
274
+ throw new Error('open: this document cannot open its inputs');
275
+ const wanted = optionsKey(openOptions);
276
+ // A concurrent open with the same options shares the one in flight;
277
+ // one with other options waits for it, then replaces it.
278
+ while (opening !== null) {
279
+ const pending = opening;
280
+ if (pending.options === wanted)
281
+ return use(await pending.opened, owner).handle;
282
+ await pending.opened.catch(() => undefined);
283
+ }
284
+ const existing = [copies.get(docId), ...ownCopies].find((o) => o !== undefined && o.options === wanted);
285
+ if (existing !== undefined) {
286
+ if (newer(updatedAt, existing.copy.updatedAt))
287
+ await existing.copy.reload?.();
288
+ return use(existing, owner).handle;
289
+ }
290
+ refuseLate();
291
+ const entry = {
292
+ options: wanted,
293
+ opened: (async () => {
294
+ const copy = (await openById(docId, openOptions));
295
+ if (settled || stopped) {
296
+ copy.close();
297
+ refuseLate();
298
+ }
299
+ const opened = { options: wanted, copy, handle: copy, closedBy: null };
300
+ opened.handle = guard(docId, copy, () => opened);
301
+ const current = copies.get(docId);
302
+ const sharedWith = current === undefined ? [] : Array.from(users.get(current) ?? []);
303
+ if (sharedWith.some((u) => u !== owner)) {
304
+ ownCopies.push(opened);
305
+ }
306
+ else {
307
+ // A copy opened with other options replaces the one open before.
308
+ closeCopy(docId, 'when the input was opened with other options');
309
+ copies.set(docId, opened);
310
+ }
311
+ return use(opened, owner);
312
+ })(),
313
+ };
314
+ opening = entry;
315
+ try {
316
+ return (await entry.opened).handle;
317
+ }
318
+ finally {
319
+ if (opening === entry)
320
+ opening = null;
321
+ }
322
+ });
323
+ // The owner a direct handler's opens are made for.
324
+ const sole = {};
325
+ const input = {
326
+ doc_id: docId,
327
+ name: file?.name ?? '',
328
+ ...(file?.family !== undefined ? { family: file.family } : {}),
329
+ updated_at: updatedAt,
330
+ from: work.from,
331
+ open: openFor(sole),
332
+ close: () => closeCopy(docId, 'by input.close()'),
333
+ [OPEN_FOR]: openFor,
334
+ };
335
+ result = handler?.(input);
336
+ }
337
+ }
338
+ catch (err) {
339
+ result = Promise.reject(err);
340
+ }
341
+ void Promise.resolve(result)
342
+ .then(() => {
343
+ if (stopped || abort.signal.aborted)
344
+ return;
345
+ if (work.entry === null) {
346
+ if (kind === 'removed')
347
+ forget(docId);
348
+ else if (joins === joinsAtStart)
349
+ done.set(`loaded:${docId}`, [work.version]);
350
+ failed.delete(failKey);
351
+ resolved = true;
352
+ return;
353
+ }
354
+ if (!host.canWrite())
355
+ return;
356
+ try {
357
+ if (kind === 'removed') {
358
+ host.markRead({ key, changed: [work.entry], inputs: [] }, { files: [], undoable: false });
359
+ forget(docId);
360
+ done.set(`removed:${docId}`, [work.version]);
361
+ }
362
+ else {
363
+ const read = openedHere.map((o) => o.copy);
364
+ // Recorded at the call's version when a copy is ahead of it, so
365
+ // the newer version gets a call of its own.
366
+ const ahead = read.some((c) => openedAhead(c.updatedAt, callAt));
367
+ const files = ahead || read.length === 0 ? [{ doc_id: docId }] : read;
368
+ host.markRead(status, { files, undoable: false });
369
+ const versions = ahead ? [] : read.map((c) => c.updatedAt).filter((v) => v !== null);
370
+ done.set(`changed:${docId}`, [work.version, ...versions]);
371
+ }
372
+ failed.delete(failKey);
373
+ resolved = true;
374
+ }
375
+ catch {
376
+ // The document closed or stopped being writable while the handler
377
+ // ran, or refused the record; the file stays changed and waits
378
+ // like a rejected one, so a refusal cannot spin.
379
+ fail(failKey, false);
380
+ }
381
+ }, (err) => {
382
+ // Work abandoned on purpose is not a failure: its replacement, if
383
+ // any, is already listed.
384
+ if (stopped || abort.signal.aborted)
385
+ return;
386
+ // A file only another app imports is called again when the status
387
+ // reports its import landed, rather than on a timer.
388
+ const notImported = err?.code === 'not_imported';
389
+ // This app imports the file and has not registered its importer:
390
+ // the import waits for the registration rather than for the status.
391
+ const untilImporter = typeof err === 'object' && err !== null && importerWaitRejections.has(err);
392
+ fail(failKey, notImported && kind !== 'removed' && awaitsImport(latest ?? status, docId), untilImporter && kind !== 'removed');
393
+ if (!notImported) {
394
+ console.error(`[brass] input ${kind} handler for ${docId} failed`, err);
395
+ }
396
+ })
397
+ .finally(() => {
398
+ settled = true;
399
+ if (pendingTimer !== null)
400
+ clearTimeout(pendingTimer);
401
+ for (const o of ownCopies) {
402
+ o.closedBy = `when the ${kind} handler resolved`;
403
+ try {
404
+ o.copy.close();
405
+ }
406
+ catch {
407
+ // Already closed by the app.
408
+ }
409
+ }
410
+ if (options().keepDocumentsOpen !== true) {
411
+ for (const o of openedHere) {
412
+ if (copies.get(docId) === o)
413
+ closeCopy(docId, `when the ${kind} handler resolved (pass keepDocumentsOpen to keep it)`);
414
+ }
415
+ }
416
+ running.delete(docId);
417
+ if (!stopped && !abort.signal.aborted && (!retry || resolved))
418
+ touch();
419
+ if (latest !== null)
420
+ start(latest);
421
+ });
422
+ };
423
+ // Abandons running work the status no longer asks for: the file left, or a
424
+ // newer version of it is listed. Its result is not recorded.
425
+ const abandon = (status) => {
426
+ if (running.size === 0)
427
+ return;
428
+ const versions = new Map(status.inputs.map((f) => [f.doc_id, f.updated_at]));
429
+ for (const { work, abort } of running.values()) {
430
+ if (work.kind === 'removed')
431
+ continue;
432
+ const now = versions.get(work.docId);
433
+ if (now === undefined || now !== work.version)
434
+ abort.abort();
435
+ }
436
+ };
437
+ const start = (status) => {
438
+ const now = Date.now();
439
+ for (const work of workOf(status)) {
440
+ if (running.size >= (options().concurrency ?? DEFAULT_CONCURRENCY))
441
+ return;
442
+ if (!canRun(work) || running.has(work.docId) || isDone(work))
443
+ continue;
444
+ const failKey = failKeyOf(work);
445
+ const f = failed.get(failKey);
446
+ if (f !== undefined) {
447
+ // An input refused for want of this page's importer runs again when
448
+ // the importer registers or when another client's import lands.
449
+ const waiting = f.awaitsImporter === true || f.awaitsImport === true
450
+ ? awaitsImport(status, work.docId)
451
+ : now < f.retryAt;
452
+ if (waiting)
453
+ continue;
454
+ }
455
+ run(status, work, failKey, f !== undefined);
456
+ }
457
+ };
458
+ // Settled once the file list is complete, every call it asks for has
459
+ // finished (resolved or rejected), and nothing has started, finished or
460
+ // changed for `QUIET_MS`. Work not yet called, or called and still running
461
+ // for the first time at its version, holds it; so does a change no writer
462
+ // has read yet.
463
+ let reportedSettled = false;
464
+ let delivering = false;
465
+ let lastShaped = null;
466
+ const outstanding = (status) => workOf(status).some((w) => !isDone(w) && !failed.has(failKeyOf(w)));
467
+ const emit = (status) => {
468
+ const reported = { ...status, settled: reportedSettled };
469
+ handlers.status?.(reported);
470
+ return reported;
471
+ };
472
+ const quiet = () => {
473
+ quietTimer = null;
474
+ if (stopped || latest === null || !latest.settled || outstanding(latest))
475
+ return;
476
+ reportedSettled = true;
477
+ const reported = emit(latest);
478
+ handlers.settled?.(reported);
479
+ };
480
+ // Something started, finished or changed: `settled` reads false until the
481
+ // set has been quiet again.
482
+ function touch() {
483
+ if (stopped)
484
+ return;
485
+ if (quietTimer !== null)
486
+ clearTimeout(quietTimer);
487
+ quietTimer = setTimeout(quiet, QUIET_MS);
488
+ if (!reportedSettled)
489
+ return;
490
+ reportedSettled = false;
491
+ if (!delivering && latest !== null)
492
+ emit(latest);
493
+ }
494
+ // What a status asks of the follower.
495
+ const shapeOf = (status) => JSON.stringify([
496
+ status.settled,
497
+ status.inputs.map((f) => [f.doc_id, f.updated_at, f.awaits_import === true]),
498
+ workOf(status).filter((w) => !isDone(w)).map(failKeyOf),
499
+ ]);
500
+ const stopStatus = host.subscribeStatus(key, (status) => {
501
+ latest = status;
502
+ const first = lastShaped === null;
503
+ delivering = true;
504
+ try {
505
+ abandon(status);
506
+ start(status);
507
+ // Both read against what is done now, so a change this document has
508
+ // just read compares equal to the status that drops it.
509
+ const changed = !first && shapeOf(lastShaped) !== shapeOf(status);
510
+ lastShaped = status;
511
+ if (changed)
512
+ touch();
513
+ }
514
+ finally {
515
+ delivering = false;
516
+ }
517
+ if (stopped)
518
+ return;
519
+ // A first status that starts no call has nothing to wait out.
520
+ const settleNow = first && quietTimer === null && status.settled && !outstanding(status);
521
+ if (settleNow)
522
+ reportedSettled = true;
523
+ const reported = emit(status);
524
+ if (settleNow)
525
+ handlers.settled?.(reported);
526
+ });
527
+ const stop = () => {
528
+ if (stopped)
529
+ return;
530
+ stopped = true;
531
+ if (retryTimer !== null)
532
+ clearTimeout(retryTimer);
533
+ if (quietTimer !== null)
534
+ clearTimeout(quietTimer);
535
+ for (const { abort, pendingTimer } of running.values()) {
536
+ abort.abort();
537
+ if (pendingTimer !== null)
538
+ clearTimeout(pendingTimer);
539
+ }
540
+ for (const docId of copies.keys())
541
+ closeCopy(docId, 'when the app unsubscribed');
542
+ stopStatus();
543
+ };
544
+ const join = (joined) => {
545
+ if (stopped || latest === null)
546
+ return;
547
+ const reported = { ...latest, settled: reportedSettled };
548
+ joined.status?.(reported);
549
+ if (typeof joined.loaded === 'function') {
550
+ joins++;
551
+ for (const k of done.keys())
552
+ if (k.startsWith('loaded:'))
553
+ done.delete(k);
554
+ }
555
+ const quietBefore = quietTimer;
556
+ start(latest);
557
+ // Nothing started for the newcomer, so the set is as settled as it was.
558
+ if (reportedSettled && quietTimer === quietBefore)
559
+ joined.settled?.(reported);
560
+ };
561
+ const importerRegistered = () => {
562
+ if (stopped)
563
+ return;
564
+ let woke = false;
565
+ for (const [failKey, f] of failed) {
566
+ if (f.awaitsImporter !== true)
567
+ continue;
568
+ failed.set(failKey, { attempts: f.attempts, retryAt: 0 });
569
+ woke = true;
570
+ }
571
+ if (woke && latest !== null)
572
+ start(latest);
573
+ };
574
+ return { stop, join, importerRegistered };
575
+ }
576
+ // The input as one subscription sees it: its `open` is that subscription's.
577
+ const inputFor = (input, set) => {
578
+ const openFor = input[OPEN_FOR];
579
+ return openFor === undefined ? input : { ...input, open: openFor(set) };
580
+ };
581
+ // Calls `name` on every set that has not resolved it at `version`, and
582
+ // rejects with the first rejection once all settle, so a retry reaches only
583
+ // the sets that failed.
584
+ async function callEach(sets, name, docId, version, call) {
585
+ const given = `${name}:${docId}`;
586
+ const due = Array.from(sets).filter((s) => typeof s.handlers[name] === 'function' && s.given.get(given) !== version);
587
+ const results = await Promise.allSettled(due.map(async (set) => {
588
+ await call(set);
589
+ set.given.set(given, version);
590
+ if (name === 'removed') {
591
+ set.given.delete(`loaded:${docId}`);
592
+ set.given.delete(`changed:${docId}`);
593
+ }
594
+ else {
595
+ set.given.delete(`removed:${docId}`);
596
+ }
597
+ }));
598
+ const failure = results.find((r) => r.status === 'rejected');
599
+ if (failure !== undefined)
600
+ throw failure.reason;
601
+ if (name === 'removed')
602
+ for (const set of due)
603
+ set.given.delete(given);
604
+ }
605
+ /** @internal The `subscribeInputDocuments` followers of one document: one
606
+ * per key, whatever number of subscriptions share it. Each input's work runs
607
+ * once and every subscription's handlers are called for it; a `changed`
608
+ * input is recorded as read once all of them resolve. */
609
+ export class InputFollowers {
610
+ byKey = new Map();
611
+ subscribe(host, key, handlers, options = {}) {
612
+ checkOptions(options);
613
+ const set = { handlers, options, given: new Map() };
614
+ let shared = this.byKey.get(key);
615
+ if (shared === undefined) {
616
+ const sets = new Set([set]);
617
+ const has = (name) => Array.from(sets).some((s) => typeof s.handlers[name] === 'function');
618
+ // Called on a snapshot of the sets, skipping one that left during an
619
+ // earlier set's handler.
620
+ const each = (fn) => {
621
+ for (const s of Array.from(sets))
622
+ if (sets.has(s))
623
+ fn(s);
624
+ };
625
+ // The tightest `concurrency` any subscription asks for, and documents
626
+ // kept open if any asks.
627
+ const merged = () => {
628
+ let concurrency;
629
+ let keepDocumentsOpen = false;
630
+ for (const s of sets) {
631
+ const c = s.options.concurrency;
632
+ if (c !== undefined)
633
+ concurrency = concurrency === undefined ? c : Math.min(concurrency, c);
634
+ if (s.options.keepDocumentsOpen === true)
635
+ keepDocumentsOpen = true;
636
+ }
637
+ return { ...(concurrency !== undefined ? { concurrency } : {}), keepDocumentsOpen };
638
+ };
639
+ const combined = {
640
+ get loaded() {
641
+ if (!has('loaded'))
642
+ return undefined;
643
+ return (input) => callEach(sets, 'loaded', input.doc_id, input.updated_at, (s) => s.handlers.loaded(inputFor(input, s)));
644
+ },
645
+ get changed() {
646
+ if (!has('changed'))
647
+ return undefined;
648
+ return (input) => callEach(sets, 'changed', input.doc_id, input.updated_at, (s) => s.handlers.changed(inputFor(input, s)));
649
+ },
650
+ get removed() {
651
+ if (!has('removed'))
652
+ return undefined;
653
+ return (docId) => callEach(sets, 'removed', docId, '', (s) => s.handlers.removed(docId));
654
+ },
655
+ status: (status) => each((s) => s.handlers.status?.(status)),
656
+ settled: (status) => each((s) => s.handlers.settled?.(status)),
657
+ };
658
+ shared = { sets, follower: follow(host, key, combined, merged) };
659
+ this.byKey.set(key, shared);
660
+ }
661
+ else {
662
+ shared.sets.add(set);
663
+ shared.follower.join(handlers);
664
+ }
665
+ const owner = shared;
666
+ let left = false;
667
+ return () => {
668
+ if (left)
669
+ return;
670
+ left = true;
671
+ owner.sets.delete(set);
672
+ if (owner.sets.size > 0)
673
+ return;
674
+ if (this.byKey.get(key) === owner)
675
+ this.byKey.delete(key);
676
+ owner.follower.stop();
677
+ };
678
+ }
679
+ /** Runs again, in every follower, the inputs whose open stopped waiting
680
+ * for `registerImporter`. */
681
+ importerRegistered() {
682
+ for (const shared of this.byKey.values())
683
+ shared.follower.importerRegistered();
684
+ }
685
+ /** Stops every follower, as the document closes or parks. */
686
+ stopAll() {
687
+ const all = [...this.byKey.values()];
688
+ this.byKey.clear();
689
+ for (const shared of all)
690
+ shared.follower.stop();
691
+ }
692
+ }
693
+ //# sourceMappingURL=input-documents.js.map