@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/docs/schemas.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Schemas
2
2
 
3
- A schema declares what your app expects a document to hold. Declaring one
3
+ A schema declares what your app expects a document to contain. Declaring one
4
4
  in your `brass-app.json` routes matching documents to your app, and lets
5
5
  any developer who can open one of those documents build against the same
6
6
  shape.
@@ -21,13 +21,16 @@ stream's root collections and records.
21
21
  so a namespaced `acme.roster` works and a URL does not. Brass does not
22
22
  record a declaration whose family is missing or not a word, and reports
23
23
  that on your app's page in the dashboard.
24
- - **`streams`** maps each stream name your app expects a document to hold
24
+ - **`streams`** maps each stream name your app expects a document to have
25
25
  to that stream's contract. `default` is the stream a document opens
26
26
  without asking for one, and takes an entry like any other name. Two
27
27
  streams that carry the same shape carry the same body.
28
28
  - **A collection of records** is an object whose `additionalProperties`
29
29
  is the record schema, so the keys are record ids. An ordered collection
30
30
  is an array, or an array of keys alongside the map.
31
+ - **A text root** (`doc.text(key)`) reads as a string wherever the
32
+ document is read as data, `brass contents` included, so describe it as a
33
+ string property of the stream: `{ "type": "string" }`.
31
34
  - **Foreign keys** ride as an `x-brass-ref` extension on the referring
32
35
  field, an object whose `to` names the collection it points at.
33
36
  Standard validators ignore an `x-` keyword.
@@ -54,8 +57,8 @@ and an approximation reads as a different contract.
54
57
  Streams let one document load in parts, so an app pays for what it opens
55
58
  rather than the document's total size. `doc.getOrCreateStream(name)`
56
59
  opens the document's stream of that name and makes it when the document
57
- does not hold it yet. `doc.stream(name)` opens one the document already
58
- holds and rejects any other name.
60
+ does not have it yet. `doc.stream(name)` opens one the document already
61
+ has and rejects any other name.
59
62
 
60
63
  ```ts
61
64
  const stream = await doc.getOrCreateStream(name);
@@ -67,7 +70,7 @@ stream.map('records').mutate((m) => {
67
70
  `getOrCreateStream` is idempotent, and concurrent callers converge on one
68
71
  stream, so an app reaches for the stream it expects on every open without
69
72
  checking first. Only that call writes, so a read-only open reads through
70
- `doc.stream` and handles the rejection when the document does not hold
73
+ `doc.stream` and handles the rejection when the document does not have
71
74
  the stream. Names follow the same rule a family does: `a-z 0-9 . _ -`, 1 to
72
75
  64 characters, starting with a letter or digit.
73
76
 
package/docs/services.md CHANGED
@@ -43,7 +43,7 @@ the host needs, and tells anyone who cannot answer who to ask.
43
43
 
44
44
  | `code` | What it means |
45
45
  | --------------------- | --------------------------------------------------------------------------------------------------------- |
46
- | `credential_required` | No credential is connected for this host, or the one connected is not approved for your app, no longer covers what your app sends, or carries no value under a name your app reads back. One code, because the dialog is the answer to all of them. The body's sentence says which, for a person to read. |
46
+ | `credential_required` | No credential is connected for this host, or the one connected is not approved for your app, no longer covers what your app sends, or carries no value under a name your app reads back. One code, because the dialog is the answer to all of them. The body's sentence says which, for a user to read. |
47
47
 
48
48
  **Your own key is what is missing.** These are yours to settle on your app's
49
49
  settings page, so there is nothing to raise on the page: report the feature as
@@ -78,7 +78,7 @@ on their own.
78
78
  | `code` | What it means |
79
79
  | -------------------- | -------------------------------------------------------------------------------------- |
80
80
  | `cap_exceeded` | A cap has been reached. Try again later. |
81
- | `rate_limited` | Over Brass's own rate limit. Slow down. |
81
+ | `rate_limited` | Over Brass's own rate limit. The SDK resends a call that limit turns away for only a moment, so wait before calling again. |
82
82
  | `calls_paused` | Whoever owns the account being spent set its cap to zero. Waiting will not clear it. |
83
83
  | `vault_unavailable` | Brass could not read the stored key just now. Nothing is wrong with it; make the same call again. |
84
84
 
@@ -112,7 +112,7 @@ signed in, the app has no organization, or it is running in one that has not
112
112
  installed it.
113
113
 
114
114
  Each entry also carries `connectable`, which says whether the dialog has
115
- anything for the person on the page to do at that host. Together the two are
115
+ anything for the user on the page to do at that host. Together the two are
116
116
  the whole branch:
117
117
 
118
118
  ```js
