@freqhole/reliquary 0.0.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 (226) hide show
  1. package/dist/automerge/acl-filtering.d.ts +87 -0
  2. package/dist/automerge/acl-filtering.d.ts.map +1 -0
  3. package/dist/automerge/acl-filtering.js +138 -0
  4. package/dist/automerge/acl-filtering.test.d.ts +2 -0
  5. package/dist/automerge/acl-filtering.test.d.ts.map +1 -0
  6. package/dist/automerge/acl-filtering.test.js +302 -0
  7. package/dist/automerge/index.d.ts +5 -0
  8. package/dist/automerge/index.d.ts.map +1 -0
  9. package/dist/automerge/index.js +7 -0
  10. package/dist/automerge/iroh-network-adapter.d.ts +178 -0
  11. package/dist/automerge/iroh-network-adapter.d.ts.map +1 -0
  12. package/dist/automerge/iroh-network-adapter.js +691 -0
  13. package/dist/automerge/iroh-network-adapter.test.d.ts +2 -0
  14. package/dist/automerge/iroh-network-adapter.test.d.ts.map +1 -0
  15. package/dist/automerge/iroh-network-adapter.test.js +818 -0
  16. package/dist/automerge/types.d.ts +66 -0
  17. package/dist/automerge/types.d.ts.map +1 -0
  18. package/dist/automerge/types.js +10 -0
  19. package/dist/blobs/bytes-backend.d.ts +41 -0
  20. package/dist/blobs/bytes-backend.d.ts.map +1 -0
  21. package/dist/blobs/bytes-backend.js +252 -0
  22. package/dist/blobs/bytes-backend.test.d.ts +2 -0
  23. package/dist/blobs/bytes-backend.test.d.ts.map +1 -0
  24. package/dist/blobs/bytes-backend.test.js +241 -0
  25. package/dist/blobs/db.d.ts +8 -0
  26. package/dist/blobs/db.d.ts.map +1 -0
  27. package/dist/blobs/db.js +141 -0
  28. package/dist/blobs/db.test.d.ts +2 -0
  29. package/dist/blobs/db.test.d.ts.map +1 -0
  30. package/dist/blobs/db.test.js +120 -0
  31. package/dist/blobs/index.d.ts +5 -0
  32. package/dist/blobs/index.d.ts.map +1 -0
  33. package/dist/blobs/index.js +2 -0
  34. package/dist/blobs/store.d.ts +47 -0
  35. package/dist/blobs/store.d.ts.map +1 -0
  36. package/dist/blobs/store.js +263 -0
  37. package/dist/blobs/store.test.d.ts +2 -0
  38. package/dist/blobs/store.test.d.ts.map +1 -0
  39. package/dist/blobs/store.test.js +377 -0
  40. package/dist/blobs/types.d.ts +55 -0
  41. package/dist/blobs/types.d.ts.map +1 -0
  42. package/dist/blobs/types.js +8 -0
  43. package/dist/ensure/client.d.ts +19 -0
  44. package/dist/ensure/client.d.ts.map +1 -0
  45. package/dist/ensure/client.js +74 -0
  46. package/dist/ensure/client.test.d.ts +2 -0
  47. package/dist/ensure/client.test.d.ts.map +1 -0
  48. package/dist/ensure/client.test.js +133 -0
  49. package/dist/ensure/index.d.ts +4 -0
  50. package/dist/ensure/index.d.ts.map +1 -0
  51. package/dist/ensure/index.js +4 -0
  52. package/dist/ensure/responder.d.ts +20 -0
  53. package/dist/ensure/responder.d.ts.map +1 -0
  54. package/dist/ensure/responder.js +100 -0
  55. package/dist/ensure/responder.test.d.ts +2 -0
  56. package/dist/ensure/responder.test.d.ts.map +1 -0
  57. package/dist/ensure/responder.test.js +191 -0
  58. package/dist/ensure/types.d.ts +19 -0
  59. package/dist/ensure/types.d.ts.map +1 -0
  60. package/dist/ensure/types.js +4 -0
  61. package/dist/ensure/types.test.d.ts +2 -0
  62. package/dist/ensure/types.test.d.ts.map +1 -0
  63. package/dist/ensure/types.test.js +81 -0
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +3 -0
  67. package/dist/index.test.d.ts +2 -0
  68. package/dist/index.test.d.ts.map +1 -0
  69. package/dist/index.test.js +7 -0
  70. package/dist/solid/blob-url.d.ts +10 -0
  71. package/dist/solid/blob-url.d.ts.map +1 -0
  72. package/dist/solid/blob-url.js +34 -0
  73. package/dist/solid/blob-url.test.d.ts +2 -0
  74. package/dist/solid/blob-url.test.d.ts.map +1 -0
  75. package/dist/solid/blob-url.test.js +62 -0
  76. package/dist/solid/connection-summary.d.ts +25 -0
  77. package/dist/solid/connection-summary.d.ts.map +1 -0
  78. package/dist/solid/connection-summary.js +29 -0
  79. package/dist/solid/connection-summary.test.d.ts +2 -0
  80. package/dist/solid/connection-summary.test.d.ts.map +1 -0
  81. package/dist/solid/connection-summary.test.js +57 -0
  82. package/dist/solid/doc-store.d.ts +29 -0
  83. package/dist/solid/doc-store.d.ts.map +1 -0
  84. package/dist/solid/doc-store.js +79 -0
  85. package/dist/solid/doc-store.test.d.ts +2 -0
  86. package/dist/solid/doc-store.test.d.ts.map +1 -0
  87. package/dist/solid/doc-store.test.js +234 -0
  88. package/dist/solid/index.d.ts +4 -0
  89. package/dist/solid/index.d.ts.map +1 -0
  90. package/dist/solid/index.js +9 -0
  91. package/dist/solid/transfer-progress.d.ts +21 -0
  92. package/dist/solid/transfer-progress.d.ts.map +1 -0
  93. package/dist/solid/transfer-progress.js +32 -0
  94. package/dist/solid/transfer-progress.test.d.ts +2 -0
  95. package/dist/solid/transfer-progress.test.d.ts.map +1 -0
  96. package/dist/solid/transfer-progress.test.js +76 -0
  97. package/dist/testing/blob-fixtures.d.ts +19 -0
  98. package/dist/testing/blob-fixtures.d.ts.map +1 -0
  99. package/dist/testing/blob-fixtures.js +46 -0
  100. package/dist/testing/blob-fixtures.test.d.ts +2 -0
  101. package/dist/testing/blob-fixtures.test.d.ts.map +1 -0
  102. package/dist/testing/blob-fixtures.test.js +46 -0
  103. package/dist/testing/idb-harness.d.ts +10 -0
  104. package/dist/testing/idb-harness.d.ts.map +1 -0
  105. package/dist/testing/idb-harness.js +17 -0
  106. package/dist/testing/idb-harness.test.d.ts +2 -0
  107. package/dist/testing/idb-harness.test.d.ts.map +1 -0
  108. package/dist/testing/idb-harness.test.js +48 -0
  109. package/dist/testing/index.d.ts +7 -0
  110. package/dist/testing/index.d.ts.map +1 -0
  111. package/dist/testing/index.js +11 -0
  112. package/dist/testing/media.d.ts +15 -0
  113. package/dist/testing/media.d.ts.map +1 -0
  114. package/dist/testing/media.js +37 -0
  115. package/dist/testing/media.test.d.ts +2 -0
  116. package/dist/testing/media.test.d.ts.map +1 -0
  117. package/dist/testing/media.test.js +47 -0
  118. package/dist/testing/mock-blob-fetch.d.ts +33 -0
  119. package/dist/testing/mock-blob-fetch.d.ts.map +1 -0
  120. package/dist/testing/mock-blob-fetch.js +39 -0
  121. package/dist/testing/mock-blob-fetch.test.d.ts +2 -0
  122. package/dist/testing/mock-blob-fetch.test.d.ts.map +1 -0
  123. package/dist/testing/mock-blob-fetch.test.js +65 -0
  124. package/dist/testing/mock-node.d.ts +36 -0
  125. package/dist/testing/mock-node.d.ts.map +1 -0
  126. package/dist/testing/mock-node.js +97 -0
  127. package/dist/testing/mock-node.test.d.ts +2 -0
  128. package/dist/testing/mock-node.test.d.ts.map +1 -0
  129. package/dist/testing/mock-node.test.js +66 -0
  130. package/dist/testing/streams.d.ts +68 -0
  131. package/dist/testing/streams.d.ts.map +1 -0
  132. package/dist/testing/streams.js +99 -0
  133. package/dist/testing/streams.test.d.ts +2 -0
  134. package/dist/testing/streams.test.d.ts.map +1 -0
  135. package/dist/testing/streams.test.js +82 -0
  136. package/dist/transfer/cancellation.d.ts +8 -0
  137. package/dist/transfer/cancellation.d.ts.map +1 -0
  138. package/dist/transfer/cancellation.js +19 -0
  139. package/dist/transfer/index.d.ts +8 -0
  140. package/dist/transfer/index.d.ts.map +1 -0
  141. package/dist/transfer/index.js +4 -0
  142. package/dist/transfer/prefetch.d.ts +38 -0
  143. package/dist/transfer/prefetch.d.ts.map +1 -0
  144. package/dist/transfer/prefetch.js +61 -0
  145. package/dist/transfer/prefetch.test.d.ts +2 -0
  146. package/dist/transfer/prefetch.test.d.ts.map +1 -0
  147. package/dist/transfer/prefetch.test.js +136 -0
  148. package/dist/transfer/serve.d.ts +66 -0
  149. package/dist/transfer/serve.d.ts.map +1 -0
  150. package/dist/transfer/serve.js +126 -0
  151. package/dist/transfer/serve.test.d.ts +2 -0
  152. package/dist/transfer/serve.test.d.ts.map +1 -0
  153. package/dist/transfer/serve.test.js +101 -0
  154. package/dist/transfer/snatch.d.ts +39 -0
  155. package/dist/transfer/snatch.d.ts.map +1 -0
  156. package/dist/transfer/snatch.js +289 -0
  157. package/dist/transfer/snatch.test.d.ts +2 -0
  158. package/dist/transfer/snatch.test.d.ts.map +1 -0
  159. package/dist/transfer/snatch.test.js +337 -0
  160. package/dist/transfer/types.d.ts +85 -0
  161. package/dist/transfer/types.d.ts.map +1 -0
  162. package/dist/transfer/types.js +15 -0
  163. package/dist/utils/hash.d.ts +13 -0
  164. package/dist/utils/hash.d.ts.map +1 -0
  165. package/dist/utils/hash.js +31 -0
  166. package/dist/utils/hash.test.d.ts +2 -0
  167. package/dist/utils/hash.test.d.ts.map +1 -0
  168. package/dist/utils/hash.test.js +56 -0
  169. package/dist/utils/image-utils.d.ts +28 -0
  170. package/dist/utils/image-utils.d.ts.map +1 -0
  171. package/dist/utils/image-utils.js +113 -0
  172. package/dist/utils/image-utils.test.d.ts +2 -0
  173. package/dist/utils/image-utils.test.d.ts.map +1 -0
  174. package/dist/utils/image-utils.test.js +181 -0
  175. package/dist/utils/index.d.ts +4 -0
  176. package/dist/utils/index.d.ts.map +1 -0
  177. package/dist/utils/index.js +3 -0
  178. package/dist/utils/log.d.ts +22 -0
  179. package/dist/utils/log.d.ts.map +1 -0
  180. package/dist/utils/log.js +100 -0
  181. package/dist/utils/log.test.d.ts +2 -0
  182. package/dist/utils/log.test.d.ts.map +1 -0
  183. package/dist/utils/log.test.js +113 -0
  184. package/dist/worker/blob-worker-client.d.ts +118 -0
  185. package/dist/worker/blob-worker-client.d.ts.map +1 -0
  186. package/dist/worker/blob-worker-client.js +361 -0
  187. package/dist/worker/blob-worker-client.test.d.ts +2 -0
  188. package/dist/worker/blob-worker-client.test.d.ts.map +1 -0
  189. package/dist/worker/blob-worker-client.test.js +137 -0
  190. package/dist/worker/blob-worker-logic.d.ts +120 -0
  191. package/dist/worker/blob-worker-logic.d.ts.map +1 -0
  192. package/dist/worker/blob-worker-logic.js +368 -0
  193. package/dist/worker/blob-worker-logic.test.d.ts +2 -0
  194. package/dist/worker/blob-worker-logic.test.d.ts.map +1 -0
  195. package/dist/worker/blob-worker-logic.test.js +191 -0
  196. package/dist/worker/blob-worker.d.ts +21 -0
  197. package/dist/worker/blob-worker.d.ts.map +1 -0
  198. package/dist/worker/blob-worker.js +44 -0
  199. package/dist/worker/index.d.ts +12 -0
  200. package/dist/worker/index.d.ts.map +1 -0
  201. package/dist/worker/index.js +6 -0
  202. package/dist/worker/midden-blake3.d.ts +23 -0
  203. package/dist/worker/midden-blake3.d.ts.map +1 -0
  204. package/dist/worker/midden-blake3.js +47 -0
  205. package/dist/worker/midden-blake3.test.d.ts +2 -0
  206. package/dist/worker/midden-blake3.test.d.ts.map +1 -0
  207. package/dist/worker/midden-blake3.test.js +25 -0
  208. package/dist/worker/midden-not-bundled.stub.d.ts +1 -0
  209. package/dist/worker/midden-not-bundled.stub.d.ts.map +1 -0
  210. package/dist/worker/midden-not-bundled.stub.js +10 -0
  211. package/dist/worker/midden-stub.d.ts +10 -0
  212. package/dist/worker/midden-stub.d.ts.map +1 -0
  213. package/dist/worker/midden-stub.js +31 -0
  214. package/dist/worker/midden-stub.test.d.ts +2 -0
  215. package/dist/worker/midden-stub.test.d.ts.map +1 -0
  216. package/dist/worker/midden-stub.test.js +25 -0
  217. package/dist/worker/midden-worker-client.d.ts +103 -0
  218. package/dist/worker/midden-worker-client.d.ts.map +1 -0
  219. package/dist/worker/midden-worker-client.js +246 -0
  220. package/dist/worker/midden-worker-client.test.d.ts +2 -0
  221. package/dist/worker/midden-worker-client.test.d.ts.map +1 -0
  222. package/dist/worker/midden-worker-client.test.js +151 -0
  223. package/dist/worker/midden-worker-contract.d.ts +87 -0
  224. package/dist/worker/midden-worker-contract.d.ts.map +1 -0
  225. package/dist/worker/midden-worker-contract.js +30 -0
  226. package/package.json +90 -0
