@brass-build/client 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/AGENTS.md +124 -45
  2. package/CHANGELOG.md +80 -4
  3. package/README.md +666 -179
  4. package/dist/api-fetch.d.ts +8 -0
  5. package/dist/api-fetch.d.ts.map +1 -1
  6. package/dist/api-fetch.js +35 -8
  7. package/dist/api-fetch.js.map +1 -1
  8. package/dist/batched-get.d.ts +11 -0
  9. package/dist/batched-get.d.ts.map +1 -0
  10. package/dist/batched-get.js +92 -0
  11. package/dist/batched-get.js.map +1 -0
  12. package/dist/call-service.d.ts +4 -0
  13. package/dist/call-service.d.ts.map +1 -1
  14. package/dist/call-service.js +66 -3
  15. package/dist/call-service.js.map +1 -1
  16. package/dist/client.d.ts +48 -2
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +918 -115
  19. package/dist/client.js.map +1 -1
  20. package/dist/document-checkpoints.d.ts +25 -2
  21. package/dist/document-checkpoints.d.ts.map +1 -1
  22. package/dist/document-checkpoints.js +258 -61
  23. package/dist/document-checkpoints.js.map +1 -1
  24. package/dist/document-commands.d.ts.map +1 -1
  25. package/dist/document-commands.js +5 -3
  26. package/dist/document-commands.js.map +1 -1
  27. package/dist/document-contents.d.ts +10 -0
  28. package/dist/document-contents.d.ts.map +1 -1
  29. package/dist/document-contents.js +23 -1
  30. package/dist/document-contents.js.map +1 -1
  31. package/dist/document-set-subscription.d.ts +56 -0
  32. package/dist/document-set-subscription.d.ts.map +1 -0
  33. package/dist/document-set-subscription.js +240 -0
  34. package/dist/document-set-subscription.js.map +1 -0
  35. package/dist/document-signals.d.ts +6 -0
  36. package/dist/document-signals.d.ts.map +1 -1
  37. package/dist/document-signals.js +31 -1
  38. package/dist/document-signals.js.map +1 -1
  39. package/dist/document-streams.d.ts +36 -2
  40. package/dist/document-streams.d.ts.map +1 -1
  41. package/dist/document-streams.js +69 -18
  42. package/dist/document-streams.js.map +1 -1
  43. package/dist/document-thumbnail-capture.d.ts +12 -2
  44. package/dist/document-thumbnail-capture.d.ts.map +1 -1
  45. package/dist/document-thumbnail-capture.js +103 -27
  46. package/dist/document-thumbnail-capture.js.map +1 -1
  47. package/dist/documents-namespace.d.ts +92 -10
  48. package/dist/documents-namespace.d.ts.map +1 -1
  49. package/dist/documents-namespace.js +285 -39
  50. package/dist/documents-namespace.js.map +1 -1
  51. package/dist/documents.d.ts +173 -8
  52. package/dist/documents.d.ts.map +1 -1
  53. package/dist/documents.js +1778 -310
  54. package/dist/documents.js.map +1 -1
  55. package/dist/errors.d.ts +3 -3
  56. package/dist/errors.d.ts.map +1 -1
  57. package/dist/errors.js.map +1 -1
  58. package/dist/fetch-retry.d.ts +1 -0
  59. package/dist/fetch-retry.d.ts.map +1 -1
  60. package/dist/fetch-retry.js +2 -0
  61. package/dist/fetch-retry.js.map +1 -1
  62. package/dist/file-choice.d.ts +2 -0
  63. package/dist/file-choice.d.ts.map +1 -0
  64. package/dist/file-choice.js +27 -0
  65. package/dist/file-choice.js.map +1 -0
  66. package/dist/guest-link.d.ts +18 -1
  67. package/dist/guest-link.d.ts.map +1 -1
  68. package/dist/guest-link.js +329 -1
  69. package/dist/guest-link.js.map +1 -1
  70. package/dist/handle-types.d.ts +67 -5
  71. package/dist/handle-types.d.ts.map +1 -1
  72. package/dist/handles.d.ts +15 -19
  73. package/dist/handles.d.ts.map +1 -1
  74. package/dist/handles.js +231 -76
  75. package/dist/handles.js.map +1 -1
  76. package/dist/hosted-ui.d.ts +61 -0
  77. package/dist/hosted-ui.d.ts.map +1 -1
  78. package/dist/hosted-ui.js +94 -42
  79. package/dist/hosted-ui.js.map +1 -1
  80. package/dist/import-contract.d.ts +74 -0
  81. package/dist/import-contract.d.ts.map +1 -0
  82. package/dist/import-contract.js +163 -0
  83. package/dist/import-contract.js.map +1 -0
  84. package/dist/import-guest.d.ts +9 -0
  85. package/dist/import-guest.d.ts.map +1 -0
  86. package/dist/import-guest.js +155 -0
  87. package/dist/import-guest.js.map +1 -0
  88. package/dist/import-report.d.ts +17 -0
  89. package/dist/import-report.d.ts.map +1 -0
  90. package/dist/import-report.js +7 -0
  91. package/dist/import-report.js.map +1 -0
  92. package/dist/importer-wait.d.ts +2 -0
  93. package/dist/importer-wait.d.ts.map +1 -0
  94. package/dist/importer-wait.js +5 -0
  95. package/dist/importer-wait.js.map +1 -0
  96. package/dist/importer.d.ts +18 -4
  97. package/dist/importer.d.ts.map +1 -1
  98. package/dist/importer.js +322 -105
  99. package/dist/importer.js.map +1 -1
  100. package/dist/in-development.d.ts +2 -0
  101. package/dist/in-development.d.ts.map +1 -0
  102. package/dist/in-development.js +10 -0
  103. package/dist/in-development.js.map +1 -0
  104. package/dist/index.d.ts +16 -8
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +24 -8
  107. package/dist/index.js.map +1 -1
  108. package/dist/init-keys.d.ts +1 -1
  109. package/dist/init-keys.d.ts.map +1 -1
  110. package/dist/init-keys.js +6 -1
  111. package/dist/init-keys.js.map +1 -1
  112. package/dist/input-documents.d.ts +67 -0
  113. package/dist/input-documents.d.ts.map +1 -0
  114. package/dist/input-documents.js +693 -0
  115. package/dist/input-documents.js.map +1 -0
  116. package/dist/inputs-status.d.ts +80 -0
  117. package/dist/inputs-status.d.ts.map +1 -0
  118. package/dist/inputs-status.js +460 -0
  119. package/dist/inputs-status.js.map +1 -0
  120. package/dist/inputs.d.ts +2 -0
  121. package/dist/inputs.d.ts.map +1 -0
  122. package/dist/inputs.js +793 -0
  123. package/dist/inputs.js.map +1 -0
  124. package/dist/internal.d.ts +6 -1
  125. package/dist/internal.d.ts.map +1 -1
  126. package/dist/internal.js +4 -1
  127. package/dist/internal.js.map +1 -1
  128. package/dist/join-content.d.ts +33 -0
  129. package/dist/join-content.d.ts.map +1 -0
  130. package/dist/join-content.js +114 -0
  131. package/dist/join-content.js.map +1 -0
  132. package/dist/join-stamp.d.ts +3 -0
  133. package/dist/join-stamp.d.ts.map +1 -0
  134. package/dist/join-stamp.js +12 -0
  135. package/dist/join-stamp.js.map +1 -0
  136. package/dist/manifest-drift.d.ts +9 -0
  137. package/dist/manifest-drift.d.ts.map +1 -0
  138. package/dist/manifest-drift.js +46 -0
  139. package/dist/manifest-drift.js.map +1 -0
  140. package/dist/open-document.d.ts +109 -2
  141. package/dist/open-document.d.ts.map +1 -1
  142. package/dist/open-document.js +1822 -811
  143. package/dist/open-document.js.map +1 -1
  144. package/dist/page-restore.d.ts +12 -0
  145. package/dist/page-restore.d.ts.map +1 -0
  146. package/dist/page-restore.js +23 -0
  147. package/dist/page-restore.js.map +1 -0
  148. package/dist/placement.d.ts +13 -0
  149. package/dist/placement.d.ts.map +1 -0
  150. package/dist/placement.js +34 -0
  151. package/dist/placement.js.map +1 -0
  152. package/dist/presence.d.ts +1 -0
  153. package/dist/presence.d.ts.map +1 -1
  154. package/dist/presence.js +16 -4
  155. package/dist/presence.js.map +1 -1
  156. package/dist/read-only-document.d.ts +87 -0
  157. package/dist/read-only-document.d.ts.map +1 -0
  158. package/dist/read-only-document.js +4 -0
  159. package/dist/read-only-document.js.map +1 -0
  160. package/dist/record-paging.d.ts +63 -6
  161. package/dist/record-paging.d.ts.map +1 -1
  162. package/dist/record-paging.js +122 -29
  163. package/dist/record-paging.js.map +1 -1
  164. package/dist/references.d.ts +40 -0
  165. package/dist/references.d.ts.map +1 -0
  166. package/dist/references.js +135 -0
  167. package/dist/references.js.map +1 -0
  168. package/dist/request-lane.d.ts +5 -0
  169. package/dist/request-lane.d.ts.map +1 -0
  170. package/dist/request-lane.js +48 -0
  171. package/dist/request-lane.js.map +1 -0
  172. package/dist/sealed-arrow.d.ts +170 -0
  173. package/dist/sealed-arrow.d.ts.map +1 -0
  174. package/dist/sealed-arrow.js +769 -0
  175. package/dist/sealed-arrow.js.map +1 -0
  176. package/dist/sealed-cache.d.ts +21 -0
  177. package/dist/sealed-cache.d.ts.map +1 -0
  178. package/dist/sealed-cache.js +247 -0
  179. package/dist/sealed-cache.js.map +1 -0
  180. package/dist/sealed-encode.d.ts +87 -0
  181. package/dist/sealed-encode.d.ts.map +1 -0
  182. package/dist/sealed-encode.js +876 -0
  183. package/dist/sealed-encode.js.map +1 -0
  184. package/dist/sealed-handles.d.ts +45 -0
  185. package/dist/sealed-handles.d.ts.map +1 -0
  186. package/dist/sealed-handles.js +408 -0
  187. package/dist/sealed-handles.js.map +1 -0
  188. package/dist/sealed-store.d.ts +242 -0
  189. package/dist/sealed-store.d.ts.map +1 -0
  190. package/dist/sealed-store.js +1155 -0
  191. package/dist/sealed-store.js.map +1 -0
  192. package/dist/serializer.d.ts +6 -15
  193. package/dist/serializer.d.ts.map +1 -1
  194. package/dist/session-toolbar.d.ts +20 -5
  195. package/dist/session-toolbar.d.ts.map +1 -1
  196. package/dist/session-toolbar.js +419 -67
  197. package/dist/session-toolbar.js.map +1 -1
  198. package/dist/session.d.ts +38 -1
  199. package/dist/session.d.ts.map +1 -1
  200. package/dist/session.js +562 -79
  201. package/dist/session.js.map +1 -1
  202. package/dist/sha256.d.ts +3 -0
  203. package/dist/sha256.d.ts.map +1 -0
  204. package/dist/sha256.js +74 -0
  205. package/dist/sha256.js.map +1 -0
  206. package/dist/stable-handles.d.ts +19 -0
  207. package/dist/stable-handles.d.ts.map +1 -0
  208. package/dist/stable-handles.js +193 -0
  209. package/dist/stable-handles.js.map +1 -0
  210. package/dist/staged-seal.d.ts +2 -0
  211. package/dist/staged-seal.d.ts.map +1 -0
  212. package/dist/staged-seal.js +76 -0
  213. package/dist/staged-seal.js.map +1 -0
  214. package/dist/subscriber-delivery.d.ts +8 -0
  215. package/dist/subscriber-delivery.d.ts.map +1 -0
  216. package/dist/subscriber-delivery.js +31 -0
  217. package/dist/subscriber-delivery.js.map +1 -0
  218. package/dist/tail-op-import.d.ts +10 -0
  219. package/dist/tail-op-import.d.ts.map +1 -0
  220. package/dist/tail-op-import.js +33 -0
  221. package/dist/tail-op-import.js.map +1 -0
  222. package/dist/topic-lifecycle.d.ts +6 -0
  223. package/dist/topic-lifecycle.d.ts.map +1 -1
  224. package/dist/topic-lifecycle.js +32 -2
  225. package/dist/topic-lifecycle.js.map +1 -1
  226. package/dist/trial-replace-dialog.d.ts +10 -0
  227. package/dist/trial-replace-dialog.d.ts.map +1 -0
  228. package/dist/trial-replace-dialog.js +153 -0
  229. package/dist/trial-replace-dialog.js.map +1 -0
  230. package/dist/types.d.ts +8 -1
  231. package/dist/types.d.ts.map +1 -1
  232. package/dist/undo.d.ts +60 -5
  233. package/dist/undo.d.ts.map +1 -1
  234. package/dist/undo.js +344 -75
  235. package/dist/undo.js.map +1 -1
  236. package/dist/version-bar.d.ts +1 -0
  237. package/dist/version-bar.d.ts.map +1 -1
  238. package/dist/version-bar.js +2 -1
  239. package/dist/version-bar.js.map +1 -1
  240. package/dist/version.d.ts +2 -2
  241. package/dist/version.js +1 -1
  242. package/dist/ws-client.d.ts +21 -0
  243. package/dist/ws-client.d.ts.map +1 -1
  244. package/dist/ws-client.js +192 -8
  245. package/dist/ws-client.js.map +1 -1
  246. package/dist/ws-frame-limit.d.ts +1 -0
  247. package/dist/ws-frame-limit.d.ts.map +1 -1
  248. package/dist/ws-frame-limit.js.map +1 -1
  249. package/docs/app-manifest.md +121 -17
  250. package/docs/custom-chrome.md +24 -16
  251. package/docs/hosted-ui-advanced.md +38 -6
  252. package/docs/imported-documents.md +104 -51
  253. package/docs/presence.md +16 -4
  254. package/docs/schemas.md +8 -5
  255. package/docs/services.md +6 -6
  256. package/docs/streams.md +139 -150
  257. package/docs/text-fields.md +26 -19
  258. package/package.json +5 -2
  259. package/src/api-fetch.ts +53 -3
  260. package/src/auth-flow.ts +1 -1
  261. package/src/batched-get.ts +117 -0
  262. package/src/call-service.ts +78 -3
  263. package/src/client.ts +1022 -112
  264. package/src/document-checkpoints.ts +307 -65
  265. package/src/document-commands.ts +5 -5
  266. package/src/document-contents.ts +27 -1
  267. package/src/document-set-subscription.ts +274 -0
  268. package/src/document-signals.ts +47 -5
  269. package/src/document-streams.ts +85 -16
  270. package/src/document-thumbnail-capture.ts +118 -32
  271. package/src/documents-namespace.ts +539 -84
  272. package/src/documents.ts +2017 -348
  273. package/src/errors.ts +17 -6
  274. package/src/fetch-retry.ts +5 -0
  275. package/src/file-choice.ts +26 -0
  276. package/src/guest-link.ts +362 -1
  277. package/src/handle-types.ts +73 -47
  278. package/src/handles.ts +253 -83
  279. package/src/hosted-ui.ts +211 -50
  280. package/src/import-contract.ts +227 -0
  281. package/src/import-guest.ts +193 -0
  282. package/src/import-report.ts +24 -0
  283. package/src/importer-wait.ts +4 -0
  284. package/src/importer.ts +386 -131
  285. package/src/in-development.ts +10 -0
  286. package/src/index.ts +76 -16
  287. package/src/init-keys.ts +6 -1
  288. package/src/input-documents.ts +819 -0
  289. package/src/inputs-status.ts +598 -0
  290. package/src/inputs.ts +1090 -0
  291. package/src/internal.ts +7 -0
  292. package/src/join-content.ts +135 -0
  293. package/src/join-stamp.ts +12 -0
  294. package/src/manifest-drift.ts +54 -0
  295. package/src/open-document.ts +2346 -930
  296. package/src/page-restore.ts +38 -0
  297. package/src/placement.ts +54 -0
  298. package/src/presence.ts +21 -5
  299. package/src/read-only-document.ts +133 -0
  300. package/src/record-paging.ts +147 -25
  301. package/src/references.ts +207 -0
  302. package/src/request-lane.ts +59 -0
  303. package/src/sealed-arrow.ts +843 -0
  304. package/src/sealed-cache.ts +267 -0
  305. package/src/sealed-encode.ts +1015 -0
  306. package/src/sealed-handles.ts +486 -0
  307. package/src/sealed-store.ts +1330 -0
  308. package/src/serializer.ts +8 -20
  309. package/src/session-toolbar.ts +463 -72
  310. package/src/session.ts +740 -99
  311. package/src/sha256.ts +65 -0
  312. package/src/stable-handles.ts +235 -0
  313. package/src/staged-seal.ts +90 -0
  314. package/src/subscriber-delivery.ts +37 -0
  315. package/src/tail-op-import.ts +30 -0
  316. package/src/topic-lifecycle.ts +38 -2
  317. package/src/trial-replace-dialog.ts +163 -0
  318. package/src/types.ts +6 -6
  319. package/src/undo.ts +386 -73
  320. package/src/version-bar.ts +5 -1
  321. package/src/version.ts +1 -1
  322. package/src/ws-client.ts +208 -16
  323. package/src/ws-frame-limit.ts +1 -0