@@ -134,7 +134,7 @@ if (await Brass.connectService('api.anthropic.com')) showSummariseButton();
134
134
 
135
135
  Passing `requestMissingCredentials: true` to `callService` opens the same
136
136
  dialog from the call, which then goes through once the user answers. It covers
137
- every refusal the person using your app can answer: `credential_required`, plus
137
+ every refusal your app's user can answer: `credential_required`, plus
138
138
  `credential_rejected` and `credential_scheme_unusable` when the account that
139
139
  failed is one they own.
140
140
 
@@ -157,7 +157,7 @@ same one: you name a host, and Brass attaches whichever credential is in
157
157
  force.
158
158
 
159
159
  Your app reads `connectable` rather than the supplier, because that is the
160
- split it can act on. Where it is true, the person using your app can
160
+ split it can act on. Where it is true, your app's user can
161
161
  settle it, so a Connect button leads somewhere; where it is false, the host is
162
162
  waiting on you, so there is nothing to raise. Leave the wording about who to
163
163
  ask to the dialog, which knows both the supplier and who is reading, where
@@ -205,7 +205,7 @@ if (cf?.ready && account) {
205
205
 
206
206
  Brass shows your setup instructions on the form that collects a credential,
207
207
  and you are strongly encouraged to provide them. Without them the form shows
208
- your field labels alone, and the person filling it in has to work out what
208
+ your field labels alone, and the user filling it in has to work out what
209
209
  to create at the vendor and where.
210
210
 
211
211
  Declare a `setup` beside the host:
package/docs/streams.md CHANGED
@@ -1,19 +1,29 @@
1
- # Streams: partitioning a document for lazy loading
1
+ # Streams: loading a document in parts
2
2
 
3
3
  A document is a container of **streams**: named units of document
4
4
  state that load independently. Opening a document costs only the
5
5
  streams your app touches, so download, load, and memory scale with what
6
- you use rather than with the document's total size. Split a
7
- multi-discipline design into architectural, plumbing, HVAC, and
8
- structural streams, and a plumbing tool opens one of them. Every stream
6
+ the app reads rather than with the document's total size. Every stream
9
7
  is live, collaborative state that merges the same way the rest of the
10
8
  document does.
11
9
 
10
+ The app or importer that writes a document decides its streams. Your
11
+ app can put a heavy part of its content (a terrain mesh, a point-cloud
12
+ index, a layer only some views draw) in a stream of its own, and an open
13
+ that does not ask for that stream never downloads it. Your app can also
14
+ keep records about a document (clash findings, review notes, a cache of
15
+ derived results) in a stream beside the document's content, so they sync
16
+ and version with it and stay out of an exported file unless the exporter
17
+ declares that stream. An
18
+ importer can split a model the same way: with its geometry in a stream
19
+ of its own, a property editor opens only the default stream, and a 3D
20
+ viewer opens only the geometry.
21
+
12
22
  Every document has a stream named **`default`**, and an open that names
13
23
  no stream opens that one, so the doc-level handles (`doc.map(...)`,
14
- `doc.orderedMap(...)`) read it. An unpartitioned document is one whose
15
- content lives entirely there. Name another stream on the open (below)
16
- and the doc-level handles read that one instead.
24
+ `doc.orderedMap(...)`) read it. A document with one stream keeps all of
25
+ its content there. Name another stream on the open (below) and the
26
+ doc-level handles read that one instead.
17
27
 
