@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,274 @@
1
+ // What lets a listing notice that its set moved (`DocumentList.onChanged`).
2
+ //
3
+ // A feed frame is a trigger and the listing is the truth. The socket feeds
4
+ // that cover a query's references say THAT something in their scope moved;
5
+ // the listing decides what the set is, so folder ancestry, archive members,
6
+ // type matching and the reader's own access are decided in one place, the
7
+ // server's listing, and never re-derived from frames here.
8
+
9
+ import type {
10
+ DocumentListing,
11
+ ListDocumentsQuery,
12
+ } from './documents-namespace.js';
13
+ import type { InputReference } from './hosted-ui.js';
14
+ import type { SubscribeOptions, WsTopicCallback } from './ws-client.js';
15
+
16
+ /** The slice of the socket client a subscription uses. */
17
+ export interface DocumentSetSocket {
18
+ subscribe(
19
+ topic: string,
20
+ callback: WsTopicCallback,
21
+ options?: SubscribeOptions,
22
+ ): () => void;
23
+ }
24
+
25
+ /**
26
+ * The feed topic that covers each reference: `organization:<id>`,
27
+ * `project:<id>`, or `reference:<id>` for a document, which fires when the
28
+ * document or anything filed under it changes.
29
+ */
30
+ export function feedTopicsFor(refs: readonly InputReference[]): string[] {
31
+ const topics = new Set<string>();
32
+ for (const ref of refs) {
33
+ if (ref.extent === 'organization') {
34
+ topics.add(`organization:${ref.organization_id}`);
35
+ } else if (ref.extent === 'project' && ref.project_id) {
36
+ topics.add(`project:${ref.project_id}`);
37
+ } else if (ref.extent === 'document' && ref.doc_id) {
38
+ topics.add(`reference:${ref.doc_id}`);
39
+ }
40
+ }
41
+ return [...topics];
42
+ }
43
+
44
+ // Frames the socket client hands a topic's callback that report on the
45
+ // subscription itself rather than on anything in its scope.
46
+ const CONTROL_FRAMES = new Set([
47
+ 'subscribed',
48
+ 'subscribed_ack',
49
+ 'unsubscribed',
50
+ 'not_subscribed',
51
+ 'server_ended',
52
+ 'error',
53
+ 'pong',
54
+ ]);
55
+
56
+ /** Whether a frame on a feed could have moved what a listing answers: any
57
+ * data frame does, whatever its type, since the listing decides. */
58
+ export function frameCouldMoveSet(frame: unknown): boolean {
59
+ if (frame === null || typeof frame !== 'object') return false;
60
+ const type = (frame as { type?: unknown }).type;
61
+ return typeof type === 'string' && !CONTROL_FRAMES.has(type);
62
+ }
63
+
64
+ /** Whether two listings differ, keyed by `doc_id`: a document joined, left,
65
+ * or has a different `updated_at` or work `status`. */
66
+ export function listingsDiffer(
67
+ previous: readonly DocumentListing[],
68
+ next: readonly DocumentListing[],
69
+ ): boolean {
70
+ if (previous.length !== next.length) return true;
71
+ const before = new Map(previous.map((d) => [d.doc_id, d]));
72
+ return next.some((doc) => {
73
+ const was = before.get(doc.doc_id);
74
+ return was === undefined || was.updated_at !== doc.updated_at || !sameStatus(was.status, doc.status);
75
+ });
76
+ }
77
+
78
+ // An import that fails writes its status without moving the stamp, so the
79
+ // status is compared as well.
80
+ function sameStatus(a: DocumentListing['status'], b: DocumentListing['status']): boolean {
81
+ if (a === undefined || b === undefined) return a === b;
82
+ return a.pending === b.pending && a.blocking === b.blocking && a.message === b.message;
83
+ }
84
+
85
+ /**
86
+ * How long a trigger waits before listing, so a burst of frames (a move
87
+ * stamps the document and nudges both containers) is one listing.
88
+ */
89
+ export const RELIST_COALESCE_MS = 250;
90
+
91
+ /** The longest the first listing waits for its feeds to be established. */
92
+ export const FEED_WAIT_MS = 3_000;
93
+
94
+ /**
95
+ * Watch the set `query` lists and call `listener` whenever a listing differs
96
+ * from the last one, starting from `initial`. Returns the stop function.
97
+ */
98
+ export function watchDocumentSet(args: {
99
+ query: ListDocumentsQuery;
100
+ list: (query: ListDocumentsQuery) => Promise<DocumentListing[]>;
101
+ socket: DocumentSetSocket | undefined;
102
+ listener: () => void;
103
+ initial: DocumentListing[];
104
+ }): () => void {
105
+ const { query, list, socket, listener } = args;
106
+ let stopped = false;
107
+ let last = args.initial;
108
+ let inFlight = false;
109
+ let again = false;
110
+ let timer: ReturnType<typeof setTimeout> | null = null;
111
+
112
+ const relist = async (): Promise<void> => {
113
+ if (stopped) return;
114
+ if (inFlight) {
115
+ // A frame that lands while a listing is out may describe a write that
116
+ // listing already missed, so one more follows it.
117
+ again = true;
118
+ return;
119
+ }
120
+ inFlight = true;
121
+ try {
122
+ const next = await list(query);
123
+ if (stopped) return;
124
+ const moved = listingsDiffer(last, next);
125
+ last = next;
126
+ if (moved) listener();
127
+ } catch (error) {
128
+ // The watch keeps its last answer and lists again on the next trigger.
129
+ if (!stopped) console.error('Brass.documents.list:', error);
130
+ } finally {
131
+ inFlight = false;
132
+ if (again && !stopped) {
133
+ again = false;
134
+ schedule();
135
+ }
136
+ }
137
+ };
138
+
139
+ const schedule = (): void => {
140
+ // Before the first listing, that listing answers whatever moved.
141
+ if (stopped || !firstListed || timer !== null) return;
142
+ timer = setTimeout(() => {
143
+ timer = null;
144
+ void relist();
145
+ }, RELIST_COALESCE_MS);
146
+ };
147
+
148
+ // The first listing waits for the feeds to be established, so a write
149
+ // landing between that listing and the subscription cannot fall between
150
+ // them. A feed the socket refuses, or one that never answers, stops being
151
+ // waited for rather than holding the first answer back.
152
+ const awaiting = new Set<string>();
153
+ let firstListed = false;
154
+ const listFirst = (): void => {
155
+ if (firstListed) return;
156
+ firstListed = true;
157
+ if (firstTimer !== null) clearTimeout(firstTimer);
158
+ void relist();
159
+ };
160
+ const settled = (topic: string): void => {
161
+ awaiting.delete(topic);
162
+ if (awaiting.size === 0) listFirst();
163
+ };
164
+
165
+ const unsubscribes: Array<() => void> = [];
166
+ const topics = socket ? feedTopicsFor(query.from ?? []) : [];
167
+ for (const topic of topics) awaiting.add(topic);
168
+ let firstTimer: ReturnType<typeof setTimeout> | null =
169
+ topics.length > 0 ? setTimeout(listFirst, FEED_WAIT_MS) : null;
170
+ for (const topic of topics) {
171
+ unsubscribes.push(
172
+ socket!.subscribe(
173
+ topic,
174
+ (frame) => {
175
+ if (frameCouldMoveSet(frame)) schedule();
176
+ },
177
+ {
178
+ onSubscribed: ({ resubscribed }) => {
179
+ // Frames fanned out while the socket was down are gone, so a
180
+ // re-established feed is answered with a listing.
181
+ if (resubscribed) schedule();
182
+ else settled(topic);
183
+ },
184
+ onEnded: () => settled(topic),
185
+ },
186
+ ),
187
+ );
188
+ }
189
+ if (topics.length === 0) listFirst();
190
+
191
+ return () => {
192
+ if (stopped) return;
193
+ stopped = true;
194
+ if (timer !== null) clearTimeout(timer);
195
+ if (firstTimer !== null) clearTimeout(firstTimer);
196
+ for (const unsubscribe of unsubscribes) unsubscribe();
197
+ };
198
+ }
199
+
200
+ /** A listing, as `Brass.documents.list` returns it. */
201
+ export interface DocumentList {
202
+ /** The set as it stood when it was listed. */
203
+ readonly documents: DocumentListing[];
204
+ /** Whether the set has moved since: a document joined it, left it, or
205
+ * changed (its `updated_at` or work `status`). List again to see how. */
206
+ readonly changed: boolean;
207
+ /** Fires once, when the set moves. The set is watched only while a
208
+ * listener is registered and until `close`; throws after `close`. */
209
+ onChanged(cb: () => void): () => void;
210
+ close(): void;
211
+ }
212
+
213
+ export function createDocumentList(args: {
214
+ documents: DocumentListing[];
215
+ query: ListDocumentsQuery;
216
+ list: (query: ListDocumentsQuery) => Promise<DocumentListing[]>;
217
+ socket: DocumentSetSocket | undefined;
218
+ }): DocumentList {
219
+ let changed = false;
220
+ let closed = false;
221
+ let stopWatching: (() => void) | null = null;
222
+ const listeners = new Set<() => void>();
223
+ const stopWatch = (): void => {
224
+ stopWatching?.();
225
+ stopWatching = null;
226
+ };
227
+ const markChanged = (): void => {
228
+ if (changed || closed) return;
229
+ changed = true;
230
+ stopWatch();
231
+ const fire = [...listeners];
232
+ listeners.clear();
233
+ for (const cb of fire) {
234
+ try {
235
+ cb();
236
+ } catch (err) {
237
+ console.error('Brass: an onChanged listener threw', err);
238
+ }
239
+ }
240
+ };
241
+ const set: DocumentList = {
242
+ documents: args.documents,
243
+ get changed() {
244
+ return changed;
245
+ },
246
+ onChanged(cb) {
247
+ if (closed) throw new Error('DocumentList is closed');
248
+ if (changed) {
249
+ queueMicrotask(() => {
250
+ if (!closed) cb();
251
+ });
252
+ return () => {};
253
+ }
254
+ listeners.add(cb);
255
+ stopWatching ??= watchDocumentSet({
256
+ query: args.query,
257
+ list: args.list,
258
+ socket: args.socket,
259
+ initial: args.documents,
260
+ listener: markChanged,
261
+ });
262
+ return () => {
263
+ listeners.delete(cb);
264
+ if (listeners.size === 0) stopWatch();
265
+ };
266
+ },
267
+ close() {
268
+ closed = true;
269
+ listeners.clear();
270
+ stopWatch();
271
+ },
272
+ };
273
+ return set;
274
+ }
@@ -67,8 +67,10 @@ export interface DocumentDriveStatus {
67
67
  driveName: string;
68
68
  // The divergence state. `ahead` / `diverged` mean unpublished local edits.
69
69
  state: DocumentDriveTrackingState;
70
- // True when publishing would do something (local edits to save back and the
71
- // link is not broken): the cue to surface a "Publish" affordance.
70
+ // True when publishing would do something now, as the server decides it:
71
+ // edits the drive takes and something here can write, or a rename or move
72
+ // to carry, on an unbroken link with no publish in flight. The cue to
73
+ // surface a "Publish" affordance.
72
74
  canPush: boolean;
73
75
  // Server-authored short pill text (`null` when nothing worth surfacing).
74
76
  label: string | null;
@@ -81,9 +83,14 @@ export interface DocumentDriveStatus {
81
83
  // (the file-roundtrip path), so an app can warn that the source's newer
82
84
  // version will be replaced, not merged.
83
85
  pushReplacesRemote: boolean;
86
+ /** @internal True when the drive keeps every prior version of the item,
87
+ * so a version a publish replaces stays in the item's history on the
88
+ * drive. The SDK's own drive surface words its publish confirm from it. */
89
+ keepsPriorVersions: boolean;
84
90
  // Whether the link round-trips (`two_way`) or only imports (`import_only`).
85
- // On `import_only`, `canPush` is always false; an app surfaces the class so
86
- // a one-way import doesn't read as a live two-way mirror.
91
+ // An `import_only` link publishes no content, so `canPush` is true there
92
+ // only for a rename or a move; an app surfaces the class so a one-way
93
+ // import doesn't read as a live two-way mirror.
87
94
  mirrorDirection: DriveMirrorDirection;
88
95
  // Current external head ref, empty when the adapter could not produce one.
89
96
  ref: string;
@@ -135,6 +142,9 @@ export interface DrivePublishTarget {
135
142
  // The last publish attempt failed, in which case `detail` carries the
136
143
  // server's account of why rather than the resting sentence.
137
144
  publishFailed: boolean;
145
+ // The drive does not take this document's file; `detail` says why, and
146
+ // there is nothing to publish.
147
+ unpublishable: boolean;
138
148
  }
139
149
 
140
150
  // Each signal is a `createEmitter` composed with the retain / attach / dedup
@@ -178,9 +188,19 @@ export class GoneSignal {
178
188
  }
179
189
  }
180
190
 
191
+ const NO_WORK_STATUS: DocumentWorkStatus = { message: '', pending: false, blocking: false };
192
+
181
193
  // Latest-retained, re-emit-per-phase signal for `doc_status`. Not one-shot:
182
194
  // phases arrive repeatedly until terminal (`pending: false`), and the latest
183
195
  // is retained so a late subscriber still sees the final state.
196
+ //
197
+ // A notice is the SDK's own status, raised over the platform's work rather
198
+ // than replacing it: `status` reads the notice while one is up, and `work`
199
+ // keeps answering what the platform reported, which is what every SDK-side
200
+ // decision about pending work reads. A work status arriving under a notice
201
+ // still reaches the listeners (the SDK's own import waiters attach here and
202
+ // wait for a terminal one), and the notice is re-emitted straight after it so
203
+ // the last thing a listener hears matches `status`.
184
204
  export class WorkStatusSignal {
185
205
  private readonly _emitter = createEmitter<DocumentWorkStatus>({
186
206
  fireOnAttach: true,
@@ -189,9 +209,19 @@ export class WorkStatusSignal {
189
209
  console.error('[Brass] onStatus listener threw:', err);
190
210
  },
191
211
  });
212
+ private _work: DocumentWorkStatus | null = null;
213
+ private _notice: DocumentWorkStatus | null = null;
192
214
 
193
215
  get status(): DocumentWorkStatus | null {
194
- return this._emitter.retained ?? null;
216
+ return this._notice ?? this._work;
217
+ }
218
+
219
+ get work(): DocumentWorkStatus | null {
220
+ return this._work;
221
+ }
222
+
223
+ get notice(): DocumentWorkStatus | null {
224
+ return this._notice;
195
225
  }
196
226
 
197
227
  on(cb: DocumentStatusListener): DocumentStatusUnsubscribe {
@@ -199,7 +229,18 @@ export class WorkStatusSignal {
199
229
  }
200
230
 
201
231
  emit(status: DocumentWorkStatus): void {
232
+ this._work = status;
202
233
  this._emitter.emit(status);
234
+ if (this._notice !== null) this._emitter.emit(this._notice);
235
+ }
236
+
237
+ // Raise or clear the notice. Clearing hands listeners the work status it
238
+ // covered, or a terminal empty one when there is none, since a listener
239
+ // takes a status rather than `null`.
240
+ setNotice(notice: DocumentWorkStatus | null): void {
241
+ if (notice === this._notice) return;
242
+ this._notice = notice;
243
+ this._emitter.emit(notice ?? this._work ?? NO_WORK_STATUS);
203
244
  }
204
245
 
205
246
  clearListeners(): void {
@@ -335,6 +376,7 @@ function driveStatusEqual(
335
376
  a.detail === b.detail &&
336
377
  a.broken === b.broken &&
337
378
  a.pushReplacesRemote === b.pushReplacesRemote &&
379
+ a.keepsPriorVersions === b.keepsPriorVersions &&
338
380
  a.mirrorDirection === b.mirrorDirection &&
339
381
  a.ref === b.ref &&
340
382
  a.refUpdatedAt === b.refUpdatedAt &&
@@ -26,12 +26,17 @@ import type {
26
26
  DocumentStatusUnsubscribe,
27
27
  DocumentWorkStatus,
28
28
  } from './document-signals.js';
29
+ import type { InputStatus } from './inputs-status.js';
30
+ import type { InputDocumentHandlers, InputDocumentOptions } from './input-documents.js';
29
31
  import type {
30
32
  ListHandle,
31
33
  MapHandle,
34
+ MutateOptions,
32
35
  OrderedMapHandle,
33
36
  TextHandle,
34
37
  } from './handle-types.js';
38
+ import type { InputReference } from './hosted-ui.js';
39
+ import type { MarkReadOptions, MarkReadStatus } from './references.js';
35
40
 
36
41
  // The wiring `Document.stream()` runs on: list/create manifests over
37
42
  // HTTP, and open a stream's line (join + WS subscribe) returning the
@@ -80,8 +85,22 @@ export class DocumentStream {
80
85
 
81
86
  mutate<E extends object = EntityProxy>(
82
87
  fn: (d: DocumentMutator<E>) => void,
88
+ options?: MutateOptions,
83
89
  ): void {
84
- this._inner.mutate<E>(fn);
90
+ this._inner.mutate<E>(fn, options);
91
+ }
92
+
93
+ /** Records the documents `key` depends on for this stream; see
94
+ * `Document.dependsOn`. A reference to any stream of this document is
95
+ * refused. */
96
+ dependsOn(key: string, refs: readonly InputReference[] | null): void {
97
+ this._inner.dependsOn(key, refs);
98
+ }
99
+
100
+ /** @internal Records what this reader read for the key `status` reports
101
+ * on this stream; see `Document.markRead`. */
102
+ markRead(status: MarkReadStatus, options?: MarkReadOptions): void {
103
+ this._inner.markRead(status, options);
85
104
  }
86
105
 
87
106
  roots(): Record<string, RootKind> {
@@ -111,6 +130,12 @@ export class DocumentStream {
111
130
  return this._inner.readOnly;
112
131
  }
113
132
 
133
+ /** @internal This stream's line's `updated_at` as it was opened; see
134
+ * `Document.updatedAt`. */
135
+ get updatedAt(): string | null {
136
+ return this._inner.updatedAt;
137
+ }
138
+
114
139
  // Locally-issued ops on this stream the server has not acked yet. This
115
140
  // stream's own line: `doc.savingCount` is the one that adds the streams up,
116
141
  // and the default stream's inner IS the document, so reading that here would
@@ -126,14 +151,18 @@ export class DocumentStream {
126
151
  return this._inner.atCapacity;
127
152
  }
128
153
 
129
- // The latest opaque work-status for this stream, or `null` when no slow
130
- // server-side work is in flight. Render `message` verbatim.
154
+ /**
155
+ * The work in flight on this stream, or `null` when there is none. Render
156
+ * `message` verbatim.
157
+ */
131
158
  get status(): DocumentWorkStatus | null {
132
159
  return this._inner.status;
133
160
  }
134
161
 
135
- // Subscribe to this stream's work-status updates. Fires once on attach when
136
- // a status is already known.
162
+ /**
163
+ * Calls `cb` with every work-status update for this stream, and once on
164
+ * attach when a status is already known. Returns an unsubscribe.
165
+ */
137
166
  onStatus(cb: DocumentStatusListener): DocumentStatusUnsubscribe {
138
167
  return this._inner.onStatus(cb);
139
168
  }
@@ -144,23 +173,63 @@ export class DocumentStream {
144
173
  return this._inner.onGone(cb);
145
174
  }
146
175
 
147
- // Per-stream undo/redo: each stream keeps its own local-edit stack
148
- // (its edits are its own CRDT commits). The doc-level `doc.undo()`
149
- // covers only the default stream.
176
+ /** @internal On a frozen copy: whether this stream has moved since it was
177
+ * read. Each stream is its own line, so each is watched on its own. */
178
+ get changed(): false {
179
+ return this._inner.changed;
180
+ }
181
+
182
+ /** @internal */
183
+ onChanged(cb: () => void): () => void {
184
+ return this._inner.onChanged(cb);
185
+ }
186
+
187
+ /** @internal Typed on `ReadOnlyStream`: loads this stream as it stands now
188
+ * on a frozen copy. */
189
+ reload(): Promise<void> {
190
+ return this._inner.reload();
191
+ }
192
+
193
+ /** The input files of a key this stream records references under;
194
+ * see `Document.subscribeInputDocuments`. A stream's keys are its own,
195
+ * apart from the document's and every other stream's. */
196
+ subscribeInputDocuments(
197
+ key: string,
198
+ handlers: InputDocumentHandlers,
199
+ options?: InputDocumentOptions,
200
+ ): () => void {
201
+ return this._inner.subscribeInputDocuments(key, handlers, options);
202
+ }
203
+
204
+ /** @internal The status of a key this stream records references under. */
205
+ subscribeInputStatus(key: string, cb: (status: InputStatus) => void): () => void {
206
+ return this._inner.subscribeInputStatus(key, cb);
207
+ }
208
+
209
+ /**
210
+ * Reverts this stream's part of the newest undo step that touched it,
211
+ * leaving the document's other streams as they are.
212
+ * @deprecated Call `doc.undo()`, which reverts the newest step in every
213
+ * stream it touched.
214
+ */
150
215
  undo(): boolean {
151
- return this._inner.undo();
216
+ return this._inner._replayOwnUndoStep('undo');
152
217
  }
153
218
 
219
+ /**
220
+ * Re-applies this stream's part of the step it most recently undid.
221
+ * @deprecated Call `doc.redo()`.
222
+ */
154
223
  redo(): boolean {
155
- return this._inner.redo();
224
+ return this._inner._replayOwnUndoStep('redo');
156
225
  }
157
226
 
158
- // Seal the stream's open undo group, so the next local edit starts a
159
- // fresh undo step. Local and synchronous (no server call). Call it
160
- // when a user operation completes: commits group into one step until
161
- // sealed (the same same-stroke collapsing the doc-level undo has), so
162
- // an app that never seals gets one giant step per session. The
163
- // cross-stream operation recipe in `docs/streams.md` is built on this.
227
+ /**
228
+ * Ends the document's open undo step, so the next local edit in any stream
229
+ * starts a new one.
230
+ * @deprecated Call `doc.createCheckpoint()` at the end of each user action,
231
+ * which also ends the step.
232
+ */
164
233
  sealUndoStep(): void {
165
234
  this._inner._sealUndoStep();
166
235
  }