@@ -0,0 +1,267 @@
1
+ // Frozen bytes kept across opens, keyed by the `content_id` a join names them
2
+ // by.
3
+ //
4
+ // A join signs a new URL for the same bytes every time, so the URL cannot key
5
+ // anything and the browser's HTTP cache never hits. The id can: it names the
6
+ // content, and every copy the server stores of the same content carries the
7
+ // same id, so a join that answers an id already held is answered from the
8
+ // copy. The join itself is still made, which is where access
9
+ // is checked; nothing here is read without a join naming it first.
10
+ //
11
+ // Two tiers. Memory holds the buffers this page already decoded, which is
12
+ // what a re-open inside one page needs, and costs nothing while the document
13
+ // that decoded them is still open (it holds the same buffer). Cache Storage
14
+ // holds them across page loads, which is what a reader returning to a result
15
+ // needs. Either can be missing or refuse (no secure context, a private window,
16
+ // quota), and a refusal reads as a miss: the bytes are then downloaded, as
17
+ // they would be with no cache at all.
18
+ //
19
+ // Cache Storage outlives the page, so the identity that filled it is recorded
20
+ // beside the bytes (`adoptSealedCacheOwner`). A page that resolves a different
21
+ // identity, or none, drops the whole cache before reading from it: a sign-out
22
+ // in this page clears it on the spot, but a session that ended with the tab
23
+ // closed reaches the next person only as a new page load.
24
+
25
+ /** Fetches a signed URL's body. */
26
+ export type SealedFetch = (url: string) => Promise<Uint8Array>;
27
+
28
+ const CACHE_NAME = 'brass-sealed-v1';
29
+ // A request URL Cache Storage accepts, on a host that cannot resolve, so an
30
+ // entry is never confused with a response the page fetched.
31
+ const ENTRY_BASE = 'https://sealed.brass.invalid/';
32
+ // The identity the kept bytes were downloaded for, read synchronously so the
33
+ // check settles before any open can read the cache.
34
+ const OWNER_KEY = 'brass:sealed-owner';
35
+ const PERSISTENT_BUDGET_BYTES = 512 * 1024 * 1024;
36
+ const MEMORY_BUDGET_BYTES = 256 * 1024 * 1024;
37
+
38
+ interface CacheLike {
39
+ match(request: string, options?: { ignoreSearch?: boolean }): Promise<Response | undefined>;
40
+ put(request: string, response: Response): Promise<void>;
41
+ delete(request: string, options?: { ignoreSearch?: boolean }): Promise<boolean>;
42
+ keys(): Promise<readonly { url: string }[]>;
43
+ }
44
+
45
+ interface CacheStorageLike {
46
+ open(name: string): Promise<CacheLike>;
47
+ delete(name: string): Promise<boolean>;
48
+ }
49
+
50
+ function cacheStorage(): CacheStorageLike | null {
51
+ try {
52
+ const storage = (globalThis as { caches?: CacheStorageLike }).caches;
53
+ return storage && typeof storage.open === 'function' ? storage : null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ // Insertion order is use order: a hit moves its entry to the end.
60
+ const memory = new Map<string, Uint8Array>();
61
+ let memoryBytes = 0;
62
+ const inFlight = new Map<string, Promise<Uint8Array>>();
63
+ // Bumped by a clear, so a fetch that began before it does not write the
64
+ // previous person's bytes back afterwards.
65
+ let generation = 0;
66
+ // Cache Storage writes still landing, by id. A delete waits for the write to
67
+ // its id first: a write that lands after the delete puts back exactly the
68
+ // copy the delete was removing.
69
+ const writing = new Map<string, Promise<void>>();
70
+ // The latest drop of Cache Storage. Reads and writes wait for it, so nothing
71
+ // is read from a cache being dropped and no write lands in it before the drop.
72
+ // A drop waits only for writes registered before it, and a write only for
73
+ // drops begun before it, so neither waits on the other in a cycle.
74
+ let ownership: Promise<void> = Promise.resolve();
75
+
76
+ function rememberInMemory(id: string, bytes: Uint8Array): void {
77
+ const held = memory.get(id);
78
+ if (held !== undefined) {
79
+ memory.delete(id);
80
+ memoryBytes -= held.byteLength;
81
+ }
82
+ if (bytes.byteLength > MEMORY_BUDGET_BYTES) return;
83
+ memory.set(id, bytes);
84
+ memoryBytes += bytes.byteLength;
85
+ for (const [oldest, old] of memory) {
86
+ if (memoryBytes <= MEMORY_BUDGET_BYTES) break;
87
+ memory.delete(oldest);
88
+ memoryBytes -= old.byteLength;
89
+ }
90
+ }
91
+
92
+ function entryUrl(id: string): string {
93
+ return ENTRY_BASE + encodeURIComponent(id);
94
+ }
95
+
96
+ async function readPersistent(id: string): Promise<Uint8Array | null> {
97
+ await ownership;
98
+ const storage = cacheStorage();
99
+ if (storage === null) return null;
100
+ try {
101
+ const cache = await storage.open(CACHE_NAME);
102
+ const hit = await cache.match(entryUrl(id), { ignoreSearch: true });
103
+ if (hit === undefined) return null;
104
+ return new Uint8Array(await hit.arrayBuffer());
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ async function writePersistent(id: string, bytes: Uint8Array, startedIn: number): Promise<void> {
111
+ await ownership;
112
+ const storage = cacheStorage();
113
+ if (storage === null || bytes.byteLength > PERSISTENT_BUDGET_BYTES) return;
114
+ try {
115
+ const cache = await storage.open(CACHE_NAME);
116
+ if (generation !== startedIn) return;
117
+ // The size and the time ride in the entry's query, which a lookup ignores,
118
+ // so the budget is kept from the key listing without reading any body.
119
+ await cache.put(
120
+ `${entryUrl(id)}?b=${bytes.byteLength}&t=${Date.now()}`,
121
+ new Response(bytes as unknown as BodyInit, {
122
+ headers: { 'content-type': 'application/octet-stream' },
123
+ }),
124
+ );
125
+ await trimPersistent(cache);
126
+ } catch {
127
+ // Quota, a private window, a closed storage: the bytes are still in hand.
128
+ }
129
+ }
130
+
131
+ async function trimPersistent(cache: CacheLike): Promise<void> {
132
+ const entries = (await cache.keys()).map((request) => {
133
+ const url = new URL(request.url);
134
+ return {
135
+ url: request.url,
136
+ bytes: Number(url.searchParams.get('b')) || 0,
137
+ at: Number(url.searchParams.get('t')) || 0,
138
+ };
139
+ });
140
+ let total = entries.reduce((sum, entry) => sum + entry.bytes, 0);
141
+ entries.sort((a, b) => a.at - b.at);
142
+ for (const entry of entries) {
143
+ if (total <= PERSISTENT_BUDGET_BYTES) break;
144
+ await cache.delete(entry.url);
145
+ total -= entry.bytes;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * The bytes a join names, from a kept copy when one is held, else from `url`.
151
+ *
152
+ * With no `contentId` this is `fetchBody(url)`: content nothing names cannot
153
+ * be recognised the next time. Concurrent asks for one id share a download.
154
+ */
155
+ export function fetchSealedBytes(
156
+ contentId: string | undefined,
157
+ url: string,
158
+ fetchBody: SealedFetch,
159
+ ): Promise<Uint8Array> {
160
+ if (contentId === undefined || contentId === '') return fetchBody(url);
161
+ const held = memory.get(contentId);
162
+ if (held !== undefined) {
163
+ rememberInMemory(contentId, held);
164
+ return Promise.resolve(held);
165
+ }
166
+ const pending = inFlight.get(contentId);
167
+ if (pending !== undefined) return pending;
168
+ const startedIn = generation;
169
+ const load = (async () => {
170
+ const kept = await readPersistent(contentId);
171
+ if (kept !== null) {
172
+ if (generation === startedIn) rememberInMemory(contentId, kept);
173
+ return kept;
174
+ }
175
+ const bytes = await fetchBody(url);
176
+ if (generation === startedIn) {
177
+ rememberInMemory(contentId, bytes);
178
+ const write = writePersistent(contentId, bytes, startedIn);
179
+ writing.set(contentId, write);
180
+ void write.then(() => {
181
+ if (writing.get(contentId) === write) writing.delete(contentId);
182
+ });
183
+ }
184
+ return bytes;
185
+ })();
186
+ inFlight.set(contentId, load);
187
+ void load.then(
188
+ () => inFlight.delete(contentId),
189
+ () => inFlight.delete(contentId),
190
+ );
191
+ return load;
192
+ }
193
+
194
+ /** Drop one id from both tiers, for a kept copy that would not decode. */
195
+ export async function forgetSealedBytes(contentId: string): Promise<void> {
196
+ const held = memory.get(contentId);
197
+ if (held !== undefined) {
198
+ memory.delete(contentId);
199
+ memoryBytes -= held.byteLength;
200
+ }
201
+ await writing.get(contentId);
202
+ const storage = cacheStorage();
203
+ if (storage === null) return;
204
+ try {
205
+ const cache = await storage.open(CACHE_NAME);
206
+ await cache.delete(entryUrl(contentId), { ignoreSearch: true });
207
+ } catch {
208
+ // Nothing kept, or nothing to reach.
209
+ }
210
+ }
211
+
212
+ /** Drop every kept copy, whoever the recorded owner is: the drop
213
+ * `adoptSealedCacheOwner` makes on a change of owner, made unconditionally. */
214
+ export function clearSealedCache(): void {
215
+ void dropEverything();
216
+ }
217
+
218
+ function dropEverything(): Promise<void> {
219
+ generation += 1;
220
+ memory.clear();
221
+ memoryBytes = 0;
222
+ inFlight.clear();
223
+ const storage = cacheStorage();
224
+ if (storage === null) return ownership;
225
+ const landing = [...writing.values()];
226
+ const drop = Promise.allSettled([ownership, ...landing])
227
+ .then(() => storage.delete(CACHE_NAME))
228
+ .then(
229
+ () => undefined,
230
+ () => undefined,
231
+ );
232
+ ownership = drop;
233
+ return drop;
234
+ }
235
+
236
+ function ownerStorage(): Storage | null {
237
+ try {
238
+ return typeof localStorage === 'undefined' ? null : localStorage;
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Record whose bytes the cache keeps: the signed-in person's `sub`, or `null`
246
+ * signed out. A cache recorded for anyone else, or for no one, is dropped
247
+ * before anything reads it.
248
+ */
249
+ export function adoptSealedCacheOwner(owner: string | null): void {
250
+ const mark = owner === null ? 'signed-out' : `sub:${owner}`;
251
+ const storage = ownerStorage();
252
+ let held: string | null = null;
253
+ try {
254
+ held = storage?.getItem(OWNER_KEY) ?? null;
255
+ } catch {
256
+ held = null;
257
+ }
258
+ if (held === mark) return;
259
+ // Unrecorded counts as someone else: bytes kept before any owner was
260
+ // recorded cannot say whose they are.
261
+ void dropEverything();
262
+ try {
263
+ storage?.setItem(OWNER_KEY, mark);
264
+ } catch {
265
+ // Unrecorded, so the next page drops the cache again rather than trust it.
266
+ }
267
+ }