18
28
  ## Opening and editing a stream
19
29
 
@@ -28,25 +38,23 @@ records.subscribe((change) => render(change));
28
38
 
29
39
  `doc.stream(name)` resolves once per name: the first call downloads the
30
40
  stream's snapshot and subscribes it live, and repeat calls return the same
31
- open. It only reads, so a name the document does not hold rejects rather
32
- than being created. `doc.getOrCreateStream(name)` is the call that makes
33
- one. It is idempotent and concurrent callers converge on one stream, so an
34
- app reaches for the stream it expects on every open without checking
35
- first. A `DocumentStream` carries the document's own reading and writing
36
- surface scoped to that stream: `map` / `list` / `orderedMap` handles,
37
- `mutate`, `subscribe`, and `undo` / `redo`.
41
+ open. It only reads, so it rejects a name the document does not have.
42
+ To make a stream, see [Adding a stream](#adding-a-stream). A
43
+ `DocumentStream` has the same read and write methods as the document,
44
+ scoped to that stream: `map` / `list` / `orderedMap` handles, `mutate`,
45
+ and `subscribe`.
38
46
 
39
47
  ## Opening at a stream
40
48
 
41
49
  `Brass.openDocumentById(docId, { stream })` opens the document with that
42
50
  stream as its own state, so `doc.map(...)` hands you its records and the rest
43
- of the document is never downloaded. An app that works in one part of a large
44
- document pays for that part alone:
51
+ of the document is never downloaded. A 3D viewer of an imported model
52
+ opens at `geometry` and downloads nothing else:
45
53
 
46
54
  ```ts
47
- const doc = await Brass.openDocumentById(docId, { stream: 'hvac' });
55
+ const doc = await Brass.openDocumentById(docId, { stream: 'geometry' });
48
56
  doc.orderedMap('instances', 'instance_order').subscribe((change) => {
49
- for (const [key, record] of change.set) hold(key, record); // hvac's records
57
+ for (const [key, record] of change.set) draw(key, record); // geometry records
50
58
  });
51
59
  ```
52
60
 
@@ -63,165 +71,142 @@ for (const info of await doc.listStreams()) {
63
71
  }
64
72
  ```
65
73
 
66
- Names are `a-z 0-9 . _ -`, 1 to 64 chars, starting with a letter or
67
- digit, the same rule a schema family follows. `default` is one of them:
68
- every document already holds it, so `getOrCreateStream` resolves it
69
- without a request, and a loop over the names your schema declares needs
70
- no case for it. `getOrCreateStream` writes, so a read-only open reads
71
- with `doc.stream` instead and shows a degraded view when the document
72
- does not hold the stream.
73
-
74
- ## Undo and editing model
74
+ ## Adding a stream
75
75
 
76
- Each stream is its own collaborative unit: concurrent edits merge per
77
- stream, and each stream keeps its own local undo stack
78
- (`stream.undo()` / `stream.redo()`). The doc-level `doc.undo()` covers
79
- only the default stream.
80
-
81
- A stream's local edits group into one undo step until the group is
82
- **sealed** with `stream.sealUndoStep()`, the same same-stroke
83
- collapsing doc-level undo gets from checkpoints. Seal when a user
84
- operation completes. An app that never seals gets one giant step per
85
- session.
86
-
87
- ### Undoing an operation that spans streams
88
-
89
- When one user action writes to several streams (move a wall in
90
- `architectural`, reroute its duct in `hvac`), keep an app-level
91
- operation journal and undo each touched stream once per journal entry.
92
- Two rules make the stacks line up:
93
-
94
- 1. **Seal every touched stream when the operation completes.** The
95
- seal is what makes the operation exactly one undo step in each
96
- stream, however many mutates it took.
97
- 2. **Route every local edit through the journal.** The alignment
98
- argument is LIFO: if every operation is journaled and sealed, each
99
- stream's stack top always belongs to the most recent journaled
100
- operation that touched it. An edit that bypasses the journal
101
- breaks that.
76
+ `doc.getOrCreateStream(name)` opens the document's stream of that name,
77
+ and creates it if the document does not have it yet. A stream your app
78
+ adds syncs and versions with the rest of the document, and only the
79
+ opens that ask for it download it. Your app can add a stream to any
80
+ document the user can edit, including a file an importer or another app
81
+ made. The call is idempotent, and
82
+ concurrent callers get the same stream, so your app can call it on
83
+ every open without checking first:
102
84
 
103
85
  ```ts
104
- import type { DocumentStream } from '@brass-build/client';
105
-
106
- type Op = { label: string; streams: string[] };
107
- const undoJournal: Op[] = [];
108
- const redoJournal: Op[] = [];
109
-
110
- // Wrap every user operation: run the writes, then seal + journal.
111
- async function operation(
112
- label: string,
113
- streams: string[],
114
- run: (open: Map<string, DocumentStream>) => void,
115
- ) {
116
- const open = new Map(
117
- await Promise.all(
118
- streams.map(async (n) => [n, await doc.stream(n)] as const),
119
- ),
120
- );
121
- run(open);
122
- for (const s of open.values()) s.sealUndoStep();
123
- undoJournal.push({ label, streams });
124
- redoJournal.length = 0; // a fresh edit clears redo, as usual
125
- }
126
-
127
- await operation('Move wall', ['architectural', 'hvac'], (open) => {
128
- open.get('architectural')!.map('instances').mutate((m) => {
129
- /* move the wall */
130
- });
131
- open.get('hvac')!.map('instances').mutate((m) => {
132
- /* reroute the attached duct */
133
- });
86
+ const findings = await doc.getOrCreateStream('clash-findings');
87
+ findings.map('findings').mutate((m) => {
88
+ m[id] = { elements: [a, b], status: 'open' };
134
89
  });
90
+ ```
135
91
 
136
- async function undoOperation() {
137
- const op = undoJournal.pop();
138
- if (!op) return;
139
- // Reverse application order, so dependent changes unwind last-first.
140
- for (const name of [...op.streams].reverse()) {
141
- (await doc.stream(name)).undo();
142
- }
143
- redoJournal.push(op);
144
- }
92
+ Your app can add a stream to any document it can edit, including one
93
+ that another app created or imported. When the document already has a
94
+ stream of that name, such as one its importer wrote, `getOrCreateStream`
95
+ opens that stream instead of making a new one. Name the streams your app
96
+ adds after your app (`acme-clash-findings`) rather than after their
97
+ content, so your app does not write into a stream that another writer
98
+ named first.
99
+
100
+ Names are `a-z 0-9 . _ -`, 1 to 64 characters, starting with a letter or
101
+ digit, the same rule a schema family follows. `default` is a valid name
102
+ too: every document already has it, so `getOrCreateStream` resolves it
103
+ like any existing name, and a loop over the names your schema declares
104
+ needs no case for it. `getOrCreateStream` writes, so on a read-only
105
+ open, read with `doc.stream` instead, and render a reduced view when the
106
+ document does not have the stream.
145
107
 
146
- async function redoOperation() {
147
- const op = redoJournal.pop();
148
- if (!op) return;
149
- for (const name of op.streams) {
150
- (await doc.stream(name)).redo();
151
- }
152
- undoJournal.push(op);
153
- }
154
- ```
108
+ ## Undo and editing model
155
109
 
156
- Bind your keyboard shortcut and undo button to `undoOperation()`
157
- rather than `doc.undo()` (which covers only the default stream).
158
- Collaborators' edits do not disturb the alignment: undo stacks are
159
- local-peer-scoped, and each stream's undo rebases your reverted step
160
- over remote changes the same way single-document undo does.
161
-
162
- Design around two properties of it. The cross-stream undo is **not
163
- atomic**: each stream's revert is its own commit, so a collaborator can
164
- briefly observe one stream reverted before the other (the same window
165
- any multi-stream operation has). And the journal is **in-memory**: a
166
- reload starts it empty, exactly like single-document undo. When an
167
- operation cannot tolerate that window, its data belongs in one stream:
168
- partition by load unit, and keep records that must change together in
169
- the same stream.
170
-
171
- ## When to partition
172
-
173
- Opening a stream costs a request for its snapshot, and the first one on a
174
- document also reads the stream manifest, so add a stream when it carries
175
- real content that some opens can skip. Partition by **load unit** (a
176
- discipline, a heavy layer, a dataset a specific tool reads), not by
177
- concept. Aim for a handful of streams carrying megabytes each. Dozens
178
- of tiny streams make opening everything slower than a monolith, and
179
- content every open needs belongs in the default stream.
110
+ Each stream is its own collaborative unit: concurrent edits merge per
111
+ stream. One Undo reverts the whole step even while a collaborator edits
112
+ the same stream, and a map or text handle's `subscribe` fires once for
113
+ each commit the step took, each with `origin.source` `'undo'` (or
114
+ `'redo'`). A write your app makes on its own, such as the results of a
115
+ pass over other documents, passes `{ undoable: false }` to `mutate` so
116
+ the user's Undo skips it.
117
+
118
+ A stream that reaches the largest size Brass keeps stops taking writes on
119
+ its own: `stream.readOnly` and `stream.atCapacity` read `true`, and the
120
+ document's other streams keep taking writes. The edit Brass refused stays
121
+ on screen until your app opens the stream again, which loads what Brass
122
+ saved. Read `stream.atCapacity` to stop offering bulk work on that stream.
123
+
124
+ ## When to add a stream
125
+
126
+ Opening a stream incurs a snapshot request, and opening the first stream
127
+ on a document also retrieves the stream manifest. Add a stream when
128
+ specific workflows can skip its content (such as a model's geometry, a heavy
129
+ layer, or tool-specific data). Partition content by what each workflow loads,
130
+ rather than by conceptual divisions. Aim for a handful of streams
131
+ carrying several megabytes each. Opening dozens of tiny streams is slower than
132
+ loading a single stream, and data required across every workflow belongs in
133
+ the default stream.
180
134
 
181
135
  ## Reading a document spread across streams
182
136
 
183
137
  Your schema names the streams it expects a document to be spread over,
184
138
  under `streams`. A small document keeps every record in its default
185
- stream; a large one moves the bulk (an IFC's geometry, an index most opens
139
+ stream; a large one moves the bulk (a model's geometry, an index most opens
186
140
  skip) onto the streams the schema names, so an app that reads only part of
187
141
  the content loads only that part. The declaration is the same either way,
188
142
  because it says which streams your app expects, not what any one document
189
- turned out to hold.
143
+ turned out to contain.
144
+
145
+ Open only the streams that contain the records your app needs. The SDK
146
+ downloads and decodes each stream you open in full, so a property editor
147
+ over a model whose geometry has its own stream opens the default stream
148
+ alone.
190
149
 
191
- If your app follows references between records, read every stream the
192
- DOCUMENT holds, because a referenced record may live in one the default
193
- stream does not. `doc.listStreams()` is that list, so merge by id:
150
+ If your app follows references into records in another stream (a
151
+ viewer resolving an element's shape, a clash check reading bodies), open
152
+ every stream the document has, because a referenced record can be in any
153
+ of them. `doc.listStreams()` lists them, so open them together and merge the
154
+ records by id:
194
155
 
195
156
  ```ts
196
157
  import type { OrderedMapHandle } from '@brass-build/client';
197
158
 
198
159
  type StoredRecord = { type: string; params: unknown };
199
160
  const store: Record<string, StoredRecord> = {};
200
- const fold = (records: OrderedMapHandle<StoredRecord>) =>
161
+
162
+ const streams = await Promise.all(
163
+ (await doc.listStreams()).map((info) => doc.stream(info.name)),
164
+ );
165
+ const handles: OrderedMapHandle<StoredRecord>[] = streams.map((s) =>
166
+ s.orderedMap<StoredRecord>('instances', 'instance_order'),
167
+ );
168
+ for (const records of handles) {
201
169
  records.subscribe((change) => {
202
170
  for (const [key, record] of change.set) store[key] = record;
203
171
  for (const key of change.deleted) delete store[key];
204
172
  });
205
-
206
- fold(doc.orderedMap<StoredRecord>('instances', 'instance_order'));
207
- for (const info of await doc.listStreams()) {
208
- const stream = await doc.stream(info.name);
209
- fold(stream.orderedMap<StoredRecord>('instances', 'instance_order'));
210
173
  }
211
174
  // store['#137'] resolves wherever #137 lives.
212
175
  ```
213
176
 
214
- The loop reads an unpartitioned document the same way it reads a split
215
- one, so it needs no special case. Open only what you read: an app that
216
- needs just the default view's records reads the default stream alone,
217
- and one that resolves references reads them all and pays for the
218
- streams it opens.
177
+ This reads a document with one stream the same way it reads one with
178
+ several, so it needs no special case.
179
+
180
+ ### Knowing when every stream has arrived
181
+
182
+ A handle's `onSettled` callback fires when that one container has arrived
183
+ whole. A conclusion drawn across several streams (a model with no
184
+ geometry, a count, "no run yet") is true only after every stream it reads
185
+ has settled. Until your app opens a stream, its records are unknown, not
186
+ empty. Render such a conclusion once every handle has settled, and show
187
+ the loading state until then:
188
+
189
+ ```ts
190
+ const unsettled = new Set(handles);
191
+ for (const records of handles) {
192
+ records.onSettled(() => {
193
+ unsettled.delete(records);
194
+ if (unsettled.size === 0) render(store);
195
+ });
196
+ }
197
+ ```
198
+
199
+ `onSettled` fires again when a version checkout replaces a container, so
200
+ `render` runs again for the version the user switched to. The same
201
+ applies to streams your app adds (findings, review notes). Open the ones
202
+ the document has alongside the rest, and treat their records as unknown
203
+ until their handles settle.
219
204
 
220
- A document can hold streams your schema does not name (computed results,
221
- review notes, a cache), so the merge above skips them and an exporter that
222
- does not declare them writes nothing from them. Name a stream in your
223
- schema to read it, and `doc.getOrCreateStream` makes it when the document
224
- does not hold it yet.
205
+ A document can have streams your schema does not name (computed results,
206
+ review notes, a cache). An exporter writes nothing from a stream it does
207
+ not declare. Read the streams your schema names, and the others only
208
+ when you follow references into them. `doc.getOrCreateStream` makes a
209
+ stream your schema names when the document does not have it yet.
225
210
 
226
211
  ## Exporting across streams
227
212
 
@@ -231,11 +216,15 @@ lives:
231
216
 
232
217
  ```ts
233
218
  Brass.registerSerializer(async (doc) => {
234
- const arch = await doc.stream('architectural');
235
- const hvac = await doc.stream('hvac');
219
+ const names = new Set((await doc.listStreams()).map((s) => s.name));
220
+ const geometry = names.has('geometry')
221
+ ? (await doc.stream('geometry'))
222
+ .orderedMap('instances', 'instance_order')
223
+ .entries()
224
+ : [];
236
225
  return encodeMyFormat(
237
- arch.orderedMap('instances', 'instance_order').ordered(),
238
- hvac.orderedMap('instances', 'instance_order').ordered(),
226
+ doc.orderedMap('instances', 'instance_order').entries(),
227
+ geometry,
239
228
  );
240
229
  });
241
230
  ```
@@ -9,7 +9,7 @@ what a keystroke costs, and where a text container belongs in a document.
9
9
  ## Paint a peer's edit without losing the caret
10
10
 
11
11
  Assigning `field.value` moves the caret to the end, so an editor that
12
- assigns unconditionally throws away the reader's place on every peer
12
+ assigns unconditionally throws away the user's place on every peer
13
13
  keystroke, and one that skips a focused field never shows a peer's typing
14
14
  to anyone who is typing themselves. Find the span the incoming value
15
15
  replaced, and move the caret across it:
@@ -29,10 +29,12 @@ report.subscribe((value, origin) => {
29
29
  const max = Math.min(painted.length, value.length);
30
30
  let start = 0;
31
31
  while (start < max && painted.charCodeAt(start) === value.charCodeAt(start)) start += 1;
32
+ if (start > 0 && isHighHalf(value.charCodeAt(start - 1))) start -= 1;
32
33
  let tail = 0;
33
34
  while (tail < max - start
34
35
  && painted.charCodeAt(painted.length - 1 - tail)
35
36
  === value.charCodeAt(value.length - 1 - tail)) tail += 1;
37
+ if (tail > 0 && isLowHalf(value.charCodeAt(value.length - tail))) tail -= 1;
36
38
  const removed = painted.length - tail - start;
37
39
  const inserted = value.length - tail - start;
38
40
 
@@ -45,13 +47,13 @@ report.subscribe((value, origin) => {
45
47
  : caret >= start + removed ? caret + inserted - removed
46
48
  : start + Math.min(caret - start, inserted);
47
49
 
48
- // A reader with text selected has two offsets, and `selectionDirection`
50
+ // A user with text selected has two offsets, and `selectionDirection`
49
51
  // says which of them their next keystroke extends from.
50
52
  const focused = document.activeElement === field;
51
53
  const anchor = carry(field.selectionStart ?? 0);
52
54
  const head = carry(field.selectionEnd ?? field.selectionStart ?? 0);
53
55
  const direction = field.selectionDirection;
54
- // Assigning `value` resets the scroll, which throws a reader who is
56
+ // Assigning `value` resets the scroll, which throws a user who is
55
57
  // partway down a long field back to the top on every peer keystroke.
56
58
  const scroll = field.scrollTop;
57
59
 
@@ -65,19 +67,24 @@ report.subscribe((value, origin) => {
65
67
  ```
66
68
 
67
69
  Offsets count UTF-16 code units, which is what a text field's
68
- `selectionStart` reports, so the two agree with no conversion. The same
69
- units are why an offset needs one more check before it is used: a character
70
+ `selectionStart` reports, so the two agree with no conversion. A character
70
71
  outside the Basic Multilingual Plane, an emoji above all, is two code units,
71
- and the `charCodeAt` walk above stops between them whenever the edit sits
72
- next to one. `nudgeOffPair` moves such an offset off the seam:
72
+ and two emoji can share their first unit (😀 and 🙂 both start with
73
+ `0xd83d`), so the `charCodeAt` walks can stop between the halves of a
74
+ character. Keep the two checks after the walks: they move each end of the
75
+ span back onto a whole character, so a caret where a peer types an emoji
76
+ ends up after it, since the caret binds to the text on its right. A carried
77
+ offset can still land inside a character the peer inserted, and
78
+ `nudgeOffPair` moves it back to that character's start:
73
79
 
74
80
  ```ts
81
+ const isHighHalf = (unit: number) => unit >= 0xd800 && unit <= 0xdbff;
82
+ const isLowHalf = (unit: number) => unit >= 0xdc00 && unit <= 0xdfff;
83
+
75
84
  function nudgeOffPair(text: string, at: number): number {
76
85
  const bounded = Math.max(0, Math.min(at, text.length));
77
86
  if (bounded <= 0 || bounded >= text.length) return bounded;
78
- const before = text.charCodeAt(bounded - 1);
79
- const after = text.charCodeAt(bounded);
80
- const splits = before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff;
87
+ const splits = isHighHalf(text.charCodeAt(bounded - 1)) && isLowHalf(text.charCodeAt(bounded));
81
88
  return splits ? bounded - 1 : bounded;
82
89
  }
83
90
  ```
@@ -94,8 +101,8 @@ path, where it would otherwise run on every keystroke.
94
101
 
95
102
  ## Defer the paint, not the write, while an IME is open
96
103
 
97
- Assigning to the field while an IME is open discards what it is holding,
98
- which costs a reader typing Japanese, Chinese, or Korean the word they were
104
+ Assigning to the field while an IME is open discards its unfinished input,
105
+ which costs a user typing Japanese, Chinese, or Korean the word they were
99
106
  in the middle of. So track `compositionstart` / `compositionend` and skip
100
107
  the paint in between.
101
108
 
@@ -113,7 +120,7 @@ field.addEventListener('compositionend', () => {
113
120
  painted = field.value;
114
121
  report.update(painted);
115
122
  // What arrived while the composition was open is older than what the
116
- // reader just committed, so paint the container's own value, which
123
+ // user just committed, so paint the container's own value, which
117
124
  // carries both.
118
125
  if (missed) {
119
126
  missed = false;
@@ -123,9 +130,9 @@ field.addEventListener('compositionend', () => {
123
130
  });
124
131
  ```
125
132
 
126
- Holding the incoming value and applying it at `compositionend` is the
133
+ Keeping the incoming value aside and applying it at `compositionend` is the
127
134
  version that looks right and loses data: that value predates the
128
- composition, so painting it discards the word the reader just committed.
135
+ composition, so painting it discards the word the user just committed.
129
136
  Deferring the write instead has the same shape of bug one layer down, since
130
137
  the field's value then has no record of the peer's edit and `update` diffs
131
138
  it back out.
@@ -152,7 +159,7 @@ revision pattern:
152
159
  | 300 KB | 4.09 |
153
160
 
154
161
  One paragraph, one description, or one report is far inside a frame at
155
- typing speed. A field holding a book is not, and an app whose document
162
+ typing speed. A field containing a book is not, and an app whose document
156
163
  grows that large wants its prose split across containers, one per section,
157
164
  so an edit compares against a section rather than the book.
158
165
 
@@ -167,9 +174,9 @@ per-session, since a snapshot carries the history.
167
174
 
168
175
  A text container is a top-level root, and no nested-text surface exists, so
169
176
  per-element prose that merges costs one root per element. Loro prices a
170
- container against how many the document holds, so that does not stay flat
171
- the way the record store does, where paging holds container count down past
172
- `UNPAGED_MAX_RECORDS`. Creating one text root per element against the same
177
+ container against how many the document contains, so that does not stay flat
178
+ the way the record store does, where paging keeps container count down once
179
+ a map contains many records. Creating one text root per element against the same
173
180
  elements written as records:
174
181
 
175
182
  | elements | a root each | as records | roots heap | records heap |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brass-build/client",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Client SDK for Brass",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -39,8 +39,11 @@
39
39
  "prepublishOnly": "tsc -b --force"
40
40
  },
41
41
  "dependencies": {
42
+ "apache-arrow": "^21.2.0",
42
43
  "fflate": "^0.8.3",
43
- "loro-crdt": "^1.13.0"
44
+ "flatbuffers": "^25.1.24",
45
+ "fzstd": "^0.1.1",
46
+ "loro-crdt": "1.13.9"
44
47
  },
45
48
  "devDependencies": {
46
49
  "typescript": "~6.0.0"