@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
@@ -14,8 +14,8 @@ build: a bundler that skips dot-directories drops it silently.
14
14
 
15
15
  `brass publish` reads that same path out of the directory it publishes
16
16
  (`<dir>/.well-known/brass-app.json`), falling back to `brass-app.json` in
17
- the working directory, so it takes your `name` and `client_token` from the
18
- manifest you already ship.
17
+ the working directory, and refuses to publish a manifest whose `$ref`
18
+ entries do not resolve.
19
19
 
20
20
  The platform re-reads the manifest after each deploy, so publishing
21
21
  your bundle is what registers it. If Brass hosts your bundle
@@ -25,8 +25,12 @@ the bundle from your own origin, the platform fetches
25
25
  `<origin>/.well-known/brass-app.json` as an anonymous client, so serve
26
26
  that path publicly even when the rest of your app requires sign-in.
27
27
 
28
- The `opens` and `creates` tokens below only drive discovery and grant
29
- no access, so you declare them yourself.
28
+ Brass applies your published manifest, including to your app running
29
+ on your own machine, so publish after every change to it.
30
+
31
+ Brass uses `opens`, `inputs` and `creates` below to decide where to
32
+ offer your app. Declaring them grants your app no access to any
33
+ document.
30
34
 
31
35
  ## What goes in it
32
36
 
@@ -47,15 +51,57 @@ no access, so you declare them yourself.
47
51
 
48
52
  Every field is optional, so ship only the ones your app uses.
49
53
 
