@freqhole/reliquary 0.2.14 → 0.3.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/blobs/db.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA8F7C,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAcjF;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAS1F;AAkBD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAE5F;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAE5F;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAchF;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAchE;AAQD,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcrG;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcxG;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAYrF;AAED,wBAAsB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBrG;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcnE;AAaD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,YAAY,CAAC;IAC1C,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC3B;mDAC+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;CACnB;AAOD,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC,CA8EtG"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/blobs/db.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA6I7C,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAcjF;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAS1F;AA4BD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAE5F;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAE5F;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAchF;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAchE;AAQD,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcrG;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcxG;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAYrF;AAED,wBAAsB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBrG;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcnE;AAaD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,YAAY,CAAC;IAC1C,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC3B;mDAC+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;CACnB;AAOD,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC,CA8EtG"}
package/dist/blobs/db.js CHANGED
@@ -10,6 +10,12 @@
10
10
  const STORE_NAME = "blobs";
11
11
  const REFS_STORE_NAME = "blob_canvas_refs";
12
12
  const DB_VERSION = 3;
13
+ // kept in sync with the createIndex calls in onupgradeneeded below - used
14
+ // by openDb's version-fallback path to detect an already-existing database
15
+ // that's missing an index added by a newer db.ts than whatever build last
16
+ // touched it.
17
+ const BLOB_INDEXES = ["sha256", "blake3", "blob_type", "parent_blob_id", "size", "created_at"];
18
+ const REFS_INDEXES = ["blob_id", "canvas_doc_id"];
13
19
  function openVersioned(dbName, version) {
14
20
  return new Promise((resolve, reject) => {
15
21
  const req = indexedDB.open(dbName, version);
@@ -18,26 +24,26 @@ function openVersioned(dbName, version) {
18
24
  let store;
19
25
  if (!db.objectStoreNames.contains(STORE_NAME)) {
20
26
  store = db.createObjectStore(STORE_NAME, { keyPath: "blob_id" });
21
- store.createIndex("sha256", "sha256", { unique: false });
22
- store.createIndex("blake3", "blake3", { unique: false });
23
- store.createIndex("blob_type", "blob_type", { unique: false });
24
- store.createIndex("parent_blob_id", "parent_blob_id", { unique: false });
25
27
  }
26
28
  else {
27
- // pre-existing store from an earlier DB_VERSION - new indexes can
28
- // still be added via the in-flight version-change transaction
29
+ // pre-existing store from an earlier DB_VERSION - or, for an app
30
+ // that used this same store/db name before adopting reliquary at
31
+ // all, a store that never had ANY of these indexes. new indexes
32
+ // can still be added via the in-flight version-change transaction
29
33
  // (`req.transaction`), they just can't be added outside
30
- // onupgradeneeded. without this branch a version bump would silently
31
- // never add the index for anyone who already has a v1/v2 database.
34
+ // onupgradeneeded.
32
35
  store = req.transaction.objectStore(STORE_NAME);
33
36
  }
34
- // added in v3 - supports sorting the local-files list (filez tab 2)
35
- // by size without a full-store scan.
36
- if (!store.indexNames.contains("size")) {
37
- store.createIndex("size", "size", { unique: false });
38
- }
39
- if (!store.indexNames.contains("created_at")) {
40
- store.createIndex("created_at", "created_at", { unique: false });
37
+ // every index gets the "add if missing" treatment (not just the
38
+ // v3 ones below) - a pre-reliquary store sharing this db/store name
39
+ // would otherwise hit the `else` branch above with none of these
40
+ // indexes present, and every `.index(...)` lookup would throw
41
+ // "the specified index was not found" instead of falling through
42
+ // the resolver chain the way a missing record does.
43
+ for (const name of BLOB_INDEXES) {
44
+ if (!store.indexNames.contains(name)) {
45
+ store.createIndex(name, name, { unique: false });
46
+ }
41
47
  }
42
48
  if (!db.objectStoreNames.contains(REFS_STORE_NAME)) {
43
49
  // tracks which canvas documents currently have a widget
@@ -46,8 +52,18 @@ function openVersioned(dbName, version) {
46
52
  // canvas. composite key so re-adding an existing ref is a no-op
47
53
  // (put with the same key overwrites, add() would throw).
48
54
  const refs = db.createObjectStore(REFS_STORE_NAME, { keyPath: ["blob_id", "canvas_doc_id"] });
49
- refs.createIndex("blob_id", "blob_id", { unique: false });
50
- refs.createIndex("canvas_doc_id", "canvas_doc_id", { unique: false });
55
+ for (const name of REFS_INDEXES) {
56
+ refs.createIndex(name, name, { unique: false });
57
+ }
58
+ }
59
+ else {
60
+ // same "add if missing" treatment for the refs store's own indexes.
61
+ const refs = req.transaction.objectStore(REFS_STORE_NAME);
62
+ for (const name of REFS_INDEXES) {
63
+ if (!refs.indexNames.contains(name)) {
64
+ refs.createIndex(name, name, { unique: false });
65
+ }
66
+ }
51
67
  }
52
68
  };
53
69
  req.onsuccess = () => resolve(req.result);
@@ -64,35 +80,63 @@ function openVersioned(dbName, version) {
64
80
  // open per operation, matching the "open fresh, close when done" pattern
65
81
  // used throughout this module.
66
82
  const resolvedVersions = new Map();
83
+ // true only if both stores exist AND every index in BLOB_INDEXES/
84
+ // REFS_INDEXES is present on them. used to detect a database that's
85
+ // already sitting at the version we're about to (re)open with but never
86
+ // actually got every index this file expects - reopening at the exact
87
+ // same version is a no-op in indexedDB (onupgradeneeded only fires when
88
+ // the requested version is HIGHER than the database's current one), so
89
+ // that case has to be caught explicitly rather than left to
90
+ // onupgradeneeded's own "add if missing" logic.
91
+ function hasAllIndexes(db) {
92
+ if (!db.objectStoreNames.contains(STORE_NAME) || !db.objectStoreNames.contains(REFS_STORE_NAME)) {
93
+ return false;
94
+ }
95
+ const tx = db.transaction([STORE_NAME, REFS_STORE_NAME], "readonly");
96
+ const blobIndexes = tx.objectStore(STORE_NAME).indexNames;
97
+ const refsIndexes = tx.objectStore(REFS_STORE_NAME).indexNames;
98
+ return (BLOB_INDEXES.every((name) => blobIndexes.contains(name)) &&
99
+ REFS_INDEXES.every((name) => refsIndexes.contains(name)));
100
+ }
67
101
  async function openDb(dbName) {
68
102
  const cachedVersion = resolvedVersions.get(dbName);
69
103
  if (cachedVersion !== undefined) {
70
104
  return openVersioned(dbName, cachedVersion);
71
105
  }
106
+ let db;
107
+ let currentVersion;
72
108
  try {
73
- const db = await openVersioned(dbName, DB_VERSION);
74
- resolvedVersions.set(dbName, DB_VERSION);
75
- return db;
109
+ db = await openVersioned(dbName, DB_VERSION);
110
+ currentVersion = DB_VERSION;
76
111
  }
77
112
  catch (err) {
78
113
  if (!(err instanceof DOMException) || err.name !== "VersionError")
79
114
  throw err;
80
- // the database already exists at some higher version - open it
81
- // version-less to discover what that is, then reopen at that exact
82
- // version (creating the store if a pre-existing database somehow
83
- // never had one).
115
+ // the database already exists at some higher version than DB_VERSION -
116
+ // open it version-less to discover what that is, then reopen at that
117
+ // exact version.
84
118
  const probe = await new Promise((resolve, reject) => {
85
119
  const req = indexedDB.open(dbName);
86
120
  req.onsuccess = () => resolve(req.result);
87
121
  req.onerror = () => reject(req.error);
88
122
  });
89
- const currentVersion = probe.version;
90
- const hasStore = probe.objectStoreNames.contains(STORE_NAME) && probe.objectStoreNames.contains(REFS_STORE_NAME);
123
+ currentVersion = probe.version;
91
124
  probe.close();
92
- const targetVersion = hasStore ? currentVersion : currentVersion + 1;
93
- resolvedVersions.set(dbName, targetVersion);
94
- return openVersioned(dbName, targetVersion);
125
+ db = await openVersioned(dbName, currentVersion);
126
+ }
127
+ // a database can reach `currentVersion` under an earlier build that
128
+ // didn't yet create every index this file expects - opening at that
129
+ // exact same version again (either branch above) never fires
130
+ // onupgradeneeded, so the "add index if missing" repair in
131
+ // onupgradeneeded never gets a chance to run. detect that case here and
132
+ // force a real upgrade by reopening one version higher.
133
+ if (!hasAllIndexes(db)) {
134
+ db.close();
135
+ currentVersion += 1;
136
+ db = await openVersioned(dbName, currentVersion);
95
137
  }
138
+ resolvedVersions.set(dbName, currentVersion);
139
+ return db;
96
140
  }
97
141
  export async function putRecord(dbName, record) {
98
142
  const db = await openDb(dbName);
@@ -125,7 +169,17 @@ async function getByIndex(dbName, indexName, value) {
125
169
  const db = await openDb(dbName);
126
170
  return new Promise((resolve, reject) => {
127
171
  const tx = db.transaction(STORE_NAME, "readonly");
128
- const req = tx.objectStore(STORE_NAME).index(indexName).get(value);
172
+ const store = tx.objectStore(STORE_NAME);
173
+ // belt-and-suspenders: openDb's upgrade path should always ensure this
174
+ // index exists, but if it's somehow still missing, fall through the
175
+ // resolver chain (return null) rather than throwing - matches the
176
+ // "missing record" behavior every other step of resolveBlob already has.
177
+ if (!store.indexNames.contains(indexName)) {
178
+ tx.oncomplete = () => db.close();
179
+ resolve(null);
180
+ return;
181
+ }
182
+ const req = store.index(indexName).get(value);
129
183
  req.onsuccess = () => resolve(req.result ?? null);
130
184
  req.onerror = () => reject(req.error);
131
185
  tx.oncomplete = () => db.close();
@@ -148,6 +148,44 @@ describe("pre-existing database at a higher version", () => {
148
148
  expect(await getRecord(dbName, record.blob_id)).toEqual(record);
149
149
  expect(await getRecordByBlake3(dbName, record.blake3)).toEqual(record);
150
150
  });
151
+ it("repairs a missing index even when both stores already exist at DB_VERSION", async () => {
152
+ // reproduces a real-world case: a database that reached DB_VERSION
153
+ // under an earlier build of this module - one that created both
154
+ // stores but not yet the "sha256" index added later. since both
155
+ // stores already exist and the version already matches, naively
156
+ // reusing that version would never fire onupgradeneeded again,
157
+ // permanently leaving the missing index unrepaired - getRecordBySha256
158
+ // would then always report a miss instead of finding the record
159
+ // (getByIndex falls through gracefully rather than throwing, so a
160
+ // silent "never repaired" regression wouldn't otherwise be caught).
161
+ const dbName = "already-at-version-missing-index-db";
162
+ await new Promise((resolve, reject) => {
163
+ const req = indexedDB.open(dbName, 3);
164
+ req.onupgradeneeded = () => {
165
+ const db = req.result;
166
+ const store = db.createObjectStore("blobs", { keyPath: "blob_id" });
167
+ // deliberately missing: "sha256" index.
168
+ store.createIndex("blake3", "blake3", { unique: false });
169
+ store.createIndex("blob_type", "blob_type", { unique: false });
170
+ store.createIndex("parent_blob_id", "parent_blob_id", { unique: false });
171
+ store.createIndex("size", "size", { unique: false });
172
+ store.createIndex("created_at", "created_at", { unique: false });
173
+ const refs = db.createObjectStore("blob_canvas_refs", {
174
+ keyPath: ["blob_id", "canvas_doc_id"],
175
+ });
176
+ refs.createIndex("blob_id", "blob_id", { unique: false });
177
+ refs.createIndex("canvas_doc_id", "canvas_doc_id", { unique: false });
178
+ };
179
+ req.onsuccess = () => {
180
+ req.result.close();
181
+ resolve();
182
+ };
183
+ req.onerror = () => reject(req.error);
184
+ });
185
+ const record = makeRecord();
186
+ await putRecord(dbName, record);
187
+ expect(await getRecordBySha256(dbName, record.sha256)).toEqual(record);
188
+ });
151
189
  });
152
190
  describe("listBlobs", () => {
153
191
  async function seedThree() {
@@ -89,6 +89,25 @@ export declare function streamFileToOpfs(file: File, options?: {
89
89
  blake3: string;
90
90
  size: number;
91
91
  }>;
92
+ /**
93
+ * hash a File with blake3, streaming it in chunks so the whole payload is
94
+ * never materialized in memory at once - unlike `hashBlake3`, which needs
95
+ * a full `Uint8Array` up front (typically built via `file.arrayBuffer()`,
96
+ * a real crash risk for multi-gigabyte files). unlike `streamFileToOpfs`,
97
+ * this has no OPFS write side at all: use it when the caller already has
98
+ * its own on-disk storage path and just needs the hash.
99
+ *
100
+ * `onProgress` reports bytes hashed / file size (0..1) per chunk.
101
+ * `signal` cancels between chunks: the worker session is freed and a
102
+ * DOMException AbortError is thrown.
103
+ *
104
+ * falls back to the one-shot `hashBlake3` (whole-file read) when no
105
+ * worker is available (e.g. SSR / certain test runners).
106
+ */
107
+ export declare function hashBlake3Streaming(file: File, options?: {
108
+ onProgress?: (fraction: number) => void;
109
+ signal?: AbortSignal;
110
+ }): Promise<string>;
92
111
  export interface ResizeImageOptions {
93
112
  maxWidth?: number;
94
113
  maxHeight?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"blob-worker-client.d.ts","sourceRoot":"","sources":["../../src/worker/blob-worker-client.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAsBtD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC,CAuDnF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAOzC;AAiCD;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAIlE;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAInE;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAIvE;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IACT,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,CAAC,CAeD;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAQxF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAC1E,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAkC3C;AAID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;iFAG6E;IAC7E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAIxB;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,SAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOhG;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAQnE"}
1
+ {"version":3,"file":"blob-worker-client.d.ts","sourceRoot":"","sources":["../../src/worker/blob-worker-client.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAsBtD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC,CAuDnF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAOzC;AAoCD;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAIlE;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAInE;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAIvE;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IACT,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,CAAC,CAkBD;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAQxF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAC1E,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAkC3C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAC1E,OAAO,CAAC,MAAM,CAAC,CAgCjB;AAID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;iFAG6E;IAC7E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAIxB;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,SAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOhG;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAQnE"}
@@ -182,7 +182,10 @@ export async function processBlobBytes(buffer, filename, mime) {
182
182
  return worker.processBlobBytes(Comlink.transfer(buffer, [buffer]), filename, mime);
183
183
  }
184
184
  // main-thread fallback path - rare, mostly for tests.
185
- const [sha256, blake3] = await Promise.all([sha256Hex(buffer), fallbackHashBlake3(new Uint8Array(buffer))]);
185
+ const [sha256, blake3] = await Promise.all([
186
+ sha256Hex(buffer),
187
+ fallbackHashBlake3(new Uint8Array(buffer)),
188
+ ]);
186
189
  return {
187
190
  blob_id: blake3,
188
191
  sha256,
@@ -263,6 +266,56 @@ export async function streamFileToOpfs(file, options) {
263
266
  reader.releaseLock();
264
267
  }
265
268
  }
269
+ /**
270
+ * hash a File with blake3, streaming it in chunks so the whole payload is
271
+ * never materialized in memory at once - unlike `hashBlake3`, which needs
272
+ * a full `Uint8Array` up front (typically built via `file.arrayBuffer()`,
273
+ * a real crash risk for multi-gigabyte files). unlike `streamFileToOpfs`,
274
+ * this has no OPFS write side at all: use it when the caller already has
275
+ * its own on-disk storage path and just needs the hash.
276
+ *
277
+ * `onProgress` reports bytes hashed / file size (0..1) per chunk.
278
+ * `signal` cancels between chunks: the worker session is freed and a
279
+ * DOMException AbortError is thrown.
280
+ *
281
+ * falls back to the one-shot `hashBlake3` (whole-file read) when no
282
+ * worker is available (e.g. SSR / certain test runners).
283
+ */
284
+ export async function hashBlake3Streaming(file, options) {
285
+ const worker = await getBlobWorker();
286
+ if (!worker)
287
+ return fallbackHashBlake3(new Uint8Array(await file.arrayBuffer()));
288
+ const sessionId = await worker.hashBegin();
289
+ const reader = file.stream().getReader();
290
+ let bytesHashed = 0;
291
+ try {
292
+ for (;;) {
293
+ if (options?.signal?.aborted) {
294
+ throw new DOMException("hash cancelled", "AbortError");
295
+ }
296
+ const { done, value } = await reader.read();
297
+ if (done)
298
+ break;
299
+ const exact = value.byteOffset === 0 && value.byteLength === value.buffer.byteLength
300
+ ? value.buffer
301
+ : value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
302
+ const chunkLen = value.byteLength;
303
+ await worker.hashPush(sessionId, Comlink.transfer(exact, [exact]));
304
+ bytesHashed += chunkLen;
305
+ if (options?.onProgress && file.size > 0) {
306
+ options.onProgress(Math.min(1, bytesHashed / file.size));
307
+ }
308
+ }
309
+ return await worker.hashFinish(sessionId);
310
+ }
311
+ catch (err) {
312
+ await worker.hashAbort(sessionId).catch(() => { });
313
+ throw err;
314
+ }
315
+ finally {
316
+ reader.releaseLock();
317
+ }
318
+ }
266
319
  /**
267
320
  * resize an image Blob to a (default WebP) data URL.
268
321
  * prefers the blob worker (keeps the decode/encode work off the main
@@ -1,6 +1,6 @@
1
1
  // @vitest-environment happy-dom
2
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";
3
+ import { base64Decode, base64Encode, generateThumbnailDataUrl, getBlobWorker, hashBlake3, hashBlake3Streaming, hashSha256, processBlobBytes, resizeImageToWebpDataUrl, shutdownBlobWorker, streamFileToOpfs, writeBlobToOpfs, } from "./blob-worker-client.js";
4
4
  import { resetMiddenBlake3Cache } from "./midden-blake3.js";
5
5
  import { log } from "../utils/log.js";
6
6
  // this test environment has no `Worker` global, so every helper below
@@ -120,6 +120,14 @@ describe("streamFileToOpfs", () => {
120
120
  await expect(streamFileToOpfs(file)).rejects.toThrow(/no blob worker/);
121
121
  });
122
122
  });
123
+ describe("hashBlake3Streaming", () => {
124
+ it("falls back to the one-shot hashBlake3 path when no worker is available", async () => {
125
+ const file = new File(["hello"], "hello.txt", { type: "text/plain" });
126
+ // no worker, no midden module in this test env - degrades to "",
127
+ // same as the one-shot hashBlake3 fallback.
128
+ expect(await hashBlake3Streaming(file)).toBe("");
129
+ });
130
+ });
123
131
  describe("resizeImageToWebpDataUrl", () => {
124
132
  it("uses the main-thread fallback and returns a data url", async () => {
125
133
  installCanvasFakes();
@@ -108,6 +108,18 @@ export declare function uploadFinish(id: number): Promise<{
108
108
  }>;
109
109
  /** abort a session: close and delete the temp file. */
110
110
  export declare function uploadAbort(id: number): Promise<void>;
111
+ /**
112
+ * begin a streaming hash-only session. throws when the environment can't
113
+ * support it (no midden module with a Blake3Hasher bundled) - callers
114
+ * fall back to the one-shot `hashBlake3`.
115
+ */
116
+ export declare function hashBegin(): Promise<number>;
117
+ /** feed the next chunk (transferred buffer) into a hash session. */
118
+ export declare function hashPush(id: number, buffer: ArrayBuffer): Promise<void>;
119
+ /** finish a session, returning the final blake3 hash. */
120
+ export declare function hashFinish(id: number): Promise<string>;
121
+ /** abort a session, freeing the hasher without finalizing. */
122
+ export declare function hashAbort(id: number): Promise<void>;
111
123
  /**
112
124
  * opfs-store selftest - runs the embedding app's midden module's
113
125
  * out-of-crate iroh-blobs store round trip against real OPFS. must run in
@@ -1 +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;;;iFAG6E;IAC7E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+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,CA6ExB;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"}
1
+ {"version":3,"file":"blob-worker-logic.d.ts","sourceRoot":"","sources":["../../src/worker/blob-worker-logic.ts"],"names":[],"mappings":"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"}
@@ -358,6 +358,58 @@ export async function uploadAbort(id) {
358
358
  const dir = await getOpfsDir(false);
359
359
  await dir?.removeEntry(session.tmpName).catch(() => { });
360
360
  }
361
+ // ---- streaming hash-only sessions -----------------------------------------
362
+ // same incremental Blake3Hasher as the upload sessions above, but with no
363
+ // OPFS write side at all - for callers that already have their own on-disk
364
+ // storage path (e.g. spume's video import, which writes to its own OPFS
365
+ // directory) and just need a blake3 without materializing the whole file
366
+ // in memory, and without a second on-disk copy under this package's own
367
+ // content-addressed OPFS_DIR.
368
+ const hashSessions = new Map();
369
+ let nextHashSessionId = 1;
370
+ /**
371
+ * begin a streaming hash-only session. throws when the environment can't
372
+ * support it (no midden module with a Blake3Hasher bundled) - callers
373
+ * fall back to the one-shot `hashBlake3`.
374
+ */
375
+ export async function hashBegin() {
376
+ const midden = await loadMiddenBlake3();
377
+ const Blake3HasherCtor = midden?.Blake3Hasher;
378
+ if (typeof Blake3HasherCtor !== "function") {
379
+ throw new Error("streaming hash unavailable: midden Blake3Hasher missing");
380
+ }
381
+ const id = nextHashSessionId++;
382
+ hashSessions.set(id, new Blake3HasherCtor());
383
+ return id;
384
+ }
385
+ /** feed the next chunk (transferred buffer) into a hash session. */
386
+ export async function hashPush(id, buffer) {
387
+ const hasher = hashSessions.get(id);
388
+ if (!hasher)
389
+ throw new Error(`unknown hash session ${id}`);
390
+ hasher.update(new Uint8Array(buffer));
391
+ }
392
+ /** finish a session, returning the final blake3 hash. */
393
+ export async function hashFinish(id) {
394
+ const hasher = hashSessions.get(id);
395
+ if (!hasher)
396
+ throw new Error(`unknown hash session ${id}`);
397
+ hashSessions.delete(id);
398
+ try {
399
+ return hasher.finalize();
400
+ }
401
+ finally {
402
+ hasher.free();
403
+ }
404
+ }
405
+ /** abort a session, freeing the hasher without finalizing. */
406
+ export async function hashAbort(id) {
407
+ const hasher = hashSessions.get(id);
408
+ if (!hasher)
409
+ return;
410
+ hashSessions.delete(id);
411
+ hasher.free();
412
+ }
361
413
  /**
362
414
  * opfs-store selftest - runs the embedding app's midden module's
363
415
  * out-of-crate iroh-blobs store round trip against real OPFS. must run in
@@ -1,6 +1,6 @@
1
1
  // @vitest-environment happy-dom
2
2
  import { afterEach, describe, expect, it, vi } from "vitest";
3
- import { base64Decode, base64Encode, generateThumbnailDataUrl, hashBlake3, hashSha256, opfsStoreSelftest, opfsStoreSelftestPersistence, processBlobBytes, readBlobFromOpfs, resizeImageToWebpDataUrl, uploadBegin, writeBlobToOpfs, } from "./blob-worker-logic.js";
3
+ import { base64Decode, base64Encode, generateThumbnailDataUrl, hashAbort, hashBegin, hashBlake3, hashFinish, hashPush, hashSha256, opfsStoreSelftest, opfsStoreSelftestPersistence, processBlobBytes, readBlobFromOpfs, resizeImageToWebpDataUrl, uploadBegin, writeBlobToOpfs, } from "./blob-worker-logic.js";
4
4
  import { resetMiddenBlake3Cache } from "./midden-blake3.js";
5
5
  // ---- fake OPFS (async writable-stream fallback path only - sync access
6
6
  // handles are worker/chromium-specific and not worth faking here; the
@@ -11,7 +11,10 @@ class FakeWritable {
11
11
  this.file = file;
12
12
  }
13
13
  async write(data) {
14
- this.file.bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer);
14
+ this.file.bytes =
15
+ data instanceof ArrayBuffer
16
+ ? new Uint8Array(data)
17
+ : new Uint8Array(data.buffer);
15
18
  }
16
19
  async close() { }
17
20
  }
@@ -160,6 +163,20 @@ describe("uploadBegin", () => {
160
163
  await expect(uploadBegin()).rejects.toThrow(/Blake3Hasher/);
161
164
  });
162
165
  });
166
+ describe("hashBegin / hashPush / hashFinish / hashAbort", () => {
167
+ it("hashBegin throws a clear error when no midden Blake3Hasher is bundled", async () => {
168
+ await expect(hashBegin()).rejects.toThrow(/Blake3Hasher/);
169
+ });
170
+ it("hashPush throws for an unknown session id", async () => {
171
+ await expect(hashPush(999, new ArrayBuffer(4))).rejects.toThrow(/unknown hash session/);
172
+ });
173
+ it("hashFinish throws for an unknown session id", async () => {
174
+ await expect(hashFinish(999)).rejects.toThrow(/unknown hash session/);
175
+ });
176
+ it("hashAbort is a no-op for an unknown session id", async () => {
177
+ await expect(hashAbort(999)).resolves.toBeUndefined();
178
+ });
179
+ });
163
180
  describe("opfsStoreSelftest / opfsStoreSelftestPersistence", () => {
164
181
  it("opfsStoreSelftest throws when no midden module is bundled", async () => {
165
182
  await expect(opfsStoreSelftest()).rejects.toThrow(/opfs_store_selftest/);
@@ -1,4 +1,4 @@
1
- import { base64Decode, base64Encode, generateThumbnailDataUrl, hashBlake3, hashSha256, opfsStoreSelftest, opfsStoreSelftestPersistence, processBlobBytes, readBlobFromOpfs, resizeImageToWebpDataUrl, uploadAbort, uploadBegin, uploadFinish, uploadPush, writeBlobToOpfs } from "./blob-worker-logic.js";
1
+ import { base64Decode, base64Encode, generateThumbnailDataUrl, hashAbort, hashBegin, hashBlake3, hashFinish, hashPush, hashSha256, opfsStoreSelftest, opfsStoreSelftestPersistence, processBlobBytes, readBlobFromOpfs, resizeImageToWebpDataUrl, uploadAbort, uploadBegin, uploadFinish, uploadPush, writeBlobToOpfs } from "./blob-worker-logic.js";
2
2
  declare const api: {
3
3
  hashBlake3: typeof hashBlake3;
4
4
  hashSha256: typeof hashSha256;
@@ -13,6 +13,10 @@ declare const api: {
13
13
  uploadPush: typeof uploadPush;
14
14
  uploadFinish: typeof uploadFinish;
15
15
  uploadAbort: typeof uploadAbort;
16
+ hashBegin: typeof hashBegin;
17
+ hashPush: typeof hashPush;
18
+ hashFinish: typeof hashFinish;
19
+ hashAbort: typeof hashAbort;
16
20
  opfsStoreSelftest: typeof opfsStoreSelftest;
17
21
  opfsStoreSelftestPersistence: typeof opfsStoreSelftestPersistence;
18
22
  };
@@ -1 +1 @@
1
- {"version":3,"file":"blob-worker.d.ts","sourceRoot":"","sources":["../../src/worker/blob-worker.ts"],"names":[],"mappings":"AAeA,OAAO,EAEL,YAAY,EACZ,YAAY,EACZ,wBAAwB,EACxB,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,4BAA4B,EAC5B,gBAAgB,EAChB,gBAAgB,EAChB,wBAAwB,EACxB,WAAW,EACX,WAAW,EACX,YAAY,EACZ,UAAU,EACV,eAAe,EAChB,MAAM,wBAAwB,CAAC;AAEhC,QAAA,MAAM,GAAG;;;;;;;;;;;;;;;;CAgBR,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,CAAC"}
1
+ {"version":3,"file":"blob-worker.d.ts","sourceRoot":"","sources":["../../src/worker/blob-worker.ts"],"names":[],"mappings":"AAeA,OAAO,EAEL,YAAY,EACZ,YAAY,EACZ,wBAAwB,EACxB,SAAS,EACT,SAAS,EACT,UAAU,EACV,UAAU,EACV,QAAQ,EACR,UAAU,EACV,iBAAiB,EACjB,4BAA4B,EAC5B,gBAAgB,EAChB,gBAAgB,EAChB,wBAAwB,EACxB,WAAW,EACX,WAAW,EACX,YAAY,EACZ,UAAU,EACV,eAAe,EAChB,MAAM,wBAAwB,CAAC;AAEhC,QAAA,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;CAoBR,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,CAAC"}
@@ -12,7 +12,7 @@
12
12
  // worker-only glue - importing it anywhere other than as a worker script
13
13
  // runs `Comlink.expose()` and posts a ready message for no reason.
14
14
  import * as Comlink from "comlink";
15
- import { BLOB_WORKER_READY_MESSAGE, base64Decode, base64Encode, generateThumbnailDataUrl, hashBlake3, hashSha256, opfsStoreSelftest, opfsStoreSelftestPersistence, processBlobBytes, readBlobFromOpfs, resizeImageToWebpDataUrl, uploadAbort, uploadBegin, uploadFinish, uploadPush, writeBlobToOpfs, } from "./blob-worker-logic.js";
15
+ import { BLOB_WORKER_READY_MESSAGE, base64Decode, base64Encode, generateThumbnailDataUrl, hashAbort, hashBegin, hashBlake3, hashFinish, hashPush, hashSha256, opfsStoreSelftest, opfsStoreSelftestPersistence, processBlobBytes, readBlobFromOpfs, resizeImageToWebpDataUrl, uploadAbort, uploadBegin, uploadFinish, uploadPush, writeBlobToOpfs, } from "./blob-worker-logic.js";
16
16
  const api = {
17
17
  hashBlake3,
18
18
  hashSha256,
@@ -27,6 +27,10 @@ const api = {
27
27
  uploadPush,
28
28
  uploadFinish,
29
29
  uploadAbort,
30
+ hashBegin,
31
+ hashPush,
32
+ hashFinish,
33
+ hashAbort,
30
34
  opfsStoreSelftest,
31
35
  opfsStoreSelftestPersistence,
32
36
  };
@@ -1,8 +1,8 @@
1
1
  export type { ProcessedBlob, ResizeImageOptions } from "./blob-worker-logic.js";
2
2
  export { BLOB_WORKER_READY_MESSAGE, OPFS_DIR as BLOB_OPFS_DIR } from "./blob-worker-logic.js";
3
3
  export type { BlobWorkerApi } from "./blob-worker.js";
4
- export { getBlobWorker, shutdownBlobWorker, hashBlake3, hashSha256, base64Encode, base64Decode, processBlobBytes, writeBlobToOpfs, streamFileToOpfs, resizeImageToWebpDataUrl, generateThumbnailDataUrl, } from "./blob-worker-client.js";
5
- export type { MiddenWorkerApi, MiddenWorkerIdentity, StreamInfo } from "./midden-worker-contract.js";
4
+ export { getBlobWorker, shutdownBlobWorker, hashBlake3, hashBlake3Streaming, hashSha256, base64Encode, base64Decode, processBlobBytes, writeBlobToOpfs, streamFileToOpfs, resizeImageToWebpDataUrl, generateThumbnailDataUrl, } from "./blob-worker-client.js";
5
+ export type { MiddenWorkerApi, MiddenWorkerIdentity, StreamInfo, } from "./midden-worker-contract.js";
6
6
  export { MIDDEN_WORKER_READY_MESSAGE, MIDDEN_WORKER_READY_TIMEOUT_MS, } from "./midden-worker-contract.js";
7
7
  export type { CreateMiddenWorker } from "./midden-worker-client.js";
8
8
  export { WorkerBiStream, WorkerImportSession, WorkerMiddenNode } from "./midden-worker-client.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAChF,OAAO,EAAE,yBAAyB,EAAE,QAAQ,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE9F,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,UAAU,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AAEjC,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACrG,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,6BAA6B,CAAC;AAErC,YAAY,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElG,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAChF,OAAO,EAAE,yBAAyB,EAAE,QAAQ,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE9F,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,UAAU,EACV,mBAAmB,EACnB,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AAEjC,YAAY,EACV,eAAe,EACf,oBAAoB,EACpB,UAAU,GACX,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,6BAA6B,CAAC;AAErC,YAAY,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElG,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -1,5 +1,5 @@
1
1
  export { BLOB_WORKER_READY_MESSAGE, OPFS_DIR as BLOB_OPFS_DIR } from "./blob-worker-logic.js";
2
- export { getBlobWorker, shutdownBlobWorker, hashBlake3, hashSha256, base64Encode, base64Decode, processBlobBytes, writeBlobToOpfs, streamFileToOpfs, resizeImageToWebpDataUrl, generateThumbnailDataUrl, } from "./blob-worker-client.js";
2
+ export { getBlobWorker, shutdownBlobWorker, hashBlake3, hashBlake3Streaming, hashSha256, base64Encode, base64Decode, processBlobBytes, writeBlobToOpfs, streamFileToOpfs, resizeImageToWebpDataUrl, generateThumbnailDataUrl, } from "./blob-worker-client.js";
3
3
  export { MIDDEN_WORKER_READY_MESSAGE, MIDDEN_WORKER_READY_TIMEOUT_MS, } from "./midden-worker-contract.js";
4
4
  export { WorkerBiStream, WorkerImportSession, WorkerMiddenNode } from "./midden-worker-client.js";
5
5
  export { MiddenNode } from "./midden-stub.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freqhole/reliquary",
3
- "version": "0.2.14",
3
+ "version": "0.3.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "freqhole media blob storage domain: content-addressed blobs, transfer, worker, solid helpers",
@@ -51,7 +51,7 @@
51
51
  }
52
52
  },
53
53
  "scripts": {
54
- "build": "cp ../README.md ./README.md && tsc -p tsconfig.json",
54
+ "build": "node -e \"require('fs').copyFileSync('../README.md','README.md')\" && tsc -p tsconfig.json",
55
55
  "typecheck": "tsc --noEmit",
56
56
  "test": "vitest run"
57
57
  },
@@ -62,7 +62,7 @@
62
62
  "@automerge/automerge-repo": "^2.5.6",
63
63
  "fake-indexeddb": "^6.0.0",
64
64
  "solid-js": "^1.9.0",
65
- "vitest": "^2.1.0"
65
+ "vitest": "^3.2.7"
66
66
  },
67
67
  "peerDependenciesMeta": {
68
68
  "@automerge/automerge-repo": {
@@ -85,6 +85,6 @@
85
85
  "happy-dom": "^20.10.6",
86
86
  "solid-js": "^1.9.0",
87
87
  "typescript": "^5.6.0",
88
- "vitest": "^2.1.0"
88
+ "vitest": "^3.2.7"
89
89
  }
90
90
  }