@freqhole/reliquary 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/automerge/acl-filtering.d.ts +87 -0
- package/dist/automerge/acl-filtering.d.ts.map +1 -0
- package/dist/automerge/acl-filtering.js +138 -0
- package/dist/automerge/acl-filtering.test.d.ts +2 -0
- package/dist/automerge/acl-filtering.test.d.ts.map +1 -0
- package/dist/automerge/acl-filtering.test.js +302 -0
- package/dist/automerge/index.d.ts +5 -0
- package/dist/automerge/index.d.ts.map +1 -0
- package/dist/automerge/index.js +7 -0
- package/dist/automerge/iroh-network-adapter.d.ts +178 -0
- package/dist/automerge/iroh-network-adapter.d.ts.map +1 -0
- package/dist/automerge/iroh-network-adapter.js +691 -0
- package/dist/automerge/iroh-network-adapter.test.d.ts +2 -0
- package/dist/automerge/iroh-network-adapter.test.d.ts.map +1 -0
- package/dist/automerge/iroh-network-adapter.test.js +818 -0
- package/dist/automerge/types.d.ts +66 -0
- package/dist/automerge/types.d.ts.map +1 -0
- package/dist/automerge/types.js +10 -0
- package/dist/blobs/bytes-backend.d.ts +41 -0
- package/dist/blobs/bytes-backend.d.ts.map +1 -0
- package/dist/blobs/bytes-backend.js +252 -0
- package/dist/blobs/bytes-backend.test.d.ts +2 -0
- package/dist/blobs/bytes-backend.test.d.ts.map +1 -0
- package/dist/blobs/bytes-backend.test.js +241 -0
- package/dist/blobs/db.d.ts +8 -0
- package/dist/blobs/db.d.ts.map +1 -0
- package/dist/blobs/db.js +141 -0
- package/dist/blobs/db.test.d.ts +2 -0
- package/dist/blobs/db.test.d.ts.map +1 -0
- package/dist/blobs/db.test.js +120 -0
- package/dist/blobs/index.d.ts +5 -0
- package/dist/blobs/index.d.ts.map +1 -0
- package/dist/blobs/index.js +2 -0
- package/dist/blobs/store.d.ts +47 -0
- package/dist/blobs/store.d.ts.map +1 -0
- package/dist/blobs/store.js +263 -0
- package/dist/blobs/store.test.d.ts +2 -0
- package/dist/blobs/store.test.d.ts.map +1 -0
- package/dist/blobs/store.test.js +377 -0
- package/dist/blobs/types.d.ts +55 -0
- package/dist/blobs/types.d.ts.map +1 -0
- package/dist/blobs/types.js +8 -0
- package/dist/ensure/client.d.ts +19 -0
- package/dist/ensure/client.d.ts.map +1 -0
- package/dist/ensure/client.js +74 -0
- package/dist/ensure/client.test.d.ts +2 -0
- package/dist/ensure/client.test.d.ts.map +1 -0
- package/dist/ensure/client.test.js +133 -0
- package/dist/ensure/index.d.ts +4 -0
- package/dist/ensure/index.d.ts.map +1 -0
- package/dist/ensure/index.js +4 -0
- package/dist/ensure/responder.d.ts +20 -0
- package/dist/ensure/responder.d.ts.map +1 -0
- package/dist/ensure/responder.js +100 -0
- package/dist/ensure/responder.test.d.ts +2 -0
- package/dist/ensure/responder.test.d.ts.map +1 -0
- package/dist/ensure/responder.test.js +191 -0
- package/dist/ensure/types.d.ts +19 -0
- package/dist/ensure/types.d.ts.map +1 -0
- package/dist/ensure/types.js +4 -0
- package/dist/ensure/types.test.d.ts +2 -0
- package/dist/ensure/types.test.d.ts.map +1 -0
- package/dist/ensure/types.test.js +81 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +7 -0
- package/dist/solid/blob-url.d.ts +10 -0
- package/dist/solid/blob-url.d.ts.map +1 -0
- package/dist/solid/blob-url.js +34 -0
- package/dist/solid/blob-url.test.d.ts +2 -0
- package/dist/solid/blob-url.test.d.ts.map +1 -0
- package/dist/solid/blob-url.test.js +62 -0
- package/dist/solid/connection-summary.d.ts +25 -0
- package/dist/solid/connection-summary.d.ts.map +1 -0
- package/dist/solid/connection-summary.js +29 -0
- package/dist/solid/connection-summary.test.d.ts +2 -0
- package/dist/solid/connection-summary.test.d.ts.map +1 -0
- package/dist/solid/connection-summary.test.js +57 -0
- package/dist/solid/doc-store.d.ts +29 -0
- package/dist/solid/doc-store.d.ts.map +1 -0
- package/dist/solid/doc-store.js +79 -0
- package/dist/solid/doc-store.test.d.ts +2 -0
- package/dist/solid/doc-store.test.d.ts.map +1 -0
- package/dist/solid/doc-store.test.js +234 -0
- package/dist/solid/index.d.ts +4 -0
- package/dist/solid/index.d.ts.map +1 -0
- package/dist/solid/index.js +9 -0
- package/dist/solid/transfer-progress.d.ts +21 -0
- package/dist/solid/transfer-progress.d.ts.map +1 -0
- package/dist/solid/transfer-progress.js +32 -0
- package/dist/solid/transfer-progress.test.d.ts +2 -0
- package/dist/solid/transfer-progress.test.d.ts.map +1 -0
- package/dist/solid/transfer-progress.test.js +76 -0
- package/dist/testing/blob-fixtures.d.ts +19 -0
- package/dist/testing/blob-fixtures.d.ts.map +1 -0
- package/dist/testing/blob-fixtures.js +46 -0
- package/dist/testing/blob-fixtures.test.d.ts +2 -0
- package/dist/testing/blob-fixtures.test.d.ts.map +1 -0
- package/dist/testing/blob-fixtures.test.js +46 -0
- package/dist/testing/idb-harness.d.ts +10 -0
- package/dist/testing/idb-harness.d.ts.map +1 -0
- package/dist/testing/idb-harness.js +17 -0
- package/dist/testing/idb-harness.test.d.ts +2 -0
- package/dist/testing/idb-harness.test.d.ts.map +1 -0
- package/dist/testing/idb-harness.test.js +48 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/media.d.ts +15 -0
- package/dist/testing/media.d.ts.map +1 -0
- package/dist/testing/media.js +37 -0
- package/dist/testing/media.test.d.ts +2 -0
- package/dist/testing/media.test.d.ts.map +1 -0
- package/dist/testing/media.test.js +47 -0
- package/dist/testing/mock-blob-fetch.d.ts +33 -0
- package/dist/testing/mock-blob-fetch.d.ts.map +1 -0
- package/dist/testing/mock-blob-fetch.js +39 -0
- package/dist/testing/mock-blob-fetch.test.d.ts +2 -0
- package/dist/testing/mock-blob-fetch.test.d.ts.map +1 -0
- package/dist/testing/mock-blob-fetch.test.js +65 -0
- package/dist/testing/mock-node.d.ts +36 -0
- package/dist/testing/mock-node.d.ts.map +1 -0
- package/dist/testing/mock-node.js +97 -0
- package/dist/testing/mock-node.test.d.ts +2 -0
- package/dist/testing/mock-node.test.d.ts.map +1 -0
- package/dist/testing/mock-node.test.js +66 -0
- package/dist/testing/streams.d.ts +68 -0
- package/dist/testing/streams.d.ts.map +1 -0
- package/dist/testing/streams.js +99 -0
- package/dist/testing/streams.test.d.ts +2 -0
- package/dist/testing/streams.test.d.ts.map +1 -0
- package/dist/testing/streams.test.js +82 -0
- package/dist/transfer/cancellation.d.ts +8 -0
- package/dist/transfer/cancellation.d.ts.map +1 -0
- package/dist/transfer/cancellation.js +19 -0
- package/dist/transfer/index.d.ts +8 -0
- package/dist/transfer/index.d.ts.map +1 -0
- package/dist/transfer/index.js +4 -0
- package/dist/transfer/prefetch.d.ts +38 -0
- package/dist/transfer/prefetch.d.ts.map +1 -0
- package/dist/transfer/prefetch.js +61 -0
- package/dist/transfer/prefetch.test.d.ts +2 -0
- package/dist/transfer/prefetch.test.d.ts.map +1 -0
- package/dist/transfer/prefetch.test.js +136 -0
- package/dist/transfer/serve.d.ts +66 -0
- package/dist/transfer/serve.d.ts.map +1 -0
- package/dist/transfer/serve.js +126 -0
- package/dist/transfer/serve.test.d.ts +2 -0
- package/dist/transfer/serve.test.d.ts.map +1 -0
- package/dist/transfer/serve.test.js +101 -0
- package/dist/transfer/snatch.d.ts +39 -0
- package/dist/transfer/snatch.d.ts.map +1 -0
- package/dist/transfer/snatch.js +289 -0
- package/dist/transfer/snatch.test.d.ts +2 -0
- package/dist/transfer/snatch.test.d.ts.map +1 -0
- package/dist/transfer/snatch.test.js +337 -0
- package/dist/transfer/types.d.ts +85 -0
- package/dist/transfer/types.d.ts.map +1 -0
- package/dist/transfer/types.js +15 -0
- package/dist/utils/hash.d.ts +13 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +31 -0
- package/dist/utils/hash.test.d.ts +2 -0
- package/dist/utils/hash.test.d.ts.map +1 -0
- package/dist/utils/hash.test.js +56 -0
- package/dist/utils/image-utils.d.ts +28 -0
- package/dist/utils/image-utils.d.ts.map +1 -0
- package/dist/utils/image-utils.js +113 -0
- package/dist/utils/image-utils.test.d.ts +2 -0
- package/dist/utils/image-utils.test.d.ts.map +1 -0
- package/dist/utils/image-utils.test.js +181 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/log.d.ts +22 -0
- package/dist/utils/log.d.ts.map +1 -0
- package/dist/utils/log.js +100 -0
- package/dist/utils/log.test.d.ts +2 -0
- package/dist/utils/log.test.d.ts.map +1 -0
- package/dist/utils/log.test.js +113 -0
- package/dist/worker/blob-worker-client.d.ts +118 -0
- package/dist/worker/blob-worker-client.d.ts.map +1 -0
- package/dist/worker/blob-worker-client.js +361 -0
- package/dist/worker/blob-worker-client.test.d.ts +2 -0
- package/dist/worker/blob-worker-client.test.d.ts.map +1 -0
- package/dist/worker/blob-worker-client.test.js +137 -0
- package/dist/worker/blob-worker-logic.d.ts +120 -0
- package/dist/worker/blob-worker-logic.d.ts.map +1 -0
- package/dist/worker/blob-worker-logic.js +368 -0
- package/dist/worker/blob-worker-logic.test.d.ts +2 -0
- package/dist/worker/blob-worker-logic.test.d.ts.map +1 -0
- package/dist/worker/blob-worker-logic.test.js +191 -0
- package/dist/worker/blob-worker.d.ts +21 -0
- package/dist/worker/blob-worker.d.ts.map +1 -0
- package/dist/worker/blob-worker.js +44 -0
- package/dist/worker/index.d.ts +12 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +6 -0
- package/dist/worker/midden-blake3.d.ts +23 -0
- package/dist/worker/midden-blake3.d.ts.map +1 -0
- package/dist/worker/midden-blake3.js +47 -0
- package/dist/worker/midden-blake3.test.d.ts +2 -0
- package/dist/worker/midden-blake3.test.d.ts.map +1 -0
- package/dist/worker/midden-blake3.test.js +25 -0
- package/dist/worker/midden-not-bundled.stub.d.ts +1 -0
- package/dist/worker/midden-not-bundled.stub.d.ts.map +1 -0
- package/dist/worker/midden-not-bundled.stub.js +10 -0
- package/dist/worker/midden-stub.d.ts +10 -0
- package/dist/worker/midden-stub.d.ts.map +1 -0
- package/dist/worker/midden-stub.js +31 -0
- package/dist/worker/midden-stub.test.d.ts +2 -0
- package/dist/worker/midden-stub.test.d.ts.map +1 -0
- package/dist/worker/midden-stub.test.js +25 -0
- package/dist/worker/midden-worker-client.d.ts +103 -0
- package/dist/worker/midden-worker-client.d.ts.map +1 -0
- package/dist/worker/midden-worker-client.js +246 -0
- package/dist/worker/midden-worker-client.test.d.ts +2 -0
- package/dist/worker/midden-worker-client.test.d.ts.map +1 -0
- package/dist/worker/midden-worker-client.test.js +151 -0
- package/dist/worker/midden-worker-contract.d.ts +87 -0
- package/dist/worker/midden-worker-contract.d.ts.map +1 -0
- package/dist/worker/midden-worker-contract.js +30 -0
- package/package.json +90 -0
package/dist/blobs/db.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// the metadata side of the resolver chain: an IndexedDB-backed record
|
|
2
|
+
// store keyed by blob_id (the blake3 hex digest), with secondary indexes
|
|
3
|
+
// so a record can also be found by its legacy sha256, by blake3 alone
|
|
4
|
+
// (covering a record whose primary key is a legacy sha256), by blob_type,
|
|
5
|
+
// or by parent_blob_id.
|
|
6
|
+
//
|
|
7
|
+
// follows a "open fresh, close when done" pattern for every operation
|
|
8
|
+
// rather than holding one long-lived connection open - simple, and avoids
|
|
9
|
+
// a version-change block from another tab ever having anything to wait on.
|
|
10
|
+
const STORE_NAME = "blobs";
|
|
11
|
+
const DB_VERSION = 1;
|
|
12
|
+
function openVersioned(dbName, version) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const req = indexedDB.open(dbName, version);
|
|
15
|
+
req.onupgradeneeded = () => {
|
|
16
|
+
const db = req.result;
|
|
17
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
18
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "blob_id" });
|
|
19
|
+
store.createIndex("sha256", "sha256", { unique: false });
|
|
20
|
+
store.createIndex("blake3", "blake3", { unique: false });
|
|
21
|
+
store.createIndex("blob_type", "blob_type", { unique: false });
|
|
22
|
+
store.createIndex("parent_blob_id", "parent_blob_id", { unique: false });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
req.onsuccess = () => resolve(req.result);
|
|
26
|
+
req.onerror = () => reject(req.error);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// a dbName passed to `createBlobStore` is often an app's pre-existing
|
|
30
|
+
// database (that's the whole point of making it a constructor parameter -
|
|
31
|
+
// see store.ts) - it may already sit at a version higher than
|
|
32
|
+
// `DB_VERSION`, and `indexedDB.open(name, version)` fails with a
|
|
33
|
+
// VersionError when asked for a version lower than the database's actual
|
|
34
|
+
// one. resolved versions are cached per dbName so the common case (a
|
|
35
|
+
// fresh database, or one already at `DB_VERSION`) only ever pays for one
|
|
36
|
+
// open per operation, matching the "open fresh, close when done" pattern
|
|
37
|
+
// used throughout this module.
|
|
38
|
+
const resolvedVersions = new Map();
|
|
39
|
+
async function openDb(dbName) {
|
|
40
|
+
const cachedVersion = resolvedVersions.get(dbName);
|
|
41
|
+
if (cachedVersion !== undefined) {
|
|
42
|
+
return openVersioned(dbName, cachedVersion);
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const db = await openVersioned(dbName, DB_VERSION);
|
|
46
|
+
resolvedVersions.set(dbName, DB_VERSION);
|
|
47
|
+
return db;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (!(err instanceof DOMException) || err.name !== "VersionError")
|
|
51
|
+
throw err;
|
|
52
|
+
// the database already exists at some higher version - open it
|
|
53
|
+
// version-less to discover what that is, then reopen at that exact
|
|
54
|
+
// version (creating the store if a pre-existing database somehow
|
|
55
|
+
// never had one).
|
|
56
|
+
const probe = await new Promise((resolve, reject) => {
|
|
57
|
+
const req = indexedDB.open(dbName);
|
|
58
|
+
req.onsuccess = () => resolve(req.result);
|
|
59
|
+
req.onerror = () => reject(req.error);
|
|
60
|
+
});
|
|
61
|
+
const currentVersion = probe.version;
|
|
62
|
+
const hasStore = probe.objectStoreNames.contains(STORE_NAME);
|
|
63
|
+
probe.close();
|
|
64
|
+
const targetVersion = hasStore ? currentVersion : currentVersion + 1;
|
|
65
|
+
resolvedVersions.set(dbName, targetVersion);
|
|
66
|
+
return openVersioned(dbName, targetVersion);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function putRecord(dbName, record) {
|
|
70
|
+
const db = await openDb(dbName);
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
73
|
+
tx.objectStore(STORE_NAME).put(record);
|
|
74
|
+
tx.oncomplete = () => {
|
|
75
|
+
db.close();
|
|
76
|
+
resolve();
|
|
77
|
+
};
|
|
78
|
+
tx.onerror = () => {
|
|
79
|
+
db.close();
|
|
80
|
+
reject(tx.error);
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export async function getRecord(dbName, blobId) {
|
|
85
|
+
const db = await openDb(dbName);
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
88
|
+
const req = tx.objectStore(STORE_NAME).get(blobId);
|
|
89
|
+
req.onsuccess = () => resolve(req.result ?? null);
|
|
90
|
+
req.onerror = () => reject(req.error);
|
|
91
|
+
tx.oncomplete = () => db.close();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function getByIndex(dbName, indexName, value) {
|
|
95
|
+
if (!value)
|
|
96
|
+
return null;
|
|
97
|
+
const db = await openDb(dbName);
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
100
|
+
const req = tx.objectStore(STORE_NAME).index(indexName).get(value);
|
|
101
|
+
req.onsuccess = () => resolve(req.result ?? null);
|
|
102
|
+
req.onerror = () => reject(req.error);
|
|
103
|
+
tx.oncomplete = () => db.close();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export function getRecordBySha256(dbName, sha256) {
|
|
107
|
+
return getByIndex(dbName, "sha256", sha256);
|
|
108
|
+
}
|
|
109
|
+
export function getRecordByBlake3(dbName, blake3) {
|
|
110
|
+
return getByIndex(dbName, "blake3", blake3);
|
|
111
|
+
}
|
|
112
|
+
export async function deleteRecord(dbName, blobId) {
|
|
113
|
+
const db = await openDb(dbName);
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
116
|
+
tx.objectStore(STORE_NAME).delete(blobId);
|
|
117
|
+
tx.oncomplete = () => {
|
|
118
|
+
db.close();
|
|
119
|
+
resolve();
|
|
120
|
+
};
|
|
121
|
+
tx.onerror = () => {
|
|
122
|
+
db.close();
|
|
123
|
+
reject(tx.error);
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
export async function clearRecords(dbName) {
|
|
128
|
+
const db = await openDb(dbName);
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
131
|
+
tx.objectStore(STORE_NAME).clear();
|
|
132
|
+
tx.oncomplete = () => {
|
|
133
|
+
db.close();
|
|
134
|
+
resolve();
|
|
135
|
+
};
|
|
136
|
+
tx.onerror = () => {
|
|
137
|
+
db.close();
|
|
138
|
+
reject(tx.error);
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.test.d.ts","sourceRoot":"","sources":["../../src/blobs/db.test.ts"],"names":[],"mappings":"AAGA,OAAO,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// exercises the metadata layer directly: CRUD + secondary indexes
|
|
2
|
+
// (sha256, blake3) that make the record resolver chain possible.
|
|
3
|
+
import "fake-indexeddb/auto";
|
|
4
|
+
import { IDBFactory } from "fake-indexeddb";
|
|
5
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { clearRecords, deleteRecord, getRecord, getRecordByBlake3, getRecordBySha256, putRecord, } from "./db.js";
|
|
7
|
+
const DB_NAME = "blobs-db-test";
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
globalThis.indexedDB = new IDBFactory();
|
|
10
|
+
});
|
|
11
|
+
function makeRecord(overrides = {}) {
|
|
12
|
+
return {
|
|
13
|
+
blob_id: "blake3-aaa",
|
|
14
|
+
blake3: "blake3-aaa",
|
|
15
|
+
sha256: "sha256-aaa",
|
|
16
|
+
filename: "song.mp3",
|
|
17
|
+
mime: "audio/mpeg",
|
|
18
|
+
size: 1234,
|
|
19
|
+
blob_type: "original",
|
|
20
|
+
parent_blob_id: null,
|
|
21
|
+
created_at: 1_000,
|
|
22
|
+
storage_backend: "opfs",
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
describe("putRecord / getRecord", () => {
|
|
27
|
+
it("round-trips a record by its primary key", async () => {
|
|
28
|
+
const record = makeRecord();
|
|
29
|
+
await putRecord(DB_NAME, record);
|
|
30
|
+
expect(await getRecord(DB_NAME, record.blob_id)).toEqual(record);
|
|
31
|
+
});
|
|
32
|
+
it("returns null for a record that was never stored", async () => {
|
|
33
|
+
expect(await getRecord(DB_NAME, "nope")).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
it("overwrites a record stored under the same blob_id", async () => {
|
|
36
|
+
await putRecord(DB_NAME, makeRecord({ filename: "first.mp3" }));
|
|
37
|
+
await putRecord(DB_NAME, makeRecord({ filename: "second.mp3" }));
|
|
38
|
+
const record = await getRecord(DB_NAME, "blake3-aaa");
|
|
39
|
+
expect(record?.filename).toBe("second.mp3");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("getRecordBySha256 / getRecordByBlake3", () => {
|
|
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
|
+
});
|
|
48
|
+
it("finds a record by its blake3 index", async () => {
|
|
49
|
+
const record = makeRecord();
|
|
50
|
+
await putRecord(DB_NAME, record);
|
|
51
|
+
expect(await getRecordByBlake3(DB_NAME, "blake3-aaa")).toEqual(record);
|
|
52
|
+
});
|
|
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
|
+
it("returns null for an empty hash rather than matching everything", async () => {
|
|
66
|
+
await putRecord(DB_NAME, makeRecord());
|
|
67
|
+
expect(await getRecordBySha256(DB_NAME, "")).toBeNull();
|
|
68
|
+
expect(await getRecordByBlake3(DB_NAME, "")).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe("deleteRecord", () => {
|
|
72
|
+
it("removes a record so subsequent lookups miss", async () => {
|
|
73
|
+
await putRecord(DB_NAME, makeRecord());
|
|
74
|
+
await deleteRecord(DB_NAME, "blake3-aaa");
|
|
75
|
+
expect(await getRecord(DB_NAME, "blake3-aaa")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe("clearRecords", () => {
|
|
79
|
+
it("empties every record in the store", async () => {
|
|
80
|
+
await putRecord(DB_NAME, makeRecord({ blob_id: "a", blake3: "a" }));
|
|
81
|
+
await putRecord(DB_NAME, makeRecord({ blob_id: "b", blake3: "b" }));
|
|
82
|
+
await clearRecords(DB_NAME);
|
|
83
|
+
expect(await getRecord(DB_NAME, "a")).toBeNull();
|
|
84
|
+
expect(await getRecord(DB_NAME, "b")).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("db name isolation", () => {
|
|
88
|
+
it("keeps records in different-named databases from colliding", async () => {
|
|
89
|
+
await putRecord("db-one", makeRecord());
|
|
90
|
+
expect(await getRecord("db-two", "blake3-aaa")).toBeNull();
|
|
91
|
+
expect(await getRecord("db-one", "blake3-aaa")).not.toBeNull();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("pre-existing database at a higher version", () => {
|
|
95
|
+
it("still reads and writes when dbName already exists at a version above DB_VERSION", async () => {
|
|
96
|
+
// simulates an app's pre-existing database (this module's whole
|
|
97
|
+
// reason for taking dbName as a parameter) that was already upgraded
|
|
98
|
+
// to a later version by that app's own code before this store ever
|
|
99
|
+
// touched it - indexedDB.open(name, 1) would otherwise fail with a
|
|
100
|
+
// VersionError since 1 is lower than the database's actual version.
|
|
101
|
+
const dbName = "pre-existing-higher-version-db";
|
|
102
|
+
await new Promise((resolve, reject) => {
|
|
103
|
+
const req = indexedDB.open(dbName, 3);
|
|
104
|
+
req.onupgradeneeded = () => {
|
|
105
|
+
const store = req.result.createObjectStore("blobs", { keyPath: "blob_id" });
|
|
106
|
+
store.createIndex("sha256", "sha256", { unique: false });
|
|
107
|
+
store.createIndex("blake3", "blake3", { unique: false });
|
|
108
|
+
};
|
|
109
|
+
req.onsuccess = () => {
|
|
110
|
+
req.result.close();
|
|
111
|
+
resolve();
|
|
112
|
+
};
|
|
113
|
+
req.onerror = () => reject(req.error);
|
|
114
|
+
});
|
|
115
|
+
const record = makeRecord();
|
|
116
|
+
await putRecord(dbName, record);
|
|
117
|
+
expect(await getRecord(dbName, record.blob_id)).toEqual(record);
|
|
118
|
+
expect(await getRecordByBlake3(dbName, record.blake3)).toEqual(record);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createBlobStore, isOPFSSupported, DEFAULT_DB_NAME, writeThroughChain, readThroughChain, hasBytesInChain, removeFromChain, } from "./store.js";
|
|
2
|
+
export type { BlobStore, BlobStoreOptions, StoreBlobFromFileOptions, BytesBackend, } from "./store.js";
|
|
3
|
+
export type { BlobLocalityInfo, BlobLocalityMetadata, BlobRecord, BlobType, BytesBackendName, NewBlobMeta, } from "./types.js";
|
|
4
|
+
export { createOpfsBackend, createCacheBackend, defaultBytesChain } from "./bytes-backend.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/blobs/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,SAAS,EACT,gBAAgB,EAChB,wBAAwB,EACxB,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,UAAU,EACV,QAAQ,EACR,gBAAgB,EAChB,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { BlobLocalityInfo, BlobRecord, NewBlobMeta } from "./types.js";
|
|
2
|
+
export { isOPFSSupported } from "./bytes-backend.js";
|
|
3
|
+
export type { BlobLocalityInfo, BlobLocalityMetadata, BlobRecord, BlobType, BytesBackendName, NewBlobMeta } from "./types.js";
|
|
4
|
+
/** default IndexedDB database name for apps that don't need to preserve an
|
|
5
|
+
* existing one. */
|
|
6
|
+
export declare const DEFAULT_DB_NAME = "reliquary-blobs";
|
|
7
|
+
export interface BlobStoreOptions {
|
|
8
|
+
/** IndexedDB database name for this store's metadata. defaults to
|
|
9
|
+
* `DEFAULT_DB_NAME`; pass an app's existing database name to keep
|
|
10
|
+
* reading/writing the same browser data it already has. */
|
|
11
|
+
dbName?: string;
|
|
12
|
+
/** when true, a write falls back to the Cache API if OPFS is
|
|
13
|
+
* unavailable or refuses the write (e.g. the webkitgtk quirk where
|
|
14
|
+
* OPFS writes from the main thread are rejected). defaults to false:
|
|
15
|
+
* a write that OPFS refuses throws instead of silently landing in the
|
|
16
|
+
* cache, since a record whose bytes silently ended up somewhere the
|
|
17
|
+
* caller didn't expect is worse than a loud failure the caller can
|
|
18
|
+
* retry or surface. */
|
|
19
|
+
allowCacheFallback?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface StoreBlobFromFileOptions {
|
|
22
|
+
onProgress?: (fraction: number) => void;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
export interface BlobStore {
|
|
26
|
+
storeBlob(data: ArrayBuffer, meta: NewBlobMeta): Promise<BlobRecord>;
|
|
27
|
+
storeBlobFromFile(file: File, meta?: Partial<NewBlobMeta>, options?: StoreBlobFromFileOptions): Promise<BlobRecord>;
|
|
28
|
+
getBlobRecord(blobId: string): Promise<BlobRecord | null>;
|
|
29
|
+
getBlobRecordByBlake3(blake3: string): Promise<BlobRecord | null>;
|
|
30
|
+
getBlobRecordBySha256(sha256: string): Promise<BlobRecord | null>;
|
|
31
|
+
resolveBlob(blobId: string, blake3?: string): Promise<BlobRecord | null>;
|
|
32
|
+
getBlobMetadata(blobId: string, blake3?: string): Promise<BlobRecord | null>;
|
|
33
|
+
getBlobData(blobId: string, blake3?: string): Promise<ArrayBuffer | null>;
|
|
34
|
+
getBlob(blobId: string, blake3?: string): Promise<Blob | null>;
|
|
35
|
+
getBlobObjectURL(blobId: string, blake3?: string): Promise<string | null>;
|
|
36
|
+
clearBlobUrlCache(): void;
|
|
37
|
+
hasBlobBytes(blobId: string): Promise<boolean>;
|
|
38
|
+
checkBlobLocality(blobId: string, blake3?: string): Promise<BlobLocalityInfo>;
|
|
39
|
+
deleteBlob(blobId: string): Promise<void>;
|
|
40
|
+
clearAll(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/** create a blob store instance. each instance owns its own object-url
|
|
43
|
+
* cache; construct one per app (not per component/request). */
|
|
44
|
+
export declare function createBlobStore(options?: BlobStoreOptions): BlobStore;
|
|
45
|
+
export { writeThroughChain, readThroughChain, hasBytesInChain, removeFromChain } from "./bytes-backend.js";
|
|
46
|
+
export type { BytesBackend } from "./bytes-backend.js";
|
|
47
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/blobs/store.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,UAAU,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9H;oBACoB;AACpB,eAAO,MAAM,eAAe,oBAAoB,CAAC;AAOjD,MAAM,WAAW,gBAAgB;IAC/B;;gEAE4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;4BAMwB;IACxB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACrE,iBAAiB,CACf,IAAI,EAAE,IAAI,EACV,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,EAC3B,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,UAAU,CAAC,CAAC;IACvB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC1D,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAClE,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAClE,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACzE,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC7E,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC1E,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC/D,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1E,iBAAiB,IAAI,IAAI,CAAC;IAC1B,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC9E,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;gEACgE;AAChE,wBAAgB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS,CAiPzE;AAKD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC3G,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// the blake3-canonical blob record store: ties the metadata side (db.ts,
|
|
2
|
+
// IndexedDB) to the bytes side (bytes-backend.ts, OPFS + Cache API) behind
|
|
3
|
+
// one API, and implements the resolver chain that lets old sha256-era
|
|
4
|
+
// references keep working alongside new blake3-keyed records.
|
|
5
|
+
//
|
|
6
|
+
// "resolver chain" here means two related lookup chains:
|
|
7
|
+
// - record resolution (`resolveBlob`): primary key -> blake3 index ->
|
|
8
|
+
// sha256 index -> blake3 index again with a separately-known hash.
|
|
9
|
+
// this is what lets a caller pass either a blake3 or a legacy sha256
|
|
10
|
+
// and get the same record back.
|
|
11
|
+
// - bytes resolution (`bytes-backend.ts`'s `readThroughChain`): a
|
|
12
|
+
// record's `storage_backend` field says where its bytes were written;
|
|
13
|
+
// when that's unknown (a record predating the field) every backend is
|
|
14
|
+
// probed in order (OPFS, then Cache API).
|
|
15
|
+
//
|
|
16
|
+
// db name is a constructor parameter (`createBlobStore({ dbName })`) so
|
|
17
|
+
// each consuming app can keep its own existing IndexedDB database name -
|
|
18
|
+
// no browser-data migration is forced by adopting this package. bytes
|
|
19
|
+
// storage (OPFS directory, Cache API cache) is content-addressed by
|
|
20
|
+
// blake3 and shared at the origin regardless of which app wrote it, so it
|
|
21
|
+
// is not parameterized per store instance - see `bytes-backend.ts`.
|
|
22
|
+
import { hashBlake3, hashSha256, streamFileToOpfs } from "../worker/index.js";
|
|
23
|
+
import { clearRecords, deleteRecord, getRecord, getRecordByBlake3, getRecordBySha256, putRecord, } from "./db.js";
|
|
24
|
+
import { createOpfsBackend, defaultBytesChain, hasBytesInChain, readThroughChain, removeFromChain, writeThroughChain, } from "./bytes-backend.js";
|
|
25
|
+
export { isOPFSSupported } from "./bytes-backend.js";
|
|
26
|
+
/** default IndexedDB database name for apps that don't need to preserve an
|
|
27
|
+
* existing one. */
|
|
28
|
+
export const DEFAULT_DB_NAME = "reliquary-blobs";
|
|
29
|
+
/** files at or above this size stream into OPFS chunk-by-chunk (incremental
|
|
30
|
+
* blake3 + sync-access-handle writes via the blob worker) instead of being
|
|
31
|
+
* buffered whole for the one-shot hash+write pipeline. */
|
|
32
|
+
const STREAM_UPLOAD_THRESHOLD = 8 * 1024 * 1024;
|
|
33
|
+
/** create a blob store instance. each instance owns its own object-url
|
|
34
|
+
* cache; construct one per app (not per component/request). */
|
|
35
|
+
export function createBlobStore(options = {}) {
|
|
36
|
+
const dbName = options.dbName ?? DEFAULT_DB_NAME;
|
|
37
|
+
const chain = options.allowCacheFallback ? defaultBytesChain() : [createOpfsBackend()];
|
|
38
|
+
const blobUrlCache = new Map();
|
|
39
|
+
let beforeUnloadRegistered = false;
|
|
40
|
+
function ensureBeforeUnloadListener() {
|
|
41
|
+
if (beforeUnloadRegistered || typeof window === "undefined")
|
|
42
|
+
return;
|
|
43
|
+
window.addEventListener("beforeunload", () => {
|
|
44
|
+
for (const url of blobUrlCache.values())
|
|
45
|
+
URL.revokeObjectURL(url);
|
|
46
|
+
blobUrlCache.clear();
|
|
47
|
+
});
|
|
48
|
+
beforeUnloadRegistered = true;
|
|
49
|
+
}
|
|
50
|
+
async function storeBlob(data, meta) {
|
|
51
|
+
// hash before writing anything - both hashers copy their input rather
|
|
52
|
+
// than transferring it, so `data` is still intact for the write below.
|
|
53
|
+
const [blake3, sha256] = await Promise.all([hashBlake3(new Uint8Array(data)), hashSha256(data)]);
|
|
54
|
+
const blobId = blake3;
|
|
55
|
+
// dedup - content-addressed, so a pre-existing record for this blake3
|
|
56
|
+
// means the bytes are already stored under it.
|
|
57
|
+
const existing = await getRecord(dbName, blobId);
|
|
58
|
+
if (existing)
|
|
59
|
+
return existing;
|
|
60
|
+
// opfs primary, cache-api fallback for opfs-less environments. only a
|
|
61
|
+
// total failure (neither backend accepted the write) throws - a record
|
|
62
|
+
// with bytes nowhere is worse than no record at all, but a successful
|
|
63
|
+
// cache-api write is not a failure.
|
|
64
|
+
const backend = await writeThroughChain(chain, blobId, data, meta.mime);
|
|
65
|
+
if (!backend) {
|
|
66
|
+
throw new Error(`blob write failed for ${blobId.slice(0, 16)}... - no bytes backend accepted it`);
|
|
67
|
+
}
|
|
68
|
+
const record = {
|
|
69
|
+
blob_id: blobId,
|
|
70
|
+
blake3,
|
|
71
|
+
sha256,
|
|
72
|
+
filename: meta.filename,
|
|
73
|
+
mime: meta.mime,
|
|
74
|
+
size: data.byteLength,
|
|
75
|
+
blob_type: meta.blob_type ?? "original",
|
|
76
|
+
parent_blob_id: meta.parent_blob_id ?? null,
|
|
77
|
+
metadata: meta.metadata,
|
|
78
|
+
created_at: Date.now(),
|
|
79
|
+
storage_backend: backend,
|
|
80
|
+
};
|
|
81
|
+
await putRecord(dbName, record);
|
|
82
|
+
return record;
|
|
83
|
+
}
|
|
84
|
+
async function storeBlobFromFile(file, meta = {}, options) {
|
|
85
|
+
const mime = meta.mime ?? file.type ?? "application/octet-stream";
|
|
86
|
+
const filename = meta.filename ?? file.name;
|
|
87
|
+
if (file.size >= STREAM_UPLOAD_THRESHOLD) {
|
|
88
|
+
try {
|
|
89
|
+
const { blake3, size } = await streamFileToOpfs(file, options);
|
|
90
|
+
const existing = await getRecord(dbName, blake3);
|
|
91
|
+
if (existing)
|
|
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
|
+
const record = {
|
|
97
|
+
blob_id: blake3,
|
|
98
|
+
blake3,
|
|
99
|
+
filename,
|
|
100
|
+
mime,
|
|
101
|
+
size,
|
|
102
|
+
blob_type: meta.blob_type ?? "original",
|
|
103
|
+
parent_blob_id: meta.parent_blob_id ?? null,
|
|
104
|
+
metadata: meta.metadata,
|
|
105
|
+
created_at: Date.now(),
|
|
106
|
+
storage_backend: "opfs",
|
|
107
|
+
};
|
|
108
|
+
await putRecord(dbName, record);
|
|
109
|
+
return record;
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
// a deliberate cancel must propagate, not silently fall back to
|
|
113
|
+
// buffering the whole file.
|
|
114
|
+
if (err instanceof DOMException && err.name === "AbortError")
|
|
115
|
+
throw err;
|
|
116
|
+
// otherwise fall through to the buffered path below (e.g. no
|
|
117
|
+
// worker, no OPFS sync-access-handle support in this browser).
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (options?.signal?.aborted) {
|
|
121
|
+
throw new DOMException("upload cancelled", "AbortError");
|
|
122
|
+
}
|
|
123
|
+
const buffer = await file.arrayBuffer();
|
|
124
|
+
return storeBlob(buffer, { filename, mime, ...meta });
|
|
125
|
+
}
|
|
126
|
+
function getBlobRecord(blobId) {
|
|
127
|
+
return getRecord(dbName, blobId);
|
|
128
|
+
}
|
|
129
|
+
function getBlobRecordByBlake3(blake3) {
|
|
130
|
+
return getRecordByBlake3(dbName, blake3);
|
|
131
|
+
}
|
|
132
|
+
function getBlobRecordBySha256(sha256) {
|
|
133
|
+
return getRecordBySha256(dbName, sha256);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* resolve a blobId to a record using multiple lookup strategies: primary
|
|
137
|
+
* key -> blake3 index (blobId itself) -> sha256 index -> blake3 index
|
|
138
|
+
* (separately-known hash). handles both id generations - blake3 is
|
|
139
|
+
* canonical now, sha256-keyed records are legacy but must keep
|
|
140
|
+
* resolving.
|
|
141
|
+
*/
|
|
142
|
+
async function resolveBlob(blobId, blake3) {
|
|
143
|
+
if (!blobId)
|
|
144
|
+
return null;
|
|
145
|
+
const byKey = await getRecord(dbName, blobId);
|
|
146
|
+
if (byKey)
|
|
147
|
+
return byKey;
|
|
148
|
+
const byIdAsBlake3 = await getRecordByBlake3(dbName, blobId);
|
|
149
|
+
if (byIdAsBlake3)
|
|
150
|
+
return byIdAsBlake3;
|
|
151
|
+
const bySha = await getRecordBySha256(dbName, blobId);
|
|
152
|
+
if (bySha)
|
|
153
|
+
return bySha;
|
|
154
|
+
if (blake3) {
|
|
155
|
+
const byBlake3 = await getRecordByBlake3(dbName, blake3);
|
|
156
|
+
if (byBlake3)
|
|
157
|
+
return byBlake3;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
async function getBlobData(blobId, blake3) {
|
|
162
|
+
const record = await resolveBlob(blobId, blake3);
|
|
163
|
+
if (!record)
|
|
164
|
+
return null;
|
|
165
|
+
return readThroughChain(chain, record.blob_id, record.storage_backend);
|
|
166
|
+
}
|
|
167
|
+
async function getBlob(blobId, blake3) {
|
|
168
|
+
const record = await resolveBlob(blobId, blake3);
|
|
169
|
+
if (!record)
|
|
170
|
+
return null;
|
|
171
|
+
const data = await readThroughChain(chain, record.blob_id, record.storage_backend);
|
|
172
|
+
if (!data)
|
|
173
|
+
return null;
|
|
174
|
+
return new Blob([data], { type: record.mime || "application/octet-stream" });
|
|
175
|
+
}
|
|
176
|
+
async function getBlobObjectURL(blobId, blake3) {
|
|
177
|
+
ensureBeforeUnloadListener();
|
|
178
|
+
const cached = blobUrlCache.get(blobId);
|
|
179
|
+
if (cached)
|
|
180
|
+
return cached;
|
|
181
|
+
const blob = await getBlob(blobId, blake3);
|
|
182
|
+
if (!blob)
|
|
183
|
+
return null;
|
|
184
|
+
const url = URL.createObjectURL(blob);
|
|
185
|
+
blobUrlCache.set(blobId, url);
|
|
186
|
+
return url;
|
|
187
|
+
}
|
|
188
|
+
function clearBlobUrlCache() {
|
|
189
|
+
for (const url of blobUrlCache.values())
|
|
190
|
+
URL.revokeObjectURL(url);
|
|
191
|
+
blobUrlCache.clear();
|
|
192
|
+
}
|
|
193
|
+
async function hasBlobBytes(blobId) {
|
|
194
|
+
const record = await resolveBlob(blobId);
|
|
195
|
+
if (!record)
|
|
196
|
+
return false;
|
|
197
|
+
return hasBytesInChain(chain, record.blob_id, record.storage_backend);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* check whether a blob is available locally. a record without bytes
|
|
201
|
+
* (e.g. a write whose bytes backend silently failed, or a record synced
|
|
202
|
+
* in from a peer before its bytes arrived) counts as "remote" rather
|
|
203
|
+
* than a stuck "local" - that stranded state previously left playback
|
|
204
|
+
* finding nothing with no re-snatch ever offered.
|
|
205
|
+
*/
|
|
206
|
+
async function checkBlobLocality(blobId, blake3) {
|
|
207
|
+
if (!blobId)
|
|
208
|
+
return { locality: "unknown" };
|
|
209
|
+
const record = await resolveBlob(blobId, blake3);
|
|
210
|
+
if (!record)
|
|
211
|
+
return { locality: "remote" };
|
|
212
|
+
const bytesPresent = await hasBytesInChain(chain, record.blob_id, record.storage_backend);
|
|
213
|
+
if (!bytesPresent)
|
|
214
|
+
return { locality: "remote" };
|
|
215
|
+
return {
|
|
216
|
+
locality: "local",
|
|
217
|
+
metadata: {
|
|
218
|
+
id: record.blob_id,
|
|
219
|
+
mime: record.mime || undefined,
|
|
220
|
+
filename: record.filename || undefined,
|
|
221
|
+
size: record.size || undefined,
|
|
222
|
+
blake3: record.blake3 || undefined,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async function deleteBlob(blobId) {
|
|
227
|
+
const record = await getRecord(dbName, blobId);
|
|
228
|
+
if (record)
|
|
229
|
+
await removeFromChain(chain, record.blob_id);
|
|
230
|
+
const cachedUrl = blobUrlCache.get(blobId);
|
|
231
|
+
if (cachedUrl) {
|
|
232
|
+
URL.revokeObjectURL(cachedUrl);
|
|
233
|
+
blobUrlCache.delete(blobId);
|
|
234
|
+
}
|
|
235
|
+
await deleteRecord(dbName, blobId);
|
|
236
|
+
}
|
|
237
|
+
async function clearAll() {
|
|
238
|
+
clearBlobUrlCache();
|
|
239
|
+
await clearRecords(dbName);
|
|
240
|
+
await Promise.all(chain.map((backend) => backend.clear()));
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
storeBlob,
|
|
244
|
+
storeBlobFromFile,
|
|
245
|
+
getBlobRecord,
|
|
246
|
+
getBlobRecordByBlake3,
|
|
247
|
+
getBlobRecordBySha256,
|
|
248
|
+
resolveBlob,
|
|
249
|
+
getBlobMetadata: resolveBlob,
|
|
250
|
+
getBlobData,
|
|
251
|
+
getBlob,
|
|
252
|
+
getBlobObjectURL,
|
|
253
|
+
clearBlobUrlCache,
|
|
254
|
+
hasBlobBytes,
|
|
255
|
+
checkBlobLocality,
|
|
256
|
+
deleteBlob,
|
|
257
|
+
clearAll,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// low-level building blocks re-exported for callers that need to compose
|
|
261
|
+
// their own resolver chain (e.g. a future transfer layer writing
|
|
262
|
+
// already-hashed, already-verified bytes without re-hashing them here).
|
|
263
|
+
export { writeThroughChain, readThroughChain, hasBytesInChain, removeFromChain } from "./bytes-backend.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.test.d.ts","sourceRoot":"","sources":["../../src/blobs/store.test.ts"],"names":[],"mappings":""}
|