50
- - **`opens`** lists the `schema_type`s of documents your app reads.
51
- Declaring a `schema` already folds its family in, so most apps list
52
- nothing extra here.
53
- - **`imports`** matches an uploaded file by extension or content type,
54
- so Brass offers your app to whoever uploads one. Your app then reads
55
- the file's bytes through `sourceBlob()`.
56
- - **`creates`** is the `schema_type` Brass stamps on the documents your
57
- app makes, so peer apps that declare `opens` for the token can open
58
- them. It folds into your own `opens` as well.
54
+ - **`opens`** lists the families of documents your app opens as
55
+ the document a user is working in. Brass offers your app in "Open
56
+ with" for them, and a user can make it their default. Declaring a `schema`
57
+ already folds its family in, so most apps list nothing extra here.
58
+ - **`inputs`** lists the schemas your app reads as inputs, such as the
59
+ models and drawings a dashboard counts. Each entry is a schema,
60
+ `{ "family", "streams" }`. The hosted picker presents a document to the
61
+ user when it matches one of these schemas (see
62
+ [What an app can open](./schemas.md#what-an-app-can-open)). For files that
63
+ have not been imported yet, the picker evaluates compatibility against the
64
+ schema declared by the file's importer. A schema declared only under
65
+ `inputs` does not add your app to the "Open with" menu for those documents.
66
+ Declare a schema under `inputs` rather than its family under `opens`
67
+ when your app reads documents through `Brass.chooseInputs` and never
68
+ edits them. Extract the schema from an existing document with
69
+ `npx @brass-build/cli schema pull --doc <docId> --out input-schema.json`,
70
+ which writes to that file while leaving your manifest's own `schema`
71
+ intact. Copy that file's `schema` into an `inputs` entry, then trim it to
72
+ only the required fields your code reads, since fewer requirements match
73
+ a wider set of documents.
74
+
75
+ ```json
76
+ {
77
+ "inputs": [
78
+ {
79
+ "family": "ifc",
80
+ "streams": { "default": { "type": "object", "required": ["instances"] } }
81
+ }
82
+ ]
83
+ }
84
+ ```
85
+
86
+ - **`imports`** lists the file types your app imports. Brass offers your app
87
+ to whoever uploads a matching file and passes the file to your
88
+ `Brass.registerImporter` callback. Each entry has these fields:
89
+
90
+ | Field | |
91
+ | --- | --- |
92
+ | `extension` | The file extension to match, without the dot, in any case, such as `"csv"`. |
93
+ | `content_type` | The media type to match exactly, such as `"text/csv"`. |
94
+ | `schema` | Optional. The schema of the document your importer makes from a matching file. Without it, your app's `schema` applies. |
95
+
96
+ An entry needs `extension` or `content_type`, and a file matches an entry
97
+ when it matches every field the entry sets. A file whose entry has no
98
+ `schema`, in an app with no `schema`, is not imported.
99
+ - **`creates`** is the family Brass stamps on the documents your
100
+ app makes, so peer apps that declare `opens` for that family can open
101
+ them. It folds into your own `opens` as well. When you declare a
102
+ `schema`, Brass stamps `schema.family` instead, so either leave
103
+ `creates` out or set it to the same family. `brass publish` warns
104
+ when the two differ.
59
105
  - **`exports`** names the file format your app writes a document out
60
106
  to, and your `Brass.registerSerializer` callback produces the bytes.
61
107
  Declaring it makes those documents downloadable. Declare it when your
@@ -74,6 +120,64 @@ Every field is optional, so ship only the ones your app uses.
74
120
  - **`schema`** declares the record shape your app works with, whether
75
121
  you designed it or adopted an existing one. See
76
122
  [`schemas.md`](./schemas.md).
123
+ - **`shared_schemas`** defines schemas by name for other fields to refer
124
+ to. See [Shared schemas](#shared-schemas) below.
125
+ - **`access`** says who may use your app. Declare `"create": "anyone"`
126
+ when a visitor who is not signed in can do something in your app, so
127
+ they can try it before creating an account. A visitor belongs to no
128
+ organization, so `Brass.chooseInputs`, `Brass.chooseInput` and calls to
129
+ a service your manifest declares are not available to them.
130
+
131
+ | Field | Values |
132
+ | --- | --- |
133
+ | `create` | Who may start a new document in your app. `false` when your app makes no new documents of its own, such as a viewer of files other apps import: Brass then offers no New for it, and if your app calls `Brass.createDocument` anyway, the call throws a `BrassError` whose `code` is `"create_not_offered"`. Brass still opens uploaded and imported files in it. `"signed-in"` for people who are signed in. `"anyone"` adds visitors without an account, who can create, upload and edit temporary documents. For `"anyone"`, publish the app with `--gate off --visibility public`. |
134
+ | `open` | Who may open an organization's public link in your app: `"anyone"`, or `"signed-in"` to ask a visitor to sign in first. |
135
+
136
+ When you leave `create` out, Brass uses `"anyone"` for a public app
137
+ whose load gate is off and `"signed-in"` otherwise, so declare it to
138
+ keep the same answer when the gate changes. When you leave `open` out,
139
+ Brass uses `"anyone"`.
140
+
141
+ ```json
142
+ { "access": { "create": "anyone", "open": "anyone" } }
143
+ ```
144
+
145
+ The hosted load gate (`brass publish --gate on|off`) is separate from
146
+ `access`: it decides whether your hosted bundle loads for a visitor who
147
+ is not signed in, before any of your code or this manifest runs.
148
+
149
+ ## Shared schemas
150
+
151
+ Define schemas by name in `shared_schemas`. To use one, write
152
+ `{ "$ref": "#/shared_schemas/<name>" }` in place of a schema in `schema`, a
153
+ `inputs` entry, or an `imports` entry's `schema`. Brass treats a reference as
154
+ the schema it names.
155
+
156
+ This app writes `tracker` documents, reads as inputs any tracker that has
157
+ `rows`, and imports CSV files as `csv-table` documents that it also reads
158
+ as inputs:
159
+
160
+ ```json
161
+ {
162
+ "shared_schemas": {
163
+ "tracker": {
164
+ "family": "tracker",
165
+ "streams": { "default": { "type": "object", "required": ["rows", "columns"] } }
166
+ },
167
+ "tracker-input": {
168
+ "family": "tracker",
169
+ "streams": { "default": { "type": "object", "required": ["rows"] } }
170
+ },
171
+ "csv-table": {
172
+ "family": "csv-table",
173
+ "streams": { "default": { "type": "object", "required": ["header", "rows"] } }
174
+ }
175
+ },
176
+ "schema": { "$ref": "#/shared_schemas/tracker" },
177
+ "inputs": [{ "$ref": "#/shared_schemas/tracker-input" }, { "$ref": "#/shared_schemas/csv-table" }],
178
+ "imports": [{ "extension": "csv", "schema": { "$ref": "#/shared_schemas/csv-table" } }]
179
+ }
180
+ ```
77
181
 
78
182
  ## Services, for calling third-party APIs
79
183
 
@@ -132,7 +236,7 @@ because that name belongs to whoever created the bucket.
132
236
 
133
237
  ### Credential fields
134
238
 
135
- A field is one value the person connecting a credential is asked for. Each one
239
+ A field is one value the user connecting a credential is asked for. Each one
136
240
  is a box on the form they fill in.
137
241
 
138
242
  Some authentication methods declare their own fields, and each method below
@@ -142,7 +246,7 @@ lists the ones it declares. Declare any others in `fields`:
142
246
  | --- | --- |
143
247
  | `name` | Required. Identifies the field. |
144
248
  | `secret` | `true` by default. Brass encrypts the value and shows only its last few characters afterwards. `false` stores it in the clear and shows it in full. |
145
- | `label` | What the form calls the field. Use the vendor's own word for it, since that is what the person is reading from. Defaults to the name. |
249
+ | `label` | What the form calls the field. Use the vendor's own word for it, since that is what the user is reading from. Defaults to the name. |
146
250
  | `readable` | `false` by default. `true` returns the value to your app with the service, for a value your app has to include in the request. Such a field is not secret, so `secret` defaults to `false` for it and declaring both is refused. See [Values the app reads](./services.md#values-the-app-reads). |
147
251
 
148
252
  ```json
@@ -200,9 +304,9 @@ The fields are `username` and `password`.
200
304
  { "host": "api.twilio.com", "auth": { "type": "basic" } }
201
305
  ```
202
306
 
203
- Some services take a fixed word as the user half rather than an account name,
307
+ Some services take a fixed word as the username rather than an account name,
204
308
  such as `api` for Mailgun. Set it as `username` on the `auth` block and the
205
- person is asked for the password alone.
309
+ form asks for the password alone.
206
310
 
207
311
  ```json
208
312
  { "host": "api.mailgun.net", "auth": { "type": "basic", "username": "api" } }
@@ -44,7 +44,9 @@ Each piece renders one cluster wired to `Brass.session` and returns
44
44
  - `Brass.mountDocumentTitle` renders the document title plus a
45
45
  location chip; both open the hosted location editor (rename / move /
46
46
  file). On a non-default branch the title carries a branch chip.
47
- - `Brass.mountUndoRedo` renders undo / redo as prominent buttons.
47
+ - `Brass.mountUndoRedo` renders undo / redo as icon buttons once either
48
+ can act, and nothing before then, so leave its container free to
49
+ collapse.
48
50
  - `Brass.mountPresenceRoster` renders the who's-here roster of
49
51
  coloured initials. It is the receiver half only: still publish your
50
52
  own presence state (see the README's "Presence").
@@ -55,17 +57,19 @@ Each piece renders one cluster wired to `Brass.session` and returns
55
57
  - `Brass.mountAccountMenu` renders the account avatar menu (email, an
56
58
  Open Brass dashboard link, and sign out).
57
59
 
58
- `mountHeader` already includes the historical-version preview banner
59
- (the return-to-live strip shown while a document is previewing an
60
- earlier version) above the bar, so a multi-pane app on the header shell
61
- needs no extra mount for it. A fully custom shell (next section, no
60
+ `mountHeader` draws two strips under the bar. One tells a visitor whose
61
+ document was made without an account that it is temporary, and offers
62
+ sign-in. The other is
63
+ the historical-version preview banner (the return-to-live strip shown
64
+ while a document is previewing an earlier version). A multi-pane app on
65
+ the header shell gets both without another mount. A fully custom shell (next section, no
62
66
  `mountHeader`) mounts it itself with `Brass.mountVersionPreview(el)`,
63
67
  which renders nothing until there is something to show, like the drive
64
68
  badge.
65
69
 
66
- Mount the chrome once, when the user signs in, not per document: the
67
- file menu has to exist in the "signed in, nothing open" state or a
68
- fresh user has no New / Open (the same rule as `mountToolbar`).
70
+ Mount the chrome once, when the user signs in or becomes a guest, not
71
+ per document: the file menu has to exist before any document is open or
72
+ a fresh user has no New / Open (the same rule as `mountToolbar`).
69
73
 
70
74
  ## Fully custom shells: the command list as data
71
75
 
@@ -87,8 +91,8 @@ The full built-in set when a doc is bound, in list order:
87
91
 
88
92
  | `id` | `group` | `surface` | `label` | Enabled when |
89
93
  | ---------- | ---------- | --------- | ------------------ | --------------------------------------- |
90
- | `new` | `session` | `menu` | New | Signed in. |
91
- | `open` | `session` | `menu` | Open... | Signed in. |
94
+ | `new` | `session` | `menu` | New | Signed in, or signed out where `access.create` in your manifest resolves to `"anyone"`. Never when it is `false`. |
95
+ | `open` | `session` | `menu` | Open... / Upload... (signed out) | Signed in, or signed out where `access.create` in your manifest resolves to `"anyone"`. |
92
96
  | `history` | `document` | `menu` | Version history... | A document is open. |
93
97
  | `branches` | `document` | `menu` | Branches... | A document is open. Has `children`. |
94
98
  | `share` | `document` | `menu` | Share... / Share / Download... | A document is open. |
@@ -98,8 +102,8 @@ The full built-in set when a doc is bound, in list order:
98
102
 
99
103
  The order runs use, then manage, then destructive: `new` / `open` put a
100
104
  different document on screen, `history` / `branches` put a different
101
- state of this one there, `share` hands the document to a person or to a
102
- file, and `manage` (the entry holding the delete) comes last. Render the
105
+ state of this one there, `share` shares the document with someone or
106
+ exports it to a file, and `manage` (the entry that offers delete) comes last. Render the
103
107
  list in the order you receive it.
104
108
 
105
109
  Each command carries `enabled` (read on every notification),
@@ -108,8 +112,8 @@ affordance), and `run()` (always resolves; failures route to
108
112
  `Brass.onError`, never throw).
109
113
 
110
114
  Read `label` on every notification too. `share` relabels once the SDK
111
- knows whether the document has a file to hand over, since downloading is
112
- one row of the share dialog rather than a command of its own.
115
+ knows whether the document has an exportable file, since downloading is
116
+ one option in the share dialog rather than a command of its own.
113
117
 
114
118
  Draw a rule where `group` changes: `'session'` (a different document),
115
119
  `'document'` (this document), `'edit'` (undo / redo). The widgets do
@@ -132,7 +136,11 @@ invoke, and the SDK owns the behavior:
132
136
 
133
137
  - **`new`** creates a document and binds it, asking the platform where
134
138
  to put it (`Brass.session.create()` under the hood).
135
- - **`open`** opens the hosted picker and binds what the user chooses.
139
+ - **`open`** opens the hosted picker and opens the document the user chooses.
140
+ Without an account it reads `Upload...` and creates a document from a file
141
+ the visitor chooses (`Brass.session.open()` under the hood). Before either
142
+ command replaces a document the visitor created without an account and has
143
+ edited, it prompts whether to sign in to save it to their account.
136
144
  - **`history`** drives version preview end to end. During a preview
137
145
  `doc.readOnly` is `true`, so gate your own edit controls on it;
138
146
  returning to live is the chrome's job (the `mountVersionPreview`
@@ -142,7 +150,7 @@ invoke, and the SDK owns the behavior:
142
150
  invoke a child's `run()`; a flat-list shell can call the parent's
143
151
  `run()` to open the hosted picker instead.
144
152
  - **`share`** opens the hosted share dialog: who can open the document,
145
- and, where there is a file to hand over, the download.
153
+ and, where an exportable file is available, the download action.
146
154
  - **`manage`** opens the same dialog on the document itself: its name,
147
155
  where it sits, Duplicate, and Delete.
148
156
  - **`undo` / `redo`** call `doc.undo()` / `doc.redo()` (see "Undo,
@@ -1,7 +1,7 @@
1
1
  # Hosted UI: advanced
2
2
 
3
3
  Brass owns some surfaces: you call one method and Brass renders the UI
4
- in a modal iframe over your page. `Brass.session` wires the common ones
4
+ in a modal iframe over your page. `Brass.session` runs the common ones
5
5
  (`history`, `share`, `manage`) behind the file menu. This page covers
6
6
  the rest and how they behave in browsers that block the iframe.
7
7
 
@@ -9,9 +9,34 @@ the rest and how they behave in browsers that block the iframe.
9
9
 
10
10
  | Method | Returns |
11
11
  | --- | --- |
12
- | `Brass.selectDocument()` | `Promise<SelectDocumentResult \| null>`: opens the hosted document picker; resolves to the picked document, or `null` if the user dismisses it. |
13
12
  | `Brass.shareDocument(docId)` | `Promise<void>`: opens the hosted share dialog for the document. |
14
13
 
14
+ ## Narrowing a choice to the files you need
15
+
16
+ `Brass.chooseInputs(families, options?)` is how a user chooses files,
17
+ folders and projects for your app to read (see the README). `accept`
18
+ narrows the files of those families, for one call, by name or extension. Pass it once your app knows which files it
19
+ needs. An app that has just parsed a federation file asks for the models
20
+ that file names:
21
+
22
+ ```js
23
+ await Brass.chooseInputs(['ifc'], {
24
+ prompt: `Models in ${federation.name}`,
25
+ accept: { names: federation.modelFileNames },
26
+ });
27
+ ```
28
+
29
+ The picker renders `prompt` as a subtitle beneath its title. Pass a prompt
30
+ when narrowing the list with `accept`, explaining why only specific files appear.
31
+ On a request you do not narrow, omit the prompt as the picker already shows
32
+ all documents your app can open.
33
+
34
+ `names` matches both the document's name and the original filename it was imported
35
+ from, and `extensions` matches either name's extension. A document in the specified
36
+ families passes when it matches any given name or extension. When a user selects a
37
+ folder or project, this filtering persists, so listing the container returns only
38
+ the matching documents.
39
+
15
40
  Version history is a built-in file-menu command (`history` in
16
41
  `session.commands` and the SDK toolbar's file menu) rather than a
17
42
  launcher method, so an app on `mountToolbar` or `mountFileMenu` gets it
@@ -22,13 +47,18 @@ with no code.
22
47
  Where the browser blocks the Hosted UI iframe (Safari, Firefox, Chrome
23
48
  with third-party cookies off), these surfaces open as a full-page
24
49
  redirect to Brass and return to your app when the user is done. The
25
- picker returns with the chosen document opened into `Brass.session`.
50
+ Open picker (`Brass.session.open()`) returns with the chosen document
51
+ opened into `Brass.session`.
26
52
 
27
53
  So render the active document from the session (or `mountToolbar`): on
28
54
  the redirect path the page reloads, so the code after `await
29
- Brass.selectDocument()` never runs, and the pick arrives through the
30
- session instead. The resolved value is the modal path's convenience,
31
- correct whenever you receive it.
55
+ Brass.session.open()` never runs, and the picked document opens into the
56
+ session on reload instead.
57
+
58
+ `chooseInputs` and `chooseInput` do not redirect. Where the browser blocks
59
+ their iframe they reject with a `BrassError` whose `code` is
60
+ `'iframe_unavailable'`. Show the error's `message` and keep the references
61
+ your app already has.
32
62
 
33
63
  ## `BrassError` `.code` values
34
64
 
@@ -38,6 +68,8 @@ The hosted-UI launchers reject with `BrassError` carrying a `.code`
38
68
  - `'embed_error'`: the hosted page loaded but couldn't proceed (e.g.
39
69
  the user isn't signed in to Brass in this context).
40
70
  - `'not_in_browser'`: called outside a browser environment.
71
+ - `'iframe_unavailable'`: the browser blocked the iframe and the surface
72
+ has no redirect to fall back on (`chooseInputs`, `chooseInput`).
41
73
 
42
74
  ## Theming
43
75
 
@@ -11,67 +11,123 @@ editor: declare the document's schema in `brass-app.json` so peers can
11
11
  open it, and read the records through the ordinary handle surface. See
12
12
  [`schemas.md`](./schemas.md).
13
13
 
14
- **Any other file.** The document is an opaque file (source `schema_type`
15
- is `brass:file`) until your app parses it. Register the parse half with
14
+ **Any other file.** Nothing imports the file until your app registers an
15
+ importer for its type. Register the parse half with
16
16
  `Brass.registerImporter` and the inverse with `Brass.registerSerializer`.
17
17
 
18
18
  ## Registering an importer
19
19
 
20
20
  Register your parse half once at boot, and Brass runs it when an import
21
- is needed:
21
+ is needed. Call `Brass.registerImporter` at startup, unconditionally: not
22
+ after sign-in, and not after a document opens. Brass may load your app only
23
+ to run its importer, in a page where nobody signs in and no document opens,
24
+ and an importer registered behind either step is never registered there.
22
25
 
23
26
  ```ts
24
- Brass.registerImporter(async (src, doc) => {
25
- // src: { url, filename, content_type, byte_size, expires_in, streams }
26
- const bytes = new Uint8Array(await (await fetch(src.url)).arrayBuffer());
27
- const parsed = parseMyFormat(bytes); // your own parser
28
- const records = doc.orderedMap('instances', 'instance_order');
29
- records.mutate((m) => {
30
- for (const r of parsed) m[r.id] = { type: r.type, params: r.body };
31
- });
32
- await doc.createCheckpoint();
27
+ Brass.registerImporter(async (file) => {
28
+ const parsed = parseMyFormat(await file.bytes()); // your own parser
29
+ const instances: Record<string, unknown> = {};
30
+ for (const r of parsed) instances[r.id] = { type: r.type, params: r.body };
31
+ return {
32
+ streams: {
33
+ default: { instances, instance_order: parsed.map((r) => r.id) },
34
+ },
35
+ };
33
36
  });
34
37
  ```
35
38
 
36
- The importer receives the stored source file (a short-lived download URL
37
- plus the file's name, type, and size, with the bytes not inlined) and the
38
- open document. `DocumentImporter` types the callback.
39
-
40
- Brass decides when an import is needed and runs your importer once. A
41
- document that already holds its records, or has no stored file, is left
42
- alone, and the records reach every open client through its normal
43
- subscription. Declare the `schema` of the records your importer writes in
44
- `brass-app.json`: importing publishes it and stamps it on the document.
45
-
46
- Sealing the write with `doc.createCheckpoint()` makes the import one undo
47
- step, so an undo reverts it whole instead of leaving a half-deleted
48
- model. An importer that throws surfaces through `Brass.onError`. Brass
49
- retries an import that wrote no records. Once records are written, it
50
- refuses a later import of that document, because running the importer
51
- again over them would write each of those records twice.
39
+ The importer takes a file and returns JSON, so a test can call it
40
+ directly with a file the test builds. `DocumentImporter` types the
41
+ callback, `ImportFile` types the file it receives (`name`,
42
+ `content_type`, `byte_size`, and `bytes()`, which fetches the file), and
43
+ `ImportResult` types what it returns.
44
+
45
+ `streams` is the document's content, keyed by stream name. The
46
+ document opens on `default`. Your app opens any other stream with
47
+ `doc.stream(name)`, so put content there that a user can skip while
48
+ reading the rest (a heavy geometry layer beside the elements). Each stream
49
+ is a JSON object whose top-level values are objects, arrays, or strings. A
50
+ stream name uses lowercase letters, digits, `.`, `-`, and `_`. The
51
+ `schema` in your `brass-app.json` describes this shape, and
52
+ `npx @brass-build/cli contents --doc <docId>` writes the content of a
53
+ document your importer produced to `contents.json` (`--stdout` prints it
54
+ instead).
55
+
56
+ Brass decides when a document needs an import and runs your importer
57
+ once. Every client with the document open then receives the whole result
58
+ together. Declare the `schema` of the records your importer returns in
59
+ `brass-app.json`. The import publishes that schema and stamps it on the
60
+ document.
61
+
62
+ When your importer throws, or returns no streams or only empty
63
+ collections, Brass reports the error through `Brass.onError`. Brass also
64
+ records the failure on the document and does not run your importer on
65
+ that file again until the file changes. Everyone who opens the document
66
+ sees your error's message after your app's name, so write it for that
67
+ user: "Row 3 has no date column" rather than a stack trace.
52
68
 
53
69
  Key each record by the format's own stable id. When the format has no
54
70
  native id, derive one from the record's own content rather than from its
55
71
  position in the file, which is not stable across a re-import.
56
72
 
57
- ## Reading the source bytes
73
+ ## Reporting what the import skipped or assumed
58
74
 
59
- A source file is a set of one or more byte streams: the primary stream
60
- (what the filename names) plus any sidecar streams uploaded with it (a
61
- glTF's `.bin` buffer, a shapefile's `.dbf`). `src.streams` lists every
62
- stream, primary first, each as `{ name, url, content_type, byte_size }`
63
- with its own download URL, so an app fetches exactly the streams it
64
- needs, in parallel. A single-stream file has exactly one entry, so code
65
- that iterates `streams` handles both cases in one path:
75
+ When your importer reads only part of a file, return an `ImportReport`
76
+ beside the streams that says what it left out and what it filled in.
77
+ Brass stores the report with the import and shows it, in your words, on
78
+ the document's page:
66
79
 
67
80
  ```ts
68
- const byName = new Map(src.streams.map((s) => [s.name, s]));
69
- const buffer = byName.get('scene.bin'); // undefined when not part of the set
81
+ Brass.registerImporter(async (file) => {
82
+ const rows = parseCsv(new TextDecoder().decode(await file.bytes()));
83
+ const content = rowsToContent(rows); // your own mapping
84
+ const findings: ImportFinding[] = [];
85
+ if (rows.ragged > 0) {
86
+ findings.push({
87
+ kind: 'skipped',
88
+ message: `Skipped ${rows.ragged} rows with more fields than the header.`,
89
+ });
90
+ }
91
+ if (!rows.delimiterDeclared) {
92
+ findings.push({
93
+ kind: 'assumed',
94
+ message: 'The file does not say how its fields are separated, so commas were used.',
95
+ });
96
+ }
97
+ return findings.length > 0
98
+ ? { streams: { default: content }, report: { summary: `Read ${rows.length} rows.`, findings } }
99
+ : { streams: { default: content } };
100
+ });
70
101
  ```
71
102
 
72
- `doc.sourceBlob()` is the same read for code outside the importer that
73
- needs the original bytes (a "view original" affordance). It rejects with
74
- a `BrassError` for a document that has no stored original file.
103
+ `summary` is one optional sentence stating what the import read, which Brass
104
+ shows above the findings.
105
+
106
+ Each finding's `message` is one sentence, and the user sees it as you
107
+ wrote it. Its `kind` says what the sentence describes:
108
+
109
+ - `skipped` is content in the file that the import did not read.
110
+ - `assumed` is a value the file did not give, which the import supplied.
111
+ - `missing` is something the file names that did not arrive (a texture a
112
+ model refers to, a drawing it references).
113
+
114
+ Return a report only when your importer has findings. Report only what
115
+ your importer can establish from the file, and do not tell the user a
116
+ file was read completely when the format cannot confirm it.
117
+
118
+ An app reads an open document's report from `doc.importReport` (`null`
119
+ when there is none), whichever importer wrote it. A `skipped` or `missing` finding means the document
120
+ contains only part of its file. An `assumed` finding means the importer
121
+ supplied some of the document's values rather than reading them from the
122
+ file.
123
+
124
+ ## Reading the source bytes
125
+
126
+ Code outside the importer that needs the original bytes (a "view
127
+ original" affordance) calls `doc.sourceBlob()`. It resolves the stored
128
+ file as `{ url, filename, content_type, byte_size }`, where `url` is a
129
+ short-lived download URL. It rejects with a `BrassError` for a document
130
+ that has no stored original file.
75
131
 
76
132
  ## Registering a serializer
77
133
 
@@ -83,23 +139,20 @@ after `Brass.init`:
83
139
  ```ts
84
140
  Brass.registerSerializer((doc) => {
85
141
  const records = doc.orderedMap('instances', 'instance_order');
86
- const text = myFormatToText(records.ordered()); // inverse of your parser
142
+ const text = myFormatToText(records.entries()); // inverse of your parser
87
143
  return new TextEncoder().encode(text);
88
144
  });
89
145
  ```
90
146
 
91
147
  The serializer returns the bytes (`Uint8Array`, or a promise of one).
92
- `DocumentSerializer` types the callback. It runs detached, with no
93
- subscription feeding it, so the document it is handed reads its
94
- containers whole: `StoredDocument` types that document, and its `map` /
95
- `list` return a `StoredMap` / `StoredList`, which add `read()` to the
96
- handle an app holds.
148
+ `DocumentSerializer` types the callback. It receives the same document
149
+ your app reads, and walks each container with `entries()` or `values()`.
97
150
 
98
151
  Your app exports so that software outside Brass can read its work, so
99
- write a format that software already opens. If your app imports a file,
100
- write that same format back out. If your app makes its documents from
101
- nothing, ask the people who use it which tool they need the download to
102
- open in, and write that tool's format.
152
+ write a format that outside software already supports. If your app imports a file
153
+ format, export that same format back out. If your app creates documents from
154
+ scratch, determine which formats users need to open in outside tools and export
155
+ that format.
103
156
 
104
157
  ## Declaring what your app handles
105
158
 
package/docs/presence.md CHANGED
@@ -3,7 +3,10 @@
3
3
  `doc.presence` is the "who is here right now" layer over an open
4
4
  document: the roster of other live participants plus a small per-user
5
5
  `state` (cursor / camera / selection) and an app-namespaced `ext` blob.
6
- It is non-null on any open writable `Document`, and `null` otherwise.
6
+ It is non-null on any live open, editable or `{ readOnly: true }`,
7
+ including a signed-out visitor's open through a shared link, so a viewer
8
+ shows up in the roster beside the editors, and `null` on a
9
+ `{ frozen: true }` copy.
7
10
  Presence is fully ephemeral: nothing survives a disconnect.
8
11
 
9
12
  ```ts
@@ -16,6 +19,8 @@ const doc = await Brass.openDocumentById('doc_abc');
16
19
  // (e.g. on every pointermove).
17
20
  doc.presence?.set({ state: { cursor: { x, y, z } } });
18
21
  doc.presence?.set({ ext: { myApp: { tool: 'select' } } });
22
+ // A key set to `undefined` is removed, for you and for every peer.
23
+ doc.presence?.set({ state: { selection: undefined } });
19
24
 
20
25
  // React to everyone else. `roster` is the other participants; each
21
26
  // carries a server-stamped `displayName` + `color` (CSS-ready string,
@@ -78,8 +83,8 @@ interface PresenceState {
78
83
 
79
84
  A caret belongs in `state` rather than in `ext` for the same reason a
80
85
  selection does: a document of prose is often open in more than one app,
81
- and a reader wants to see where the other person is working whichever
82
- app they are each in. Publish it on every selection change, which the
86
+ and each user wants to see where the others are working, whichever
87
+ app each is in. Publish it on every selection change, which the
83
88
  10 Hz debounce already paces.
84
89
 
85
90
  App-specific state (the current tool, a chat bubble, a typing
@@ -122,7 +127,14 @@ is a `PresenceParticipant` with
122
127
  `{ connectionId, sub, displayName, avatarUrl, color, joinedAt, lastSeen,
123
128
  state, ext }`. `displayName`, `avatarUrl`, and `color` are always
124
129
  present as strings, and `avatarUrl` is `''` when the user has no
125
- picture. Both types are exported from `@brass-build/client`, alongside
130
+ picture. A signed-out visitor on a shared link has `sub: ''` and a
131
+ `displayName` such as `Guest 1a2b3c`, so when you group participants
132
+ into people by `sub`, show each participant with an empty `sub` on its
133
+ own, keyed by `connectionId`. A visitor's second tab is a second guest.
134
+ On that visitor's page, every signed-in participant also arrives with
135
+ `sub: ''`, an empty `avatarUrl` and a `displayName` such as
136
+ `Member 4d5e6f`.
137
+ Both types are exported from `@brass-build/client`, alongside
126
138
  `PresenceSetInput` and `PresenceChangeListener`, for annotating your
127
139
  handlers and roster state.
128
140