@freqhole/reliquary 0.3.12 → 0.3.13
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.
- package/README.md +3 -4
- package/dist/blobs/bytes-backend.d.ts +8 -2
- package/dist/blobs/bytes-backend.d.ts.map +1 -1
- package/dist/blobs/bytes-backend.js +9 -3
- package/dist/blobs/db.d.ts +0 -1
- package/dist/blobs/db.d.ts.map +1 -1
- package/dist/blobs/db.js +5 -8
- package/dist/blobs/db.test.d.ts.map +1 -1
- package/dist/blobs/db.test.js +13 -32
- package/dist/blobs/store.d.ts +9 -3
- package/dist/blobs/store.d.ts.map +1 -1
- package/dist/blobs/store.js +16 -28
- package/dist/blobs/store.test.js +36 -24
- package/dist/blobs/types.d.ts +3 -7
- package/dist/blobs/types.d.ts.map +1 -1
- package/dist/blobs/types.js +5 -5
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +0 -1
- package/dist/worker/blob-worker-client.d.ts +0 -5
- package/dist/worker/blob-worker-client.d.ts.map +1 -1
- package/dist/worker/blob-worker-client.js +1 -15
- package/dist/worker/blob-worker-client.test.js +2 -10
- package/dist/worker/blob-worker-logic.d.ts +3 -14
- package/dist/worker/blob-worker-logic.d.ts.map +1 -1
- package/dist/worker/blob-worker-logic.js +7 -24
- package/dist/worker/blob-worker-logic.test.js +1 -9
- package/dist/worker/blob-worker.d.ts +1 -2
- package/dist/worker/blob-worker.d.ts.map +1 -1
- package/dist/worker/blob-worker.js +3 -4
- package/dist/worker/index.d.ts +1 -1
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +1 -1
- package/package.json +1 -1
- package/dist/utils/hash.d.ts +0 -13
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js +0 -31
- package/dist/utils/hash.test.d.ts +0 -2
- package/dist/utils/hash.test.d.ts.map +0 -1
- package/dist/utils/hash.test.js +0 -56
package/README.md
CHANGED
|
@@ -8,10 +8,9 @@ utils. rust crate `reliquary` + npm package `@freqhole/reliquary`.
|
|
|
8
8
|
|
|
9
9
|
## architecture
|
|
10
10
|
|
|
11
|
-
blake3 is the canonical content address everywhere
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
the consuming app.
|
|
11
|
+
blake3 is the sole canonical content address everywhere. iroh p2p is a transport, not the
|
|
12
|
+
architecture - the store contracts and transfer flows are transport-injected. this library
|
|
13
|
+
never owns an http server: range serving stays in the consuming app.
|
|
15
14
|
|
|
16
15
|
```mermaid
|
|
17
16
|
graph LR
|
|
@@ -16,8 +16,14 @@ export interface BytesBackend {
|
|
|
16
16
|
export declare function isOPFSSupported(): boolean;
|
|
17
17
|
export declare function createOpfsBackend(): BytesBackend;
|
|
18
18
|
export declare function createCacheBackend(): BytesBackend;
|
|
19
|
-
/** the default resolver chain: OPFS first, Cache API fallback.
|
|
20
|
-
|
|
19
|
+
/** the default resolver chain: OPFS first, Cache API fallback. pass
|
|
20
|
+
* `skipOpfs` to drop OPFS entirely - for engines where
|
|
21
|
+
* `navigator.storage.getDirectory` is feature-detectable but silently
|
|
22
|
+
* non-functional (confirmed real 2026-10-07 on macOS Catalina's bundled
|
|
23
|
+
* WebKit: OPFS writes/reads never throw, but every read comes back
|
|
24
|
+
* empty - a blob URL registers with the right MIME type yet "has no
|
|
25
|
+
* content" in the inspector). */
|
|
26
|
+
export declare function defaultBytesChain(skipOpfs?: boolean): BytesBackend[];
|
|
21
27
|
/**
|
|
22
28
|
* write bytes through the chain, trying each backend in order until one
|
|
23
29
|
* accepts the write. returns the name of the backend that succeeded, or
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bytes-backend.d.ts","sourceRoot":"","sources":["../../src/blobs/bytes-backend.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAInD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAChC;;iBAEa;IACb,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC9C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,+DAA+D;IAC/D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,iEAAiE;AACjE,wBAAgB,eAAe,IAAI,OAAO,CAMzC;AAYD,wBAAgB,iBAAiB,IAAI,YAAY,CAmEhD;AAgBD,wBAAgB,kBAAkB,IAAI,YAAY,CA8DjD;AAED
|
|
1
|
+
{"version":3,"file":"bytes-backend.d.ts","sourceRoot":"","sources":["../../src/blobs/bytes-backend.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAInD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAChC;;iBAEa;IACb,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC9C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,+DAA+D;IAC/D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,iEAAiE;AACjE,wBAAgB,eAAe,IAAI,OAAO,CAMzC;AAYD,wBAAgB,iBAAiB,IAAI,YAAY,CAmEhD;AAgBD,wBAAgB,kBAAkB,IAAI,YAAY,CA8DjD;AAED;;;;;;kCAMkC;AAClC,wBAAgB,iBAAiB,CAAC,QAAQ,UAAQ,GAAG,YAAY,EAAE,CAElE;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,YAAY,EAAE,EACrB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAMlC;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,YAAY,EAAE,EACrB,EAAE,EAAE,MAAM,EACV,YAAY,CAAC,EAAE,gBAAgB,GAC9B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAa7B;AAED,sEAAsE;AACtE,wBAAsB,eAAe,CACnC,KAAK,EAAE,YAAY,EAAE,EACrB,EAAE,EAAE,MAAM,EACV,YAAY,CAAC,EAAE,gBAAgB,GAC9B,OAAO,CAAC,OAAO,CAAC,CASlB;AAED;;kDAEkD;AAClD,wBAAsB,eAAe,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtF"}
|
|
@@ -190,9 +190,15 @@ export function createCacheBackend() {
|
|
|
190
190
|
},
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
|
-
/** the default resolver chain: OPFS first, Cache API fallback.
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
/** the default resolver chain: OPFS first, Cache API fallback. pass
|
|
194
|
+
* `skipOpfs` to drop OPFS entirely - for engines where
|
|
195
|
+
* `navigator.storage.getDirectory` is feature-detectable but silently
|
|
196
|
+
* non-functional (confirmed real 2026-10-07 on macOS Catalina's bundled
|
|
197
|
+
* WebKit: OPFS writes/reads never throw, but every read comes back
|
|
198
|
+
* empty - a blob URL registers with the right MIME type yet "has no
|
|
199
|
+
* content" in the inspector). */
|
|
200
|
+
export function defaultBytesChain(skipOpfs = false) {
|
|
201
|
+
return skipOpfs ? [createCacheBackend()] : [createOpfsBackend(), createCacheBackend()];
|
|
196
202
|
}
|
|
197
203
|
/**
|
|
198
204
|
* write bytes through the chain, trying each backend in order until one
|
package/dist/blobs/db.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { BlobRecord } from "./types.js";
|
|
2
2
|
export declare function putRecord(dbName: string, record: BlobRecord): Promise<void>;
|
|
3
3
|
export declare function getRecord(dbName: string, blobId: string): Promise<BlobRecord | null>;
|
|
4
|
-
export declare function getRecordBySha256(dbName: string, sha256: string): Promise<BlobRecord | null>;
|
|
5
4
|
export declare function getRecordByBlake3(dbName: string, blake3: string): Promise<BlobRecord | null>;
|
|
6
5
|
export declare function deleteRecord(dbName: string, blobId: string): Promise<void>;
|
|
7
6
|
export declare function clearRecords(dbName: string): Promise<void>;
|
package/dist/blobs/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/blobs/db.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/blobs/db.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA+I7C,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,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,CAChC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAYrF;AAED,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAuBf;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,CAC7B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,aAAa,CAAC,CA+ExB"}
|
package/dist/blobs/db.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// the metadata side of the resolver chain: an IndexedDB-backed record
|
|
2
2
|
// store keyed by blob_id (the blake3 hex digest), with secondary indexes
|
|
3
|
-
// so a record can also be found by
|
|
4
|
-
// (covering a record whose primary key is a legacy sha256), by blob_type,
|
|
5
|
-
// or by parent_blob_id.
|
|
3
|
+
// so a record can also be found by blob_type or parent_blob_id.
|
|
6
4
|
//
|
|
7
5
|
// follows a "open fresh, close when done" pattern for every operation
|
|
8
6
|
// rather than holding one long-lived connection open - simple, and avoids
|
|
@@ -14,7 +12,7 @@ const DB_VERSION = 3;
|
|
|
14
12
|
// by openDb's version-fallback path to detect an already-existing database
|
|
15
13
|
// that's missing an index added by a newer db.ts than whatever build last
|
|
16
14
|
// touched it.
|
|
17
|
-
const BLOB_INDEXES = ["
|
|
15
|
+
const BLOB_INDEXES = ["blake3", "blob_type", "parent_blob_id", "size", "created_at"];
|
|
18
16
|
const REFS_INDEXES = ["blob_id", "canvas_doc_id"];
|
|
19
17
|
function openVersioned(dbName, version) {
|
|
20
18
|
return new Promise((resolve, reject) => {
|
|
@@ -51,7 +49,9 @@ function openVersioned(dbName, version) {
|
|
|
51
49
|
// this before purging local bytes, instead of scanning every
|
|
52
50
|
// canvas. composite key so re-adding an existing ref is a no-op
|
|
53
51
|
// (put with the same key overwrites, add() would throw).
|
|
54
|
-
const refs = db.createObjectStore(REFS_STORE_NAME, {
|
|
52
|
+
const refs = db.createObjectStore(REFS_STORE_NAME, {
|
|
53
|
+
keyPath: ["blob_id", "canvas_doc_id"],
|
|
54
|
+
});
|
|
55
55
|
for (const name of REFS_INDEXES) {
|
|
56
56
|
refs.createIndex(name, name, { unique: false });
|
|
57
57
|
}
|
|
@@ -185,9 +185,6 @@ async function getByIndex(dbName, indexName, value) {
|
|
|
185
185
|
tx.oncomplete = () => db.close();
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
|
-
export function getRecordBySha256(dbName, sha256) {
|
|
189
|
-
return getByIndex(dbName, "sha256", sha256);
|
|
190
|
-
}
|
|
191
188
|
export function getRecordByBlake3(dbName, blake3) {
|
|
192
189
|
return getByIndex(dbName, "blake3", blake3);
|
|
193
190
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.test.d.ts","sourceRoot":"","sources":["../../src/blobs/db.test.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"db.test.d.ts","sourceRoot":"","sources":["../../src/blobs/db.test.ts"],"names":[],"mappings":"AAIA,OAAO,qBAAqB,CAAC"}
|
package/dist/blobs/db.test.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// exercises the metadata layer directly: CRUD + secondary indexes
|
|
2
|
-
// (
|
|
2
|
+
// (blake3, blob_type, parent_blob_id) that make the record resolver chain
|
|
3
|
+
// possible.
|
|
3
4
|
import "fake-indexeddb/auto";
|
|
4
5
|
import { IDBFactory } from "fake-indexeddb";
|
|
5
6
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
6
|
-
import { addCanvasRef, clearCanvasRefs, clearRecords, deleteRecord, getCanvasRefs, getRecord, getRecordByBlake3,
|
|
7
|
+
import { addCanvasRef, clearCanvasRefs, clearRecords, deleteRecord, getCanvasRefs, getRecord, getRecordByBlake3, listBlobs, putRecord, removeAllCanvasRefsForCanvas, removeCanvasRef, } from "./db.js";
|
|
7
8
|
const DB_NAME = "blobs-db-test";
|
|
8
9
|
beforeEach(() => {
|
|
9
10
|
globalThis.indexedDB = new IDBFactory();
|
|
@@ -12,7 +13,6 @@ function makeRecord(overrides = {}) {
|
|
|
12
13
|
return {
|
|
13
14
|
blob_id: "blake3-aaa",
|
|
14
15
|
blake3: "blake3-aaa",
|
|
15
|
-
sha256: "sha256-aaa",
|
|
16
16
|
filename: "song.mp3",
|
|
17
17
|
mime: "audio/mpeg",
|
|
18
18
|
size: 1234,
|
|
@@ -39,32 +39,14 @@ describe("putRecord / getRecord", () => {
|
|
|
39
39
|
expect(record?.filename).toBe("second.mp3");
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
|
-
describe("
|
|
43
|
-
it("finds a record by its sha256 index", async () => {
|
|
44
|
-
const record = makeRecord();
|
|
45
|
-
await putRecord(DB_NAME, record);
|
|
46
|
-
expect(await getRecordBySha256(DB_NAME, "sha256-aaa")).toEqual(record);
|
|
47
|
-
});
|
|
42
|
+
describe("getRecordByBlake3", () => {
|
|
48
43
|
it("finds a record by its blake3 index", async () => {
|
|
49
44
|
const record = makeRecord();
|
|
50
45
|
await putRecord(DB_NAME, record);
|
|
51
46
|
expect(await getRecordByBlake3(DB_NAME, "blake3-aaa")).toEqual(record);
|
|
52
47
|
});
|
|
53
|
-
it("finds a legacy record whose primary key is a sha256, not its blake3", async () => {
|
|
54
|
-
// simulates a record created before blake3 became canonical: blob_id
|
|
55
|
-
// is the legacy sha256, but blake3 is known and indexed separately.
|
|
56
|
-
const legacy = makeRecord({
|
|
57
|
-
blob_id: "sha256-legacy",
|
|
58
|
-
blake3: "blake3-discovered-later",
|
|
59
|
-
sha256: "sha256-legacy",
|
|
60
|
-
});
|
|
61
|
-
await putRecord(DB_NAME, legacy);
|
|
62
|
-
expect(await getRecordByBlake3(DB_NAME, "blake3-discovered-later")).toEqual(legacy);
|
|
63
|
-
expect(await getRecord(DB_NAME, "sha256-legacy")).toEqual(legacy);
|
|
64
|
-
});
|
|
65
48
|
it("returns null for an empty hash rather than matching everything", async () => {
|
|
66
49
|
await putRecord(DB_NAME, makeRecord());
|
|
67
|
-
expect(await getRecordBySha256(DB_NAME, "")).toBeNull();
|
|
68
50
|
expect(await getRecordByBlake3(DB_NAME, "")).toBeNull();
|
|
69
51
|
});
|
|
70
52
|
});
|
|
@@ -134,7 +116,6 @@ describe("pre-existing database at a higher version", () => {
|
|
|
134
116
|
const req = indexedDB.open(dbName, 3);
|
|
135
117
|
req.onupgradeneeded = () => {
|
|
136
118
|
const store = req.result.createObjectStore("blobs", { keyPath: "blob_id" });
|
|
137
|
-
store.createIndex("sha256", "sha256", { unique: false });
|
|
138
119
|
store.createIndex("blake3", "blake3", { unique: false });
|
|
139
120
|
};
|
|
140
121
|
req.onsuccess = () => {
|
|
@@ -151,22 +132,22 @@ describe("pre-existing database at a higher version", () => {
|
|
|
151
132
|
it("repairs a missing index even when both stores already exist at DB_VERSION", async () => {
|
|
152
133
|
// reproduces a real-world case: a database that reached DB_VERSION
|
|
153
134
|
// under an earlier build of this module - one that created both
|
|
154
|
-
// stores but not yet the "
|
|
155
|
-
// stores already exist and the version already matches, naively
|
|
135
|
+
// stores but not yet the "parent_blob_id" index added later. since
|
|
136
|
+
// both stores already exist and the version already matches, naively
|
|
156
137
|
// reusing that version would never fire onupgradeneeded again,
|
|
157
|
-
// permanently leaving the missing index unrepaired -
|
|
158
|
-
// would then always report a miss instead of finding the
|
|
159
|
-
// (getByIndex falls through gracefully rather than throwing,
|
|
160
|
-
// silent "never repaired" regression wouldn't otherwise be
|
|
138
|
+
// permanently leaving the missing index unrepaired - resolveBlob's
|
|
139
|
+
// lookups would then always report a miss instead of finding the
|
|
140
|
+
// record (getByIndex falls through gracefully rather than throwing,
|
|
141
|
+
// so a silent "never repaired" regression wouldn't otherwise be
|
|
142
|
+
// caught).
|
|
161
143
|
const dbName = "already-at-version-missing-index-db";
|
|
162
144
|
await new Promise((resolve, reject) => {
|
|
163
145
|
const req = indexedDB.open(dbName, 3);
|
|
164
146
|
req.onupgradeneeded = () => {
|
|
165
147
|
const db = req.result;
|
|
166
148
|
const store = db.createObjectStore("blobs", { keyPath: "blob_id" });
|
|
167
|
-
// deliberately missing: "sha256" index.
|
|
168
149
|
store.createIndex("blake3", "blake3", { unique: false });
|
|
169
|
-
|
|
150
|
+
// deliberately missing: "blob_type" index.
|
|
170
151
|
store.createIndex("parent_blob_id", "parent_blob_id", { unique: false });
|
|
171
152
|
store.createIndex("size", "size", { unique: false });
|
|
172
153
|
store.createIndex("created_at", "created_at", { unique: false });
|
|
@@ -184,7 +165,7 @@ describe("pre-existing database at a higher version", () => {
|
|
|
184
165
|
});
|
|
185
166
|
const record = makeRecord();
|
|
186
167
|
await putRecord(dbName, record);
|
|
187
|
-
expect(await
|
|
168
|
+
expect(await getRecordByBlake3(dbName, record.blake3)).toEqual(record);
|
|
188
169
|
});
|
|
189
170
|
});
|
|
190
171
|
describe("listBlobs", () => {
|
package/dist/blobs/store.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ListBlobsOptions, type ListBlobsPage } from "./db.js";
|
|
2
2
|
import type { BlobLocalityInfo, BlobRecord, NewBlobMeta } from "./types.js";
|
|
3
3
|
export { isOPFSSupported } from "./bytes-backend.js";
|
|
4
|
-
export type { BlobLocalityInfo, BlobLocalityMetadata, BlobRecord, BlobType, BytesBackendName, NewBlobMeta } from "./types.js";
|
|
4
|
+
export type { BlobLocalityInfo, BlobLocalityMetadata, BlobRecord, BlobType, BytesBackendName, NewBlobMeta, } from "./types.js";
|
|
5
5
|
export type { ListBlobsOptions, ListBlobsPage } from "./db.js";
|
|
6
6
|
/** default IndexedDB database name for apps that don't need to preserve an
|
|
7
7
|
* existing one. */
|
|
@@ -19,6 +19,13 @@ export interface BlobStoreOptions {
|
|
|
19
19
|
* caller didn't expect is worse than a loud failure the caller can
|
|
20
20
|
* retry or surface. */
|
|
21
21
|
allowCacheFallback?: boolean;
|
|
22
|
+
/** when true, skip OPFS entirely and always use the Cache API - for
|
|
23
|
+
* engines where OPFS is feature-detectable (`navigator.storage.
|
|
24
|
+
* getDirectory` exists) but silently non-functional: confirmed real
|
|
25
|
+
* 2026-10-07 on macOS Catalina's bundled WebKit, every OPFS read came
|
|
26
|
+
* back empty (no exception anywhere) despite writes reporting
|
|
27
|
+
* success. defaults to false. */
|
|
28
|
+
disableOpfs?: boolean;
|
|
22
29
|
}
|
|
23
30
|
export interface StoreBlobFromFileOptions {
|
|
24
31
|
onProgress?: (fraction: number) => void;
|
|
@@ -29,7 +36,6 @@ export interface BlobStore {
|
|
|
29
36
|
storeBlobFromFile(file: File, meta?: Partial<NewBlobMeta>, options?: StoreBlobFromFileOptions): Promise<BlobRecord>;
|
|
30
37
|
getBlobRecord(blobId: string): Promise<BlobRecord | null>;
|
|
31
38
|
getBlobRecordByBlake3(blake3: string): Promise<BlobRecord | null>;
|
|
32
|
-
getBlobRecordBySha256(sha256: string): Promise<BlobRecord | null>;
|
|
33
39
|
resolveBlob(blobId: string, blake3?: string): Promise<BlobRecord | null>;
|
|
34
40
|
getBlobMetadata(blobId: string, blake3?: string): Promise<BlobRecord | null>;
|
|
35
41
|
getBlobData(blobId: string, blake3?: string): Promise<ArrayBuffer | null>;
|
|
@@ -55,6 +61,6 @@ export interface BlobStore {
|
|
|
55
61
|
/** create a blob store instance. each instance owns its own object-url
|
|
56
62
|
* cache; construct one per app (not per component/request). */
|
|
57
63
|
export declare function createBlobStore(options?: BlobStoreOptions): BlobStore;
|
|
58
|
-
export { writeThroughChain, readThroughChain, hasBytesInChain, removeFromChain } from "./bytes-backend.js";
|
|
64
|
+
export { writeThroughChain, readThroughChain, hasBytesInChain, removeFromChain, } from "./bytes-backend.js";
|
|
59
65
|
export type { BytesBackend } from "./bytes-backend.js";
|
|
60
66
|
//# sourceMappingURL=store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/blobs/store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/blobs/store.ts"],"names":[],"mappings":"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"}
|
package/dist/blobs/store.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
// the blake3-canonical blob record store: ties the metadata side (db.ts,
|
|
2
2
|
// IndexedDB) to the bytes side (bytes-backend.ts, OPFS + Cache API) behind
|
|
3
|
-
// one API
|
|
4
|
-
// references keep working alongside new blake3-keyed records.
|
|
3
|
+
// one API.
|
|
5
4
|
//
|
|
6
5
|
// "resolver chain" here means two related lookup chains:
|
|
7
|
-
// - record resolution (`resolveBlob`): primary key -> blake3 index
|
|
8
|
-
//
|
|
9
|
-
// this is what lets a caller pass either a blake3 or a legacy sha256
|
|
10
|
-
// and get the same record back.
|
|
6
|
+
// - record resolution (`resolveBlob`): primary key -> blake3 index
|
|
7
|
+
// (separately-known hash).
|
|
11
8
|
// - bytes resolution (`bytes-backend.ts`'s `readThroughChain`): a
|
|
12
9
|
// record's `storage_backend` field says where its bytes were written;
|
|
13
10
|
// when that's unknown (a record predating the field) every backend is
|
|
@@ -19,9 +16,9 @@
|
|
|
19
16
|
// storage (OPFS directory, Cache API cache) is content-addressed by
|
|
20
17
|
// blake3 and shared at the origin regardless of which app wrote it, so it
|
|
21
18
|
// is not parameterized per store instance - see `bytes-backend.ts`.
|
|
22
|
-
import { hashBlake3,
|
|
23
|
-
import { addCanvasRef, clearCanvasRefs, clearRecords, deleteRecord, getCanvasRefs, getRecord, getRecordByBlake3,
|
|
24
|
-
import { createOpfsBackend, defaultBytesChain, hasBytesInChain, readThroughChain, removeFromChain, writeThroughChain, } from "./bytes-backend.js";
|
|
19
|
+
import { hashBlake3, streamFileToOpfs } from "../worker/index.js";
|
|
20
|
+
import { addCanvasRef, clearCanvasRefs, clearRecords, deleteRecord, getCanvasRefs, getRecord, getRecordByBlake3, listBlobs, putRecord, removeAllCanvasRefsForCanvas, removeCanvasRef, } from "./db.js";
|
|
21
|
+
import { createCacheBackend, createOpfsBackend, defaultBytesChain, hasBytesInChain, readThroughChain, removeFromChain, writeThroughChain, } from "./bytes-backend.js";
|
|
25
22
|
export { isOPFSSupported } from "./bytes-backend.js";
|
|
26
23
|
/** default IndexedDB database name for apps that don't need to preserve an
|
|
27
24
|
* existing one. */
|
|
@@ -34,7 +31,11 @@ const STREAM_UPLOAD_THRESHOLD = 8 * 1024 * 1024;
|
|
|
34
31
|
* cache; construct one per app (not per component/request). */
|
|
35
32
|
export function createBlobStore(options = {}) {
|
|
36
33
|
const dbName = options.dbName ?? DEFAULT_DB_NAME;
|
|
37
|
-
const chain = options.
|
|
34
|
+
const chain = options.disableOpfs
|
|
35
|
+
? [createCacheBackend()]
|
|
36
|
+
: options.allowCacheFallback
|
|
37
|
+
? defaultBytesChain()
|
|
38
|
+
: [createOpfsBackend()];
|
|
38
39
|
const blobUrlCache = new Map();
|
|
39
40
|
let beforeUnloadRegistered = false;
|
|
40
41
|
function ensureBeforeUnloadListener() {
|
|
@@ -48,9 +49,9 @@ export function createBlobStore(options = {}) {
|
|
|
48
49
|
beforeUnloadRegistered = true;
|
|
49
50
|
}
|
|
50
51
|
async function storeBlob(data, meta) {
|
|
51
|
-
// hash before writing anything -
|
|
52
|
+
// hash before writing anything - blake3 hashing copies its input rather
|
|
52
53
|
// than transferring it, so `data` is still intact for the write below.
|
|
53
|
-
const
|
|
54
|
+
const blake3 = await hashBlake3(new Uint8Array(data));
|
|
54
55
|
const blobId = blake3;
|
|
55
56
|
// dedup - content-addressed, so a pre-existing record for this blake3
|
|
56
57
|
// means the bytes are already stored under it.
|
|
@@ -68,7 +69,6 @@ export function createBlobStore(options = {}) {
|
|
|
68
69
|
const record = {
|
|
69
70
|
blob_id: blobId,
|
|
70
71
|
blake3,
|
|
71
|
-
sha256,
|
|
72
72
|
filename: meta.filename,
|
|
73
73
|
mime: meta.mime,
|
|
74
74
|
size: data.byteLength,
|
|
@@ -90,9 +90,6 @@ export function createBlobStore(options = {}) {
|
|
|
90
90
|
const existing = await getRecord(dbName, blake3);
|
|
91
91
|
if (existing)
|
|
92
92
|
return existing;
|
|
93
|
-
// sha256 is legacy-only - it's never computed for a streamed
|
|
94
|
-
// upload, since brand-new content has no old sha256-keyed
|
|
95
|
-
// reference that would need it.
|
|
96
93
|
const record = {
|
|
97
94
|
blob_id: blake3,
|
|
98
95
|
blake3,
|
|
@@ -129,15 +126,10 @@ export function createBlobStore(options = {}) {
|
|
|
129
126
|
function getBlobRecordByBlake3(blake3) {
|
|
130
127
|
return getRecordByBlake3(dbName, blake3);
|
|
131
128
|
}
|
|
132
|
-
function getBlobRecordBySha256(sha256) {
|
|
133
|
-
return getRecordBySha256(dbName, sha256);
|
|
134
|
-
}
|
|
135
129
|
/**
|
|
136
130
|
* resolve a blobId to a record using multiple lookup strategies: primary
|
|
137
|
-
* key -> blake3 index (blobId itself) ->
|
|
138
|
-
*
|
|
139
|
-
* canonical now, sha256-keyed records are legacy but must keep
|
|
140
|
-
* resolving.
|
|
131
|
+
* key -> blake3 index (blobId itself) -> blake3 index (separately-known
|
|
132
|
+
* hash).
|
|
141
133
|
*/
|
|
142
134
|
async function resolveBlob(blobId, blake3) {
|
|
143
135
|
if (!blobId)
|
|
@@ -148,9 +140,6 @@ export function createBlobStore(options = {}) {
|
|
|
148
140
|
const byIdAsBlake3 = await getRecordByBlake3(dbName, blobId);
|
|
149
141
|
if (byIdAsBlake3)
|
|
150
142
|
return byIdAsBlake3;
|
|
151
|
-
const bySha = await getRecordBySha256(dbName, blobId);
|
|
152
|
-
if (bySha)
|
|
153
|
-
return bySha;
|
|
154
143
|
if (blake3) {
|
|
155
144
|
const byBlake3 = await getRecordByBlake3(dbName, blake3);
|
|
156
145
|
if (byBlake3)
|
|
@@ -245,7 +234,6 @@ export function createBlobStore(options = {}) {
|
|
|
245
234
|
storeBlobFromFile,
|
|
246
235
|
getBlobRecord,
|
|
247
236
|
getBlobRecordByBlake3,
|
|
248
|
-
getBlobRecordBySha256,
|
|
249
237
|
resolveBlob,
|
|
250
238
|
getBlobMetadata: resolveBlob,
|
|
251
239
|
getBlobData,
|
|
@@ -266,4 +254,4 @@ export function createBlobStore(options = {}) {
|
|
|
266
254
|
// low-level building blocks re-exported for callers that need to compose
|
|
267
255
|
// their own resolver chain (e.g. a future transfer layer writing
|
|
268
256
|
// already-hashed, already-verified bytes without re-hashing them here).
|
|
269
|
-
export { writeThroughChain, readThroughChain, hasBytesInChain, removeFromChain } from "./bytes-backend.js";
|
|
257
|
+
export { writeThroughChain, readThroughChain, hasBytesInChain, removeFromChain, } from "./bytes-backend.js";
|
package/dist/blobs/store.test.js
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// (db.ts) and the bytes layer (bytes-backend.ts) together behind
|
|
5
5
|
// createBlobStore(). the worker (hashing + the OPFS write path) is mocked
|
|
6
6
|
// since no environment here has a real Worker or a bundled midden module -
|
|
7
|
-
// the mock computes a deterministic fake "blake3" from
|
|
8
|
-
// (real crypto.subtle is available in happy-dom
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// the mock computes a deterministic fake "blake3" from a sha256 digest
|
|
8
|
+
// (real crypto.subtle is available in happy-dom, sha256 is just used here
|
|
9
|
+
// as a convenient stand-in digest, not a feature under test) and writes
|
|
10
|
+
// bytes through the exact same fake OPFS directory the direct-read path
|
|
11
|
+
// uses, so the round trip is meaningful.
|
|
11
12
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
13
|
import { fakeIdbHarness } from "../testing/index.js";
|
|
13
14
|
// ---- fake OPFS (same shape as blob-worker-logic.test.ts / bytes-backend.test.ts) ----
|
|
@@ -17,7 +18,10 @@ class FakeWritable {
|
|
|
17
18
|
this.file = file;
|
|
18
19
|
}
|
|
19
20
|
async write(data) {
|
|
20
|
-
this.file.bytes =
|
|
21
|
+
this.file.bytes =
|
|
22
|
+
data instanceof ArrayBuffer
|
|
23
|
+
? new Uint8Array(data)
|
|
24
|
+
: new Uint8Array(data.buffer);
|
|
21
25
|
}
|
|
22
26
|
async close() { }
|
|
23
27
|
}
|
|
@@ -61,7 +65,7 @@ function installFakeOpfs() {
|
|
|
61
65
|
},
|
|
62
66
|
});
|
|
63
67
|
}
|
|
64
|
-
async function
|
|
68
|
+
async function fakeDigestHex(data) {
|
|
65
69
|
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
66
70
|
return Array.from(new Uint8Array(digest))
|
|
67
71
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
@@ -82,18 +86,17 @@ vi.mock("../worker/index.js", () => ({
|
|
|
82
86
|
}),
|
|
83
87
|
// a real blake3 digest is unavailable in this test environment (no
|
|
84
88
|
// bundled midden module) - derive a distinct, deterministic stand-in
|
|
85
|
-
// from
|
|
86
|
-
// content addresses.
|
|
89
|
+
// from a sha256 digest so different content still gets different
|
|
90
|
+
// content addresses. the hasher doesn't write any bytes - matching the
|
|
87
91
|
// real worker client, which only hashes here.
|
|
88
92
|
hashBlake3: vi.fn(async (data) => {
|
|
89
|
-
const
|
|
90
|
-
return `b3-${
|
|
93
|
+
const digest = await fakeDigestHex(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
|
|
94
|
+
return `b3-${digest}`;
|
|
91
95
|
}),
|
|
92
|
-
hashSha256: vi.fn(async (data) => fakeSha256Hex(data)),
|
|
93
96
|
streamFileToOpfs: vi.fn(async (file) => {
|
|
94
97
|
const buffer = await file.arrayBuffer();
|
|
95
|
-
const
|
|
96
|
-
const blake3 = `b3-${
|
|
98
|
+
const digest = await fakeDigestHex(buffer);
|
|
99
|
+
const blake3 = `b3-${digest}`;
|
|
97
100
|
await writeToFakeOpfs(blake3, buffer);
|
|
98
101
|
return { blake3, size: buffer.byteLength };
|
|
99
102
|
}),
|
|
@@ -228,7 +231,10 @@ describe("storeBlob / getBlob / getBlobData", () => {
|
|
|
228
231
|
// available but must NOT be used since allowCacheFallback defaults to
|
|
229
232
|
// false (Q25: loam's crash-loud semantics are the default).
|
|
230
233
|
const store = createBlobStore({ dbName: "store-test-no-fallback-default" });
|
|
231
|
-
await expect(store.storeBlob(textBuffer("opfs-less content"), {
|
|
234
|
+
await expect(store.storeBlob(textBuffer("opfs-less content"), {
|
|
235
|
+
filename: "no-opfs.txt",
|
|
236
|
+
mime: "text/plain",
|
|
237
|
+
})).rejects.toThrow(/no bytes backend accepted/);
|
|
232
238
|
});
|
|
233
239
|
});
|
|
234
240
|
describe("storeBlobFromFile", () => {
|
|
@@ -237,17 +243,15 @@ describe("storeBlobFromFile", () => {
|
|
|
237
243
|
const file = new File([textBuffer("small file contents")], "small.txt", { type: "text/plain" });
|
|
238
244
|
const record = await store.storeBlobFromFile(file);
|
|
239
245
|
expect(record.filename).toBe("small.txt");
|
|
240
|
-
expect(record.sha256).toBeTruthy();
|
|
241
246
|
const data = await store.getBlobData(record.blob_id);
|
|
242
247
|
expect(new TextDecoder().decode(data)).toBe("small file contents");
|
|
243
248
|
});
|
|
244
|
-
it("streams large files
|
|
249
|
+
it("streams large files", async () => {
|
|
245
250
|
const store = createBlobStore({ dbName: "store-test-file-large" });
|
|
246
251
|
const bigBytes = new Uint8Array(8 * 1024 * 1024 + 10).fill(7);
|
|
247
252
|
const file = new File([bigBytes], "large.bin", { type: "application/octet-stream" });
|
|
248
253
|
const record = await store.storeBlobFromFile(file);
|
|
249
254
|
expect(record.size).toBe(bigBytes.byteLength);
|
|
250
|
-
expect(record.sha256).toBeUndefined();
|
|
251
255
|
const data = await store.getBlobData(record.blob_id);
|
|
252
256
|
expect(data?.byteLength).toBe(bigBytes.byteLength);
|
|
253
257
|
});
|
|
@@ -261,11 +265,10 @@ describe("storeBlobFromFile", () => {
|
|
|
261
265
|
});
|
|
262
266
|
});
|
|
263
267
|
describe("resolveBlob", () => {
|
|
264
|
-
it("resolves a
|
|
268
|
+
it("resolves a record whose primary key differs from its blake3", async () => {
|
|
265
269
|
await putRecord("store-test-resolve", {
|
|
266
|
-
blob_id: "legacy-
|
|
270
|
+
blob_id: "legacy-key",
|
|
267
271
|
blake3: "blake3-known-later",
|
|
268
|
-
sha256: "legacy-sha256-key",
|
|
269
272
|
filename: "legacy.txt",
|
|
270
273
|
mime: "text/plain",
|
|
271
274
|
size: 4,
|
|
@@ -276,13 +279,16 @@ describe("resolveBlob", () => {
|
|
|
276
279
|
});
|
|
277
280
|
const store = createBlobStore({ dbName: "store-test-resolve" });
|
|
278
281
|
expect(await store.resolveBlob("blake3-known-later")).not.toBeNull();
|
|
279
|
-
expect(await store.resolveBlob("legacy-
|
|
282
|
+
expect(await store.resolveBlob("legacy-key")).not.toBeNull();
|
|
280
283
|
expect(await store.resolveBlob("anything", "blake3-known-later")).not.toBeNull();
|
|
281
284
|
expect(await store.resolveBlob("nothing-matches")).toBeNull();
|
|
282
285
|
});
|
|
283
286
|
it("getBlobMetadata is an alias for resolveBlob", async () => {
|
|
284
287
|
const store = createBlobStore({ dbName: "store-test-metadata-alias" });
|
|
285
|
-
const record = await store.storeBlob(textBuffer("meta"), {
|
|
288
|
+
const record = await store.storeBlob(textBuffer("meta"), {
|
|
289
|
+
filename: "m.txt",
|
|
290
|
+
mime: "text/plain",
|
|
291
|
+
});
|
|
286
292
|
expect(await store.getBlobMetadata(record.blob_id)).toEqual(record);
|
|
287
293
|
});
|
|
288
294
|
});
|
|
@@ -367,8 +373,14 @@ describe("deleteBlob / clearAll", () => {
|
|
|
367
373
|
});
|
|
368
374
|
it("clearAll wipes every record and every backend's bytes", async () => {
|
|
369
375
|
const store = createBlobStore({ dbName: "store-test-clear-all" });
|
|
370
|
-
const a = await store.storeBlob(textBuffer("clear-all-a"), {
|
|
371
|
-
|
|
376
|
+
const a = await store.storeBlob(textBuffer("clear-all-a"), {
|
|
377
|
+
filename: "a.txt",
|
|
378
|
+
mime: "text/plain",
|
|
379
|
+
});
|
|
380
|
+
const b = await store.storeBlob(textBuffer("clear-all-b"), {
|
|
381
|
+
filename: "b.txt",
|
|
382
|
+
mime: "text/plain",
|
|
383
|
+
});
|
|
372
384
|
await store.clearAll();
|
|
373
385
|
expect(await store.getBlobRecord(a.blob_id)).toBeNull();
|
|
374
386
|
expect(await store.getBlobRecord(b.blob_id)).toBeNull();
|
package/dist/blobs/types.d.ts
CHANGED
|
@@ -8,12 +8,8 @@ export interface BlobRecord {
|
|
|
8
8
|
/** canonical id - the blake3 hex digest. */
|
|
9
9
|
blob_id: string;
|
|
10
10
|
/** blake3 hex digest (same value as blob_id for every record created by
|
|
11
|
-
* this store; kept as its own field, and its own index
|
|
12
|
-
* legacy-keyed by sha256 can still be found by blake3 once known). */
|
|
11
|
+
* this store; kept as its own field, and its own index). */
|
|
13
12
|
blake3: string;
|
|
14
|
-
/** legacy sha256 hex digest, kept for records/references that predate
|
|
15
|
-
* blake3 becoming canonical. never computed for new streamed uploads. */
|
|
16
|
-
sha256?: string;
|
|
17
13
|
filename: string;
|
|
18
14
|
mime: string;
|
|
19
15
|
size: number;
|
|
@@ -26,8 +22,8 @@ export interface BlobRecord {
|
|
|
26
22
|
storage_backend?: BytesBackendName;
|
|
27
23
|
}
|
|
28
24
|
/** everything needed to store a new blob, other than the bytes and the
|
|
29
|
-
* fields this store computes/fills in itself (blob_id, blake3,
|
|
30
|
-
*
|
|
25
|
+
* fields this store computes/fills in itself (blob_id, blake3, size,
|
|
26
|
+
* created_at, storage_backend). */
|
|
31
27
|
export interface NewBlobMeta {
|
|
32
28
|
filename: string;
|
|
33
29
|
mime: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/blobs/types.ts"],"names":[],"mappings":"AAQA,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAEzE;;2EAE2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEhD,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/blobs/types.ts"],"names":[],"mappings":"AAQA,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAEzE;;2EAE2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEhD,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB;iEAC6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,QAAQ,CAAC;IACpB;qDACiD;IACjD,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,gBAAgB,CAAC;CACpC;AAED;;oCAEoC;AACpC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,QAAQ,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,QAAQ,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC"}
|
package/dist/blobs/types.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// shared shapes for the blake3-canonical blob record store.
|
|
2
2
|
//
|
|
3
|
-
// core schema: blake3 primary key,
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// core schema: blake3 primary key, blob_type, parent linkage. this
|
|
4
|
+
// browser version omits native-only fields (on-disk path, external flag,
|
|
5
|
+
// soft-delete bookkeeping) and app-specific classification fields (e.g.
|
|
6
|
+
// domain tags) - callers that want classification can use the
|
|
7
|
+
// `metadata` field.
|
|
8
8
|
export {};
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC"}
|
package/dist/utils/index.js
CHANGED
|
@@ -31,10 +31,6 @@ export declare function shutdownBlobWorker(): void;
|
|
|
31
31
|
* underlying buffer.
|
|
32
32
|
*/
|
|
33
33
|
export declare function hashBlake3(data: Uint8Array): Promise<string>;
|
|
34
|
-
/**
|
|
35
|
-
* sha256 hash of an ArrayBuffer.
|
|
36
|
-
*/
|
|
37
|
-
export declare function hashSha256(data: ArrayBuffer): Promise<string>;
|
|
38
34
|
/**
|
|
39
35
|
* base64-encode an ArrayBuffer.
|
|
40
36
|
*
|
|
@@ -50,7 +46,6 @@ export declare function base64Encode(buffer: ArrayBuffer): Promise<string>;
|
|
|
50
46
|
*/
|
|
51
47
|
export declare function processBlobBytes(buffer: ArrayBuffer, filename: string, mime: string): Promise<{
|
|
52
48
|
blob_id: string;
|
|
53
|
-
sha256: string;
|
|
54
49
|
blake3: string;
|
|
55
50
|
size: number;
|
|
56
51
|
mime: string;
|
|
@@ -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;
|
|
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;AAqBtD;;;;;;;;;;;;;;;;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;;;;;;;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,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAcD;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"}
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
// the client falls back to a synchronous in-process implementation so
|
|
9
9
|
// callers don't have to branch.
|
|
10
10
|
import * as Comlink from "comlink";
|
|
11
|
-
import { sha256Hex } from "../utils/hash.js";
|
|
12
11
|
import { loadMiddenBlake3 } from "./midden-blake3.js";
|
|
13
12
|
import { BLOB_WORKER_READY_MESSAGE } from "./blob-worker-logic.js";
|
|
14
13
|
import { log } from "../utils/log.js";
|
|
@@ -149,15 +148,6 @@ export async function hashBlake3(data) {
|
|
|
149
148
|
return worker.hashBlake3(data);
|
|
150
149
|
return fallbackHashBlake3(data);
|
|
151
150
|
}
|
|
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
151
|
/**
|
|
162
152
|
* base64-encode an ArrayBuffer.
|
|
163
153
|
*
|
|
@@ -182,13 +172,9 @@ export async function processBlobBytes(buffer, filename, mime) {
|
|
|
182
172
|
return worker.processBlobBytes(Comlink.transfer(buffer, [buffer]), filename, mime);
|
|
183
173
|
}
|
|
184
174
|
// main-thread fallback path - rare, mostly for tests.
|
|
185
|
-
const
|
|
186
|
-
sha256Hex(buffer),
|
|
187
|
-
fallbackHashBlake3(new Uint8Array(buffer)),
|
|
188
|
-
]);
|
|
175
|
+
const blake3 = await fallbackHashBlake3(new Uint8Array(buffer));
|
|
189
176
|
return {
|
|
190
177
|
blob_id: blake3,
|
|
191
|
-
sha256,
|
|
192
178
|
blake3,
|
|
193
179
|
size: buffer.byteLength,
|
|
194
180
|
mime,
|
|
@@ -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, hashBlake3Streaming,
|
|
3
|
+
import { base64Decode, base64Encode, generateThumbnailDataUrl, getBlobWorker, hashBlake3, hashBlake3Streaming, 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
|
|
@@ -84,13 +84,6 @@ describe("hashBlake3", () => {
|
|
|
84
84
|
expect(await hashBlake3(new Uint8Array([1, 2, 3]))).toBe("");
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
|
-
describe("hashSha256", () => {
|
|
88
|
-
it("falls back to a real sha-256 digest with no worker", async () => {
|
|
89
|
-
const bytes = new TextEncoder().encode("abc");
|
|
90
|
-
const hash = await hashSha256(bytes.buffer);
|
|
91
|
-
expect(hash).toBe("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
87
|
describe("base64Encode / base64Decode", () => {
|
|
95
88
|
it("round-trips bytes through the fallback path", async () => {
|
|
96
89
|
const original = new Uint8Array([0, 1, 2, 253, 254, 255]);
|
|
@@ -100,12 +93,11 @@ describe("base64Encode / base64Decode", () => {
|
|
|
100
93
|
});
|
|
101
94
|
});
|
|
102
95
|
describe("processBlobBytes", () => {
|
|
103
|
-
it("hashes without a worker (blake3 empty
|
|
96
|
+
it("hashes without a worker (blake3 empty)", async () => {
|
|
104
97
|
const bytes = new TextEncoder().encode("hello world");
|
|
105
98
|
const result = await processBlobBytes(bytes.buffer, "hello.txt", "text/plain");
|
|
106
99
|
expect(result.blake3).toBe("");
|
|
107
100
|
expect(result.blob_id).toBe("");
|
|
108
|
-
expect(result.sha256).toBe("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9");
|
|
109
101
|
expect(result.size).toBe(bytes.byteLength);
|
|
110
102
|
});
|
|
111
103
|
});
|
|
@@ -12,12 +12,6 @@ export declare const OPFS_DIR = "reliquary-blobs";
|
|
|
12
12
|
* it does not export `hash_blake3`.
|
|
13
13
|
*/
|
|
14
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
15
|
/**
|
|
22
16
|
* base64-encode an ArrayBuffer. uses chunked btoa to avoid stack overflow
|
|
23
17
|
* on large buffers (`String.fromCharCode(...veryLargeArray)` blows up).
|
|
@@ -42,20 +36,15 @@ export declare function writeBlobToOpfs(blobId: string, data: ArrayBuffer): Prom
|
|
|
42
36
|
export declare function readBlobFromOpfs(blobId: string): Promise<ArrayBuffer | null>;
|
|
43
37
|
export interface ProcessedBlob {
|
|
44
38
|
blob_id: string;
|
|
45
|
-
sha256: string;
|
|
46
39
|
blake3: string;
|
|
47
40
|
size: number;
|
|
48
41
|
mime: string;
|
|
49
42
|
filename: string;
|
|
50
43
|
}
|
|
51
44
|
/**
|
|
52
|
-
* one-shot: hash bytes (
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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.
|
|
45
|
+
* one-shot: hash bytes (blake3), write to OPFS, return metadata. lets
|
|
46
|
+
* callers avoid round-trips across the worker boundary for an upload.
|
|
47
|
+
* `data` should be transferred.
|
|
59
48
|
*/
|
|
60
49
|
export declare function processBlobBytes(data: ArrayBuffer, filename: string, mime: string): Promise<ProcessedBlob>;
|
|
61
50
|
export interface ResizeImageOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blob-worker-logic.d.ts","sourceRoot":"","sources":["../../src/worker/blob-worker-logic.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"blob-worker-logic.d.ts","sourceRoot":"","sources":["../../src/worker/blob-worker-logic.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// blob worker logic - the CPU-bound blob work (blake3 hashing,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// blob worker logic - the CPU-bound blob work (blake3 hashing, base64
|
|
2
|
+
// encode/decode, OPFS writes, thumbnail generation, chunked upload
|
|
3
|
+
// sessions) that `blob-worker.ts` exposes over comlink.
|
|
4
4
|
//
|
|
5
5
|
// kept free of comlink/postMessage side effects on purpose: this module is
|
|
6
6
|
// what the test suite imports directly, and what `blob-worker.ts` (the
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
// degrades to an empty string and the streaming upload/selftest functions
|
|
13
13
|
// throw a clear error, matching the graceful-degradation contract of the
|
|
14
14
|
// rest of this file.
|
|
15
|
-
import { sha256Hex } from "../utils/hash.js";
|
|
16
15
|
import { loadMiddenBlake3 } from "./midden-blake3.js";
|
|
17
16
|
import { log } from "../utils/log.js";
|
|
18
17
|
const TAG = "blob.worker.logic";
|
|
@@ -43,14 +42,6 @@ export async function hashBlake3(data) {
|
|
|
43
42
|
return "";
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
|
-
/**
|
|
47
|
-
* compute sha256 hash via SubtleCrypto. SubtleCrypto.digest is already
|
|
48
|
-
* async/non-blocking on the main thread, but this is exposed here too so
|
|
49
|
-
* callers can do sha256 + blake3 in a single round-trip.
|
|
50
|
-
*/
|
|
51
|
-
export async function hashSha256(data) {
|
|
52
|
-
return sha256Hex(data);
|
|
53
|
-
}
|
|
54
45
|
/**
|
|
55
46
|
* base64-encode an ArrayBuffer. uses chunked btoa to avoid stack overflow
|
|
56
47
|
* on large buffers (`String.fromCharCode(...veryLargeArray)` blows up).
|
|
@@ -140,23 +131,15 @@ export async function readBlobFromOpfs(blobId) {
|
|
|
140
131
|
}
|
|
141
132
|
}
|
|
142
133
|
/**
|
|
143
|
-
* one-shot: hash bytes (
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* blake3 is the canonical blob id (matches iroh-blobs / a native rust
|
|
148
|
-
* store); sha256 is still computed so legacy sha256-keyed records and old
|
|
149
|
-
* doc references keep resolving via the sha256 index.
|
|
134
|
+
* one-shot: hash bytes (blake3), write to OPFS, return metadata. lets
|
|
135
|
+
* callers avoid round-trips across the worker boundary for an upload.
|
|
136
|
+
* `data` should be transferred.
|
|
150
137
|
*/
|
|
151
138
|
export async function processBlobBytes(data, filename, mime) {
|
|
152
|
-
|
|
153
|
-
// copy of the bytes, so we can't transfer-and-reuse - do them in parallel
|
|
154
|
-
// and let the runtime overlap them.
|
|
155
|
-
const [sha256, blake3] = await Promise.all([hashSha256(data), hashBlake3(new Uint8Array(data))]);
|
|
139
|
+
const blake3 = await hashBlake3(new Uint8Array(data));
|
|
156
140
|
await writeBlobToOpfs(blake3, data);
|
|
157
141
|
return {
|
|
158
142
|
blob_id: blake3,
|
|
159
|
-
sha256,
|
|
160
143
|
blake3,
|
|
161
144
|
size: data.byteLength,
|
|
162
145
|
mime,
|
|
@@ -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, hashAbort, hashBegin, hashBlake3, hashFinish, hashPush,
|
|
3
|
+
import { base64Decode, base64Encode, generateThumbnailDataUrl, hashAbort, hashBegin, hashBlake3, hashFinish, hashPush, 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
|
|
@@ -91,13 +91,6 @@ afterEach(() => {
|
|
|
91
91
|
vi.unstubAllGlobals();
|
|
92
92
|
resetMiddenBlake3Cache();
|
|
93
93
|
});
|
|
94
|
-
describe("hashSha256", () => {
|
|
95
|
-
it("hashes a known buffer to its well-known sha-256 digest", async () => {
|
|
96
|
-
const bytes = new TextEncoder().encode("abc");
|
|
97
|
-
const hash = await hashSha256(bytes.buffer);
|
|
98
|
-
expect(hash).toBe("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
94
|
describe("hashBlake3", () => {
|
|
102
95
|
it("degrades to an empty string when no midden module is bundled", async () => {
|
|
103
96
|
const hash = await hashBlake3(new Uint8Array([1, 2, 3]));
|
|
@@ -154,7 +147,6 @@ describe("processBlobBytes", () => {
|
|
|
154
147
|
// to an empty string, and blob_id mirrors it (documented behavior).
|
|
155
148
|
expect(result.blake3).toBe("");
|
|
156
149
|
expect(result.blob_id).toBe(result.blake3);
|
|
157
|
-
expect(result.sha256).toBe("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9");
|
|
158
150
|
});
|
|
159
151
|
});
|
|
160
152
|
describe("uploadBegin", () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { base64Decode, base64Encode, generateThumbnailDataUrl, hashAbort, hashBegin, hashBlake3, hashFinish, hashPush,
|
|
1
|
+
import { base64Decode, base64Encode, generateThumbnailDataUrl, hashAbort, hashBegin, hashBlake3, hashFinish, hashPush, 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
|
-
hashSha256: typeof hashSha256;
|
|
5
4
|
base64Encode: typeof base64Encode;
|
|
6
5
|
base64Decode: typeof base64Decode;
|
|
7
6
|
writeBlobToOpfs: typeof writeBlobToOpfs;
|
|
@@ -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,SAAS,EACT,SAAS,EACT,UAAU,EACV,UAAU,EACV,QAAQ,EACR,
|
|
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,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;;;;;;;;;;;;;;;;;;;CAmBR,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// blob worker entry - moves CPU-bound blob work (blake3 hashing,
|
|
2
|
-
//
|
|
1
|
+
// blob worker entry - moves CPU-bound blob work (blake3 hashing, base64
|
|
2
|
+
// encode/decode, OPFS writes) off the main thread.
|
|
3
3
|
//
|
|
4
4
|
// architecture: comlink-exposed module. spun up lazily by
|
|
5
5
|
// `blob-worker-client.ts` on first use. shares no state with the main
|
|
@@ -12,10 +12,9 @@
|
|
|
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, hashAbort, hashBegin, hashBlake3, hashFinish, hashPush,
|
|
15
|
+
import { BLOB_WORKER_READY_MESSAGE, base64Decode, base64Encode, generateThumbnailDataUrl, hashAbort, hashBegin, hashBlake3, hashFinish, hashPush, opfsStoreSelftest, opfsStoreSelftestPersistence, processBlobBytes, readBlobFromOpfs, resizeImageToWebpDataUrl, uploadAbort, uploadBegin, uploadFinish, uploadPush, writeBlobToOpfs, } from "./blob-worker-logic.js";
|
|
16
16
|
const api = {
|
|
17
17
|
hashBlake3,
|
|
18
|
-
hashSha256,
|
|
19
18
|
base64Encode,
|
|
20
19
|
base64Decode,
|
|
21
20
|
writeBlobToOpfs,
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, hashBlake3Streaming,
|
|
4
|
+
export { getBlobWorker, shutdownBlobWorker, hashBlake3, hashBlake3Streaming, base64Encode, base64Decode, processBlobBytes, writeBlobToOpfs, streamFileToOpfs, resizeImageToWebpDataUrl, generateThumbnailDataUrl, } from "./blob-worker-client.js";
|
|
5
5
|
export type { MiddenWorkerApi, MiddenWorkerIdentity, MiddenWorkerInitOptions, 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";
|
|
@@ -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,mBAAmB,EACnB,
|
|
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,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AAEjC,YAAY,EACV,eAAe,EACf,oBAAoB,EACpB,uBAAuB,EACvB,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,EACL,cAAc,EACd,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,2BAA2B,CAAC;AAEnC,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"}
|
package/dist/worker/index.js
CHANGED
|
@@ -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, hashBlake3Streaming,
|
|
2
|
+
export { getBlobWorker, shutdownBlobWorker, hashBlake3, hashBlake3Streaming, 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, WorkerRadioHandle, } from "./midden-worker-client.js";
|
|
5
5
|
export { MiddenNode } from "./midden-stub.js";
|
package/package.json
CHANGED
package/dist/utils/hash.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hash of an ArrayBuffer (or a view over one) as a lowercase hex string.
|
|
3
|
-
*/
|
|
4
|
-
export declare function sha256Hex(data: ArrayBuffer | ArrayBufferView): Promise<string>;
|
|
5
|
-
/**
|
|
6
|
-
* hash of a File/Blob's contents as a lowercase hex string.
|
|
7
|
-
*/
|
|
8
|
-
export declare function sha256HexOfBlob(blob: Blob): Promise<string>;
|
|
9
|
-
/**
|
|
10
|
-
* true if `data` hashes to `expectedHex` (case-insensitive).
|
|
11
|
-
*/
|
|
12
|
-
export declare function verifySha256Hex(data: ArrayBuffer | ArrayBufferView, expectedHex: string): Promise<boolean>;
|
|
13
|
-
//# sourceMappingURL=hash.d.ts.map
|
package/dist/utils/hash.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../src/utils/hash.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAMpF;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjE;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,WAAW,GAAG,eAAe,EACnC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,OAAO,CAAC,CAGlB"}
|
package/dist/utils/hash.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// sha256 hashing helpers built on the web crypto subtle api. used wherever a
|
|
2
|
-
// content hash is needed but the full blake3-canonical blob pipeline (./blobs,
|
|
3
|
-
// ./worker) is overkill - legacy sha256 references, quick integrity checks, etc.
|
|
4
|
-
/**
|
|
5
|
-
* hash of an ArrayBuffer (or a view over one) as a lowercase hex string.
|
|
6
|
-
*/
|
|
7
|
-
export async function sha256Hex(data) {
|
|
8
|
-
const buffer = ArrayBuffer.isView(data)
|
|
9
|
-
? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
|
|
10
|
-
: data;
|
|
11
|
-
const digest = await crypto.subtle.digest("SHA-256", buffer);
|
|
12
|
-
return bufferToHex(digest);
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* hash of a File/Blob's contents as a lowercase hex string.
|
|
16
|
-
*/
|
|
17
|
-
export async function sha256HexOfBlob(blob) {
|
|
18
|
-
return sha256Hex(await blob.arrayBuffer());
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* true if `data` hashes to `expectedHex` (case-insensitive).
|
|
22
|
-
*/
|
|
23
|
-
export async function verifySha256Hex(data, expectedHex) {
|
|
24
|
-
const actual = await sha256Hex(data);
|
|
25
|
-
return actual === expectedHex.toLowerCase();
|
|
26
|
-
}
|
|
27
|
-
function bufferToHex(buffer) {
|
|
28
|
-
return Array.from(new Uint8Array(buffer))
|
|
29
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
30
|
-
.join("");
|
|
31
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hash.test.d.ts","sourceRoot":"","sources":["../../src/utils/hash.test.ts"],"names":[],"mappings":""}
|
package/dist/utils/hash.test.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { sha256Hex, sha256HexOfBlob, verifySha256Hex } from "./hash.js";
|
|
3
|
-
// known-answer test vectors for sha-256
|
|
4
|
-
const EMPTY_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
5
|
-
const ABC_HASH = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
|
|
6
|
-
function textToBuffer(text) {
|
|
7
|
-
return new TextEncoder().encode(text).buffer;
|
|
8
|
-
}
|
|
9
|
-
describe("sha256Hex", () => {
|
|
10
|
-
it("hashes an empty buffer to the well-known sha-256 empty digest", async () => {
|
|
11
|
-
const hash = await sha256Hex(new ArrayBuffer(0));
|
|
12
|
-
expect(hash).toBe(EMPTY_HASH);
|
|
13
|
-
});
|
|
14
|
-
it("hashes 'abc' to its well-known sha-256 digest", async () => {
|
|
15
|
-
const hash = await sha256Hex(textToBuffer("abc"));
|
|
16
|
-
expect(hash).toBe(ABC_HASH);
|
|
17
|
-
});
|
|
18
|
-
it("accepts an ArrayBufferView (Uint8Array) as well as a raw ArrayBuffer", async () => {
|
|
19
|
-
const bytes = new TextEncoder().encode("abc");
|
|
20
|
-
const hash = await sha256Hex(bytes);
|
|
21
|
-
expect(hash).toBe(ABC_HASH);
|
|
22
|
-
});
|
|
23
|
-
it("only hashes the view's byte range, not the whole underlying buffer", async () => {
|
|
24
|
-
const backing = new Uint8Array(10);
|
|
25
|
-
backing.set(new TextEncoder().encode("abc"), 2);
|
|
26
|
-
const view = new Uint8Array(backing.buffer, 2, 3);
|
|
27
|
-
const hash = await sha256Hex(view);
|
|
28
|
-
expect(hash).toBe(ABC_HASH);
|
|
29
|
-
});
|
|
30
|
-
it("produces different hashes for different inputs", async () => {
|
|
31
|
-
const a = await sha256Hex(textToBuffer("hello"));
|
|
32
|
-
const b = await sha256Hex(textToBuffer("world"));
|
|
33
|
-
expect(a).not.toBe(b);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
describe("sha256HexOfBlob", () => {
|
|
37
|
-
it("hashes a Blob's contents the same as its raw bytes", async () => {
|
|
38
|
-
const blob = new Blob(["abc"]);
|
|
39
|
-
const hash = await sha256HexOfBlob(blob);
|
|
40
|
-
expect(hash).toBe(ABC_HASH);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
describe("verifySha256Hex", () => {
|
|
44
|
-
it("returns true when the expected hash matches", async () => {
|
|
45
|
-
const ok = await verifySha256Hex(textToBuffer("abc"), ABC_HASH);
|
|
46
|
-
expect(ok).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
it("returns false when the expected hash does not match", async () => {
|
|
49
|
-
const ok = await verifySha256Hex(textToBuffer("abc"), EMPTY_HASH);
|
|
50
|
-
expect(ok).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
it("is case-insensitive when comparing the expected hash", async () => {
|
|
53
|
-
const ok = await verifySha256Hex(textToBuffer("abc"), ABC_HASH.toUpperCase());
|
|
54
|
-
expect(ok).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
});
|