@@ -0,0 +1,361 @@
1
+ // blob-worker-client - lazy comlink wrapper around the blob worker.
2
+ //
3
+ // the worker is spun up on first call to `getBlobWorker()` and reused for
4
+ // the lifetime of the page. importing this module costs nothing - the
5
+ // worker bundle is only fetched/instantiated on first use.
6
+ //
7
+ // in environments without Worker support (e.g. SSR / certain test runners)
8
+ // the client falls back to a synchronous in-process implementation so
9
+ // callers don't have to branch.
10
+ import * as Comlink from "comlink";
11
+ import { sha256Hex } from "../utils/hash.js";
12
+ import { loadMiddenBlake3 } from "./midden-blake3.js";
13
+ import { BLOB_WORKER_READY_MESSAGE } from "./blob-worker-logic.js";
14
+ import { log } from "../utils/log.js";
15
+ const TAG = "blob.worker";
16
+ let workerProxy = null;
17
+ let workerInstance = null;
18
+ let workerReadyPromise = null;
19
+ // generous - the worker's top-level module may load a wasm-backed midden
20
+ // module (when the embedding app bundles one) before it can register its
21
+ // Comlink message listener. slow CI runners / cold caches can take a few
22
+ // seconds for this.
23
+ const WORKER_READY_TIMEOUT_MS = 20_000;
24
+ function canSpawnWorker() {
25
+ return typeof Worker !== "undefined";
26
+ }
27
+ /**
28
+ * get (and lazily spawn) the comlink-wrapped blob worker proxy.
29
+ *
30
+ * returns null if Worker isn't available, or if the worker doesn't signal
31
+ * readiness within `WORKER_READY_TIMEOUT_MS` - callers should branch and use
32
+ * a main-thread fallback (every exported helper below already does this).
33
+ *
34
+ * waits for an explicit "ready" postMessage from the worker before handing
35
+ * out the Comlink proxy. this avoids a real race: the worker's module top
36
+ * level may await a wasm module's instantiation before calling
37
+ * `Comlink.expose()` (which registers the "message" listener). an RPC call
38
+ * sent before that listener exists fires with no listener attached and is
39
+ * silently dropped forever - the caller's promise then never resolves.
40
+ * waiting for the ready signal (sent immediately after `Comlink.expose()`
41
+ * in blob-worker.ts) guarantees the listener is registered before any real
42
+ * RPC call goes out.
43
+ */
44
+ export async function getBlobWorker() {
45
+ if (workerProxy)
46
+ return workerProxy;
47
+ if (!canSpawnWorker())
48
+ return null;
49
+ if (workerReadyPromise)
50
+ return workerReadyPromise;
51
+ workerReadyPromise = (async () => {
52
+ // standard web-platform worker construction: resolves relative to this
53
+ // module's own compiled location, so it works under vite, webpack 5,
54
+ // and plain browsers without a bundler-specific import suffix.
55
+ const worker = new Worker(new URL("./blob-worker.js", import.meta.url), { type: "module" });
56
+ const ready = await new Promise((resolve) => {
57
+ const timeout = setTimeout(() => {
58
+ worker.removeEventListener("message", onMessage);
59
+ worker.removeEventListener("error", onError);
60
+ log.warn(TAG, `worker did not signal ready within ${WORKER_READY_TIMEOUT_MS}ms`);
61
+ resolve(false);
62
+ }, WORKER_READY_TIMEOUT_MS);
63
+ const onMessage = (e) => {
64
+ if (e.data !== BLOB_WORKER_READY_MESSAGE)
65
+ return;
66
+ clearTimeout(timeout);
67
+ worker.removeEventListener("error", onError);
68
+ worker.removeEventListener("message", onMessage);
69
+ resolve(true);
70
+ };
71
+ // a worker script that fails to load (network error, blocked by a dev
72
+ // server's file-serving restrictions, syntax error, etc.) fires an
73
+ // `error` event, never a `message` - without this listener the promise
74
+ // above would otherwise sit unresolved for the full ready-timeout with
75
+ // no indication anything went wrong.
76
+ const onError = (e) => {
77
+ clearTimeout(timeout);
78
+ worker.removeEventListener("message", onMessage);
79
+ worker.removeEventListener("error", onError);
80
+ log.warn(TAG, "worker failed to load:", e.message || e);
81
+ resolve(false);
82
+ };
83
+ worker.addEventListener("message", onMessage);
84
+ worker.addEventListener("error", onError);
85
+ });
86
+ if (!ready) {
87
+ worker.terminate();
88
+ // allow a future call to retry (e.g. a one-off slow load) instead of
89
+ // permanently caching this failure.
90
+ workerReadyPromise = null;
91
+ return null;
92
+ }
93
+ workerInstance = worker;
94
+ workerProxy = Comlink.wrap(worker);
95
+ return workerProxy;
96
+ })();
97
+ return workerReadyPromise;
98
+ }
99
+ /**
100
+ * tear down the worker. mainly useful for tests; production code can leave
101
+ * the worker alive for the page lifetime.
102
+ */
103
+ export function shutdownBlobWorker() {
104
+ if (workerInstance) {
105
+ workerInstance.terminate();
106
+ workerInstance = null;
107
+ workerProxy = null;
108
+ }
109
+ workerReadyPromise = null;
110
+ }
111
+ // ---- main-thread fallbacks -----------------------------------------------
112
+ // used when Worker isn't available. these mirror the worker's API exactly
113
+ // so consumers can share a single code path.
114
+ async function fallbackHashBlake3(data) {
115
+ try {
116
+ const midden = await loadMiddenBlake3();
117
+ if (!midden || typeof midden.hash_blake3 !== "function") {
118
+ log.warn(TAG, "no midden module with hash_blake3 available, blake3 hashing degraded to empty string");
119
+ return "";
120
+ }
121
+ return midden.hash_blake3(data);
122
+ }
123
+ catch (err) {
124
+ log.warn(TAG, "blake3 hashing threw, degrading to empty string:", err);
125
+ return "";
126
+ }
127
+ }
128
+ async function fallbackBase64Encode(buffer) {
129
+ const bytes = new Uint8Array(buffer);
130
+ const chunkSize = 0x8000;
131
+ let binary = "";
132
+ for (let i = 0; i < bytes.length; i += chunkSize) {
133
+ const chunk = bytes.subarray(i, i + chunkSize);
134
+ binary += String.fromCharCode.apply(null, chunk);
135
+ }
136
+ return btoa(binary);
137
+ }
138
+ // ---- convenience helpers --------------------------------------------------
139
+ /**
140
+ * hash bytes with blake3, preferring the worker. accepts a Uint8Array.
141
+ * note: this DOES copy across the worker boundary (Uint8Array isn't
142
+ * transferable directly without giving up the underlying ArrayBuffer);
143
+ * for upload pipelines, prefer `processBlobBytes` which transfers the
144
+ * underlying buffer.
145
+ */
146
+ export async function hashBlake3(data) {
147
+ const worker = await getBlobWorker();
148
+ if (worker)
149
+ return worker.hashBlake3(data);
150
+ return fallbackHashBlake3(data);
151
+ }
152
+ /**
153
+ * sha256 hash of an ArrayBuffer.
154
+ */
155
+ export async function hashSha256(data) {
156
+ const worker = await getBlobWorker();
157
+ if (worker)
158
+ return worker.hashSha256(data);
159
+ return sha256Hex(data);
160
+ }
161
+ /**
162
+ * base64-encode an ArrayBuffer.
163
+ *
164
+ * NOTE: the buffer is structured-cloned (copied) across the worker
165
+ * boundary so callers can safely reuse it afterwards. if you have a
166
+ * dedicated buffer that won't be touched again, you can transfer
167
+ * ownership manually for a small perf win.
168
+ */
169
+ export async function base64Encode(buffer) {
170
+ const worker = await getBlobWorker();
171
+ if (worker)
172
+ return worker.base64Encode(buffer);
173
+ return fallbackBase64Encode(buffer);
174
+ }
175
+ /**
176
+ * one-shot upload pipeline: hash + write to OPFS in the worker, return
177
+ * metadata. transfers the buffer.
178
+ */
179
+ export async function processBlobBytes(buffer, filename, mime) {
180
+ const worker = await getBlobWorker();
181
+ if (worker) {
182
+ return worker.processBlobBytes(Comlink.transfer(buffer, [buffer]), filename, mime);
183
+ }
184
+ // main-thread fallback path - rare, mostly for tests.
185
+ const [sha256, blake3] = await Promise.all([sha256Hex(buffer), fallbackHashBlake3(new Uint8Array(buffer))]);
186
+ return {
187
+ blob_id: blake3,
188
+ sha256,
189
+ blake3,
190
+ size: buffer.byteLength,
191
+ mime,
192
+ filename,
193
+ };
194
+ }
195
+ /**
196
+ * write a blob to OPFS via the worker (uses `FileSystemSyncAccessHandle`
197
+ * for max throughput).
198
+ *
199
+ * NOTE: the buffer is structured-cloned (copied) across the worker
200
+ * boundary so callers can safely reuse it afterwards - important for
201
+ * code paths that read or return the buffer after kicking off an OPFS
202
+ * write (e.g. a snatch/download cache-back-fill). for the upload pipeline
203
+ * use `processBlobBytes` instead, which transfers.
204
+ */
205
+ export async function writeBlobToOpfs(blobId, buffer) {
206
+ const worker = await getBlobWorker();
207
+ if (worker) {
208
+ await worker.writeBlobToOpfs(blobId, buffer);
209
+ return;
210
+ }
211
+ // no main-thread fallback - opfs writes from main thread don't have a
212
+ // sync access handle path anyway. silently no-op.
213
+ }
214
+ /**
215
+ * stream a File into OPFS via the worker's chunked upload session:
216
+ * incremental blake3 + incremental sync-access-handle writes, so neither
217
+ * thread ever holds the whole payload. each chunk buffer is transferred
218
+ * (zero-copy) across the worker boundary.
219
+ *
220
+ * `onProgress` reports bytes pushed / file size (0..1) per chunk.
221
+ * `signal` cancels between chunks: the worker session is aborted (temp
222
+ * file cleaned up) and a DOMException AbortError is thrown.
223
+ *
224
+ * returns { blake3, size } - the file lands in OPFS under its blake3
225
+ * content address. throws when streaming isn't available (no worker, no
226
+ * OPFS sync handles, no midden module bundled); callers should fall back
227
+ * to the one-shot processBlobBytes path.
228
+ */
229
+ export async function streamFileToOpfs(file, options) {
230
+ const worker = await getBlobWorker();
231
+ if (!worker)
232
+ throw new Error("streaming upload unavailable: no blob worker");
233
+ const sessionId = await worker.uploadBegin();
234
+ const reader = file.stream().getReader();
235
+ let bytesPushed = 0;
236
+ try {
237
+ for (;;) {
238
+ if (options?.signal?.aborted) {
239
+ throw new DOMException("upload cancelled", "AbortError");
240
+ }
241
+ const { done, value } = await reader.read();
242
+ if (done)
243
+ break;
244
+ // chunks are usually freshly-allocated exact buffers, but a view into
245
+ // a larger buffer is legal - slice to exact bytes before transferring
246
+ const exact = value.byteOffset === 0 && value.byteLength === value.buffer.byteLength
247
+ ? value.buffer
248
+ : value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
249
+ const chunkLen = value.byteLength;
250
+ await worker.uploadPush(sessionId, Comlink.transfer(exact, [exact]));
251
+ bytesPushed += chunkLen;
252
+ if (options?.onProgress && file.size > 0) {
253
+ options.onProgress(Math.min(1, bytesPushed / file.size));
254
+ }
255
+ }
256
+ return await worker.uploadFinish(sessionId);
257
+ }
258
+ catch (err) {
259
+ await worker.uploadAbort(sessionId).catch(() => { });
260
+ throw err;
261
+ }
262
+ finally {
263
+ reader.releaseLock();
264
+ }
265
+ }
266
+ /**
267
+ * resize an image Blob to a (default WebP) data URL.
268
+ * prefers the blob worker (keeps the decode/encode work off the main
269
+ * thread); falls back to a main-thread `OffscreenCanvas` path if the
270
+ * worker isn't available. returns null on failure (non-image input,
271
+ * decode failure, etc.).
272
+ */
273
+ export async function resizeImageToWebpDataUrl(blob, options) {
274
+ const worker = await getBlobWorker();
275
+ if (worker)
276
+ return worker.resizeImageToWebpDataUrl(blob, options);
277
+ return mainThreadResizeImage(blob, options);
278
+ }
279
+ /**
280
+ * generate a thumbnail data URL (default 200x200 WebP @ q=0.75) for an
281
+ * image Blob. delegates to the worker.
282
+ */
283
+ export async function generateThumbnailDataUrl(blob, maxSize = 200) {
284
+ if (!blob.type.startsWith("image/"))
285
+ return null;
286
+ return resizeImageToWebpDataUrl(blob, {
287
+ maxWidth: maxSize,
288
+ maxHeight: maxSize,
289
+ quality: 0.75,
290
+ });
291
+ }
292
+ /**
293
+ * decode a base64 string into a Uint8Array via the worker. for large
294
+ * payloads (megabyte-scale snatch responses) this avoids blocking the
295
+ * main thread on a tight `String.charCodeAt` loop.
296
+ */
297
+ export async function base64Decode(b64) {
298
+ const worker = await getBlobWorker();
299
+ if (worker)
300
+ return worker.base64Decode(b64);
301
+ // main-thread fallback
302
+ const binary = atob(b64);
303
+ const out = new Uint8Array(binary.length);
304
+ for (let i = 0; i < binary.length; i++)
305
+ out[i] = binary.charCodeAt(i);
306
+ return out;
307
+ }
308
+ // ---- main-thread fallbacks -----------------------------------------------
309
+ async function mainThreadResizeImage(blob, options) {
310
+ if (typeof OffscreenCanvas === "undefined" || typeof createImageBitmap !== "function") {
311
+ return null;
312
+ }
313
+ const maxWidth = options?.maxWidth ?? 200;
314
+ const maxHeight = options?.maxHeight ?? 200;
315
+ const quality = options?.quality ?? 0.8;
316
+ const cropSquare = options?.cropSquare ?? false;
317
+ const mime = options?.mime ?? "image/webp";
318
+ let bitmap = null;
319
+ try {
320
+ bitmap = await createImageBitmap(blob);
321
+ let sx = 0;
322
+ let sy = 0;
323
+ let sw = bitmap.width;
324
+ let sh = bitmap.height;
325
+ if (cropSquare) {
326
+ const minDim = Math.min(bitmap.width, bitmap.height);
327
+ sx = (bitmap.width - minDim) / 2;
328
+ sy = (bitmap.height - minDim) / 2;
329
+ sw = minDim;
330
+ sh = minDim;
331
+ }
332
+ const aspect = sw / sh;
333
+ let outW = sw;
334
+ let outH = sh;
335
+ if (outW > maxWidth) {
336
+ outW = maxWidth;
337
+ outH = Math.round(outW / aspect);
338
+ }
339
+ if (outH > maxHeight) {
340
+ outH = maxHeight;
341
+ outW = Math.round(outH * aspect);
342
+ }
343
+ outW = Math.max(1, outW);
344
+ outH = Math.max(1, outH);
345
+ const canvas = new OffscreenCanvas(outW, outH);
346
+ const ctx = canvas.getContext("2d");
347
+ if (!ctx)
348
+ return null;
349
+ ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, outW, outH);
350
+ const out = await canvas.convertToBlob({ type: mime, quality });
351
+ const buf = await out.arrayBuffer();
352
+ const b64 = await fallbackBase64Encode(buf);
353
+ return `data:${mime};base64,${b64}`;
354
+ }
355
+ catch {
356
+ return null;
357
+ }
358
+ finally {
359
+ bitmap?.close();
360
+ }
361
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=blob-worker-client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blob-worker-client.test.d.ts","sourceRoot":"","sources":["../../src/worker/blob-worker-client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,137 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { base64Decode, base64Encode, generateThumbnailDataUrl, getBlobWorker, hashBlake3, hashSha256, processBlobBytes, resizeImageToWebpDataUrl, shutdownBlobWorker, streamFileToOpfs, writeBlobToOpfs, } from "./blob-worker-client.js";
4
+ import { resetMiddenBlake3Cache } from "./midden-blake3.js";
5
+ import { log } from "../utils/log.js";
6
+ // this test environment has no `Worker` global, so every helper below
7
+ // exercises its main-thread fallback path - the same path used in
8
+ // environments without Worker support (SSR, certain test runners) -
9
+ // except the `getBlobWorker` tests below that explicitly stub in a fake
10
+ // `Worker` to exercise the real ready-handshake logic instead.
11
+ /** minimal fake `Worker`: enough of the `EventTarget`-based message/error
12
+ * surface for the ready-handshake tests below, without a real thread. */
13
+ class FakeWorker extends EventTarget {
14
+ postMessage = vi.fn();
15
+ terminate = vi.fn();
16
+ }
17
+ class FakeImageBitmap {
18
+ width;
19
+ height;
20
+ constructor(width, height) {
21
+ this.width = width;
22
+ this.height = height;
23
+ }
24
+ close = vi.fn();
25
+ }
26
+ class FakeOffscreenCanvasContext {
27
+ drawImage = vi.fn();
28
+ }
29
+ class FakeOffscreenCanvas {
30
+ width;
31
+ height;
32
+ context = new FakeOffscreenCanvasContext();
33
+ constructor(width, height) {
34
+ this.width = width;
35
+ this.height = height;
36
+ }
37
+ getContext() {
38
+ return this.context;
39
+ }
40
+ async convertToBlob(options) {
41
+ return new Blob(["fake-encoded-bytes"], { type: options?.type ?? "image/webp" });
42
+ }
43
+ }
44
+ function installCanvasFakes(size = { width: 100, height: 100 }) {
45
+ vi.stubGlobal("createImageBitmap", vi.fn(async () => new FakeImageBitmap(size.width, size.height)));
46
+ vi.stubGlobal("OffscreenCanvas", FakeOffscreenCanvas);
47
+ }
48
+ afterEach(() => {
49
+ vi.unstubAllGlobals();
50
+ resetMiddenBlake3Cache();
51
+ shutdownBlobWorker();
52
+ });
53
+ describe("getBlobWorker", () => {
54
+ it("returns null when Worker isn't available in this environment", async () => {
55
+ expect(await getBlobWorker()).toBeNull();
56
+ });
57
+ it("resolves once the worker signals ready via a message, without waiting out the timeout", async () => {
58
+ const worker = new FakeWorker();
59
+ vi.stubGlobal("Worker", vi.fn(() => worker));
60
+ const proxyPromise = getBlobWorker();
61
+ worker.dispatchEvent(new MessageEvent("message", { data: "blob-worker-ready" }));
62
+ expect(await proxyPromise).not.toBeNull();
63
+ });
64
+ it("resolves null quickly (not after the full ready-timeout) when the worker script fails to load, and logs a warning", async () => {
65
+ vi.useFakeTimers();
66
+ const warnSpy = vi.spyOn(log, "warn");
67
+ const worker = new FakeWorker();
68
+ vi.stubGlobal("Worker", vi.fn(() => worker));
69
+ const proxyPromise = getBlobWorker();
70
+ // a worker script that fails to load fires an `error` event, never a
71
+ // `message` - this must resolve right away, not after the 20s
72
+ // ready-timeout (proven by asserting the result WITHOUT advancing
73
+ // fake timers at all).
74
+ worker.dispatchEvent(new Event("error"));
75
+ expect(await proxyPromise).toBeNull();
76
+ expect(warnSpy).toHaveBeenCalled();
77
+ vi.useRealTimers();
78
+ });
79
+ });
80
+ describe("hashBlake3", () => {
81
+ it("falls back to an empty string with no worker and no midden module", async () => {
82
+ expect(await hashBlake3(new Uint8Array([1, 2, 3]))).toBe("");
83
+ });
84
+ });
85
+ describe("hashSha256", () => {
86
+ it("falls back to a real sha-256 digest with no worker", async () => {
87
+ const bytes = new TextEncoder().encode("abc");
88
+ const hash = await hashSha256(bytes.buffer);
89
+ expect(hash).toBe("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
90
+ });
91
+ });
92
+ describe("base64Encode / base64Decode", () => {
93
+ it("round-trips bytes through the fallback path", async () => {
94
+ const original = new Uint8Array([0, 1, 2, 253, 254, 255]);
95
+ const encoded = await base64Encode(original.buffer);
96
+ const decoded = await base64Decode(encoded);
97
+ expect(Array.from(decoded)).toEqual(Array.from(original));
98
+ });
99
+ });
100
+ describe("processBlobBytes", () => {
101
+ it("hashes without a worker (blake3 empty, sha256 real)", async () => {
102
+ const bytes = new TextEncoder().encode("hello world");
103
+ const result = await processBlobBytes(bytes.buffer, "hello.txt", "text/plain");
104
+ expect(result.blake3).toBe("");
105
+ expect(result.blob_id).toBe("");
106
+ expect(result.sha256).toBe("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9");
107
+ expect(result.size).toBe(bytes.byteLength);
108
+ });
109
+ });
110
+ describe("writeBlobToOpfs", () => {
111
+ it("silently no-ops when there's no worker to delegate to", async () => {
112
+ await expect(writeBlobToOpfs("some-hash", new ArrayBuffer(4))).resolves.toBeUndefined();
113
+ });
114
+ });
115
+ describe("streamFileToOpfs", () => {
116
+ it("throws when no worker is available", async () => {
117
+ const file = new File(["hello"], "hello.txt", { type: "text/plain" });
118
+ await expect(streamFileToOpfs(file)).rejects.toThrow(/no blob worker/);
119
+ });
120
+ });
121
+ describe("resizeImageToWebpDataUrl", () => {
122
+ it("uses the main-thread fallback and returns a data url", async () => {
123
+ installCanvasFakes();
124
+ const result = await resizeImageToWebpDataUrl(new Blob(["x"], { type: "image/png" }));
125
+ expect(result).toMatch(/^data:image\/webp;base64,/);
126
+ });
127
+ it("returns null when OffscreenCanvas/createImageBitmap are unavailable", async () => {
128
+ const result = await resizeImageToWebpDataUrl(new Blob(["x"], { type: "image/png" }));
129
+ expect(result).toBeNull();
130
+ });
131
+ });
132
+ describe("generateThumbnailDataUrl", () => {
133
+ it("skips non-image blobs", async () => {
134
+ const result = await generateThumbnailDataUrl(new Blob(["x"], { type: "text/plain" }));
135
+ expect(result).toBeNull();
136
+ });
137
+ });
@@ -0,0 +1,120 @@
1
+ /** message the worker entry posts once comlink has registered its message
2
+ * listener - see `blob-worker.ts` for why the ordering matters. */
3
+ export declare const BLOB_WORKER_READY_MESSAGE = "blob-worker-ready";
4
+ /** directory name for the on-disk (OPFS) blob store. exported so callers
5
+ * reading bytes back directly (bypassing the worker's write path, e.g.
6
+ * `./blobs`'s main-thread OPFS reads) always agree with the worker on
7
+ * where the bytes actually live. */
8
+ export declare const OPFS_DIR = "reliquary-blobs";
9
+ /**
10
+ * compute blake3 hash of a Uint8Array via the embedding app's midden
11
+ * module. returns an empty string when no such module is bundled, or when
12
+ * it does not export `hash_blake3`.
13
+ */
14
+ export declare function hashBlake3(data: Uint8Array): Promise<string>;
15
+ /**
16
+ * compute sha256 hash via SubtleCrypto. SubtleCrypto.digest is already
17
+ * async/non-blocking on the main thread, but this is exposed here too so
18
+ * callers can do sha256 + blake3 in a single round-trip.
19
+ */
20
+ export declare function hashSha256(data: ArrayBuffer): Promise<string>;
21
+ /**
22
+ * base64-encode an ArrayBuffer. uses chunked btoa to avoid stack overflow
23
+ * on large buffers (`String.fromCharCode(...veryLargeArray)` blows up).
24
+ */
25
+ export declare function base64Encode(buffer: ArrayBuffer): Promise<string>;
26
+ /**
27
+ * decode a base64 string into a Uint8Array.
28
+ */
29
+ export declare function base64Decode(b64: string): Promise<Uint8Array>;
30
+ /**
31
+ * write blob bytes to OPFS. prefers `FileSystemSyncAccessHandle` (worker-only,
32
+ * fastest path on chromium/safari/firefox) and falls back to the async
33
+ * writable-stream API. silently no-ops if OPFS is unavailable.
34
+ *
35
+ * `data` should be transferred across postMessage to avoid a copy.
36
+ */
37
+ export declare function writeBlobToOpfs(blobId: string, data: ArrayBuffer): Promise<void>;
38
+ /**
39
+ * read blob bytes from OPFS into a transferable ArrayBuffer. returns null
40
+ * if the file doesn't exist or OPFS is unavailable.
41
+ */
42
+ export declare function readBlobFromOpfs(blobId: string): Promise<ArrayBuffer | null>;
43
+ export interface ProcessedBlob {
44
+ blob_id: string;
45
+ sha256: string;
46
+ blake3: string;
47
+ size: number;
48
+ mime: string;
49
+ filename: string;
50
+ }
51
+ /**
52
+ * one-shot: hash bytes (sha256 + blake3), write to OPFS, return metadata.
53
+ * lets callers avoid three round-trips across the worker boundary for an
54
+ * upload. `data` should be transferred.
55
+ *
56
+ * blake3 is the canonical blob id (matches iroh-blobs / a native rust
57
+ * store); sha256 is still computed so legacy sha256-keyed records and old
58
+ * doc references keep resolving via the sha256 index.
59
+ */
60
+ export declare function processBlobBytes(data: ArrayBuffer, filename: string, mime: string): Promise<ProcessedBlob>;
61
+ export interface ResizeImageOptions {
62
+ /** maximum output width in pixels (default: 200) */
63
+ maxWidth?: number;
64
+ /** maximum output height in pixels (default: 200) */
65
+ maxHeight?: number;
66
+ /** WebP quality 0..1 (default: 0.8) */
67
+ quality?: number;
68
+ /** if true, center-crop to a square before resizing */
69
+ cropSquare?: boolean;
70
+ /** output mime type (default: "image/webp") */
71
+ mime?: string;
72
+ }
73
+ /**
74
+ * resize an image Blob to a WebP data URL via OffscreenCanvas. all heavy
75
+ * work (image decode, resize, WebP encode, base64 encode) happens here.
76
+ * returns null on any failure.
77
+ *
78
+ * the input Blob is structured-cloned across postMessage by reference
79
+ * (the underlying bytes aren't copied), so this is cheap to call.
80
+ */
81
+ export declare function resizeImageToWebpDataUrl(blob: Blob, options?: ResizeImageOptions): Promise<string | null>;
82
+ /**
83
+ * convenience wrapper that fits an image inside `maxSize` x `maxSize`,
84
+ * encoding WebP at 0.75 quality. skips non-image blobs.
85
+ */
86
+ export declare function generateThumbnailDataUrl(blob: Blob, maxSize?: number): Promise<string | null>;
87
+ /**
88
+ * begin a streaming upload session. throws when the environment can't
89
+ * support it (no OPFS sync access handles, or no midden module with a
90
+ * Blake3Hasher bundled) - callers fall back to the one-shot
91
+ * processBlobBytes path.
92
+ */
93
+ export declare function uploadBegin(): Promise<number>;
94
+ /** append the next chunk (transferred buffer) to a session. */
95
+ export declare function uploadPush(id: number, buffer: ArrayBuffer): Promise<void>;
96
+ /**
97
+ * finish a session: flush + close the temp file, then rename it to the
98
+ * final blake3 content address. returns { blake3, size }.
99
+ */
100
+ export declare function uploadFinish(id: number): Promise<{
101
+ blake3: string;
102
+ size: number;
103
+ }>;
104
+ /** abort a session: close and delete the temp file. */
105
+ export declare function uploadAbort(id: number): Promise<void>;
106
+ /**
107
+ * opfs-store selftest - runs the embedding app's midden module's
108
+ * out-of-crate iroh-blobs store round trip against real OPFS. must run in
109
+ * a dedicated worker: sync access handles don't exist on the main thread.
110
+ * throws when no midden module (or no `opfs_store_selftest` export) is
111
+ * bundled.
112
+ */
113
+ export declare function opfsStoreSelftest(): Promise<string>;
114
+ /**
115
+ * persistence selftest - blobs + tags must survive a store shutdown and
116
+ * reopen over the same OPFS directory (the whole point of the store:
117
+ * cross-reload resume without re-import). worker context required.
118
+ */
119
+ export declare function opfsStoreSelftestPersistence(): Promise<string>;
120
+ //# sourceMappingURL=blob-worker-logic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blob-worker-logic.d.ts","sourceRoot":"","sources":["../../src/worker/blob-worker-logic.ts"],"names":[],"mappings":"AAqBA;oEACoE;AACpE,eAAO,MAAM,yBAAyB,sBAAsB,CAAC;AAE7D;;;qCAGqC;AACrC,eAAO,MAAM,QAAQ,oBAAoB,CAAC;AAE1C;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAYlE;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAEnE;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CASvE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAOnE;AAuBD;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCtF;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAUlF;AAID,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,aAAa,CAAC,CAcxB;AAID,MAAM,WAAW,kBAAkB;IACjC,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuDxB;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,SAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOhG;AAoBD;;;;;GAKG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAwBnD;AAED,+DAA+D;AAC/D,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAO/E;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CA0CxF;AAED,uDAAuD;AACvD,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAY3D;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAOzD;AAED;;;;GAIG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,MAAM,CAAC,CAOpE"}