@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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* minimal interface for a midden BiStream: a QUIC bidirectional stream
|
|
3
|
+
* exposing automerge-repo's length-delimited message framing, plus raw
|
|
4
|
+
* (unframed) reads/writes for protocols that terminate a stream without
|
|
5
|
+
* a length prefix instead.
|
|
6
|
+
*/
|
|
7
|
+
export interface BiStreamLike {
|
|
8
|
+
peer_node_id(): string;
|
|
9
|
+
alpn(): string;
|
|
10
|
+
write_message(data: Uint8Array): Promise<void>;
|
|
11
|
+
/** resolves to null when the stream is closed. */
|
|
12
|
+
read_message(): Promise<Uint8Array | null>;
|
|
13
|
+
close(): void;
|
|
14
|
+
/** raw framing (no length prefix) - used by protocols layered on top of
|
|
15
|
+
* the sync ALPN via `registerAlpnHandler`. optional because not every
|
|
16
|
+
* BiStream implementation supports it. */
|
|
17
|
+
read_to_end?(max_size: number): Promise<Uint8Array>;
|
|
18
|
+
write_raw_and_finish?(data: Uint8Array): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* midden node interface exposing the raw bidirectional-stream apis this
|
|
22
|
+
* adapter needs: dialing a peer (`open_bi`) and accepting inbound streams
|
|
23
|
+
* (`accept`).
|
|
24
|
+
*/
|
|
25
|
+
export interface MiddenStreamNode {
|
|
26
|
+
node_id(): string;
|
|
27
|
+
open_bi(peer_addr: string, alpn: string): Promise<BiStreamLike>;
|
|
28
|
+
accept(): Promise<BiStreamLike | null>;
|
|
29
|
+
}
|
|
30
|
+
/** the iroh endpoint's lifecycle state, for ui display. */
|
|
31
|
+
export type EndpointState = "off" | "starting" | "online" | "error";
|
|
32
|
+
/** summary of the adapter's connection state, for ui display. */
|
|
33
|
+
export interface ConnectionSummary {
|
|
34
|
+
/** number of peers we're actively connected to (stream is open). */
|
|
35
|
+
connected: number;
|
|
36
|
+
/** number of peers we're trying to reconnect to (in backoff). */
|
|
37
|
+
reconnecting: number;
|
|
38
|
+
/** number of peers where reconnection gave up (max attempts exceeded). */
|
|
39
|
+
failed: number;
|
|
40
|
+
}
|
|
41
|
+
export interface IrohNetworkAdapterOptions {
|
|
42
|
+
/**
|
|
43
|
+
* factory that returns (or lazily creates) the midden stream node.
|
|
44
|
+
* called at most once; the resolved node is cached.
|
|
45
|
+
*/
|
|
46
|
+
getNode: () => Promise<MiddenStreamNode>;
|
|
47
|
+
/**
|
|
48
|
+
* returns the current identity (any truthy value = identity exists).
|
|
49
|
+
* called once at connect() time to decide whether to start transport
|
|
50
|
+
* immediately or defer until onIdentityChange fires.
|
|
51
|
+
*/
|
|
52
|
+
getIdentity: () => Promise<unknown | null>;
|
|
53
|
+
/**
|
|
54
|
+
* optional subscription for identity changes. when provided and no
|
|
55
|
+
* identity exists at connect() time, the adapter subscribes and starts
|
|
56
|
+
* transport when the first truthy identity arrives. must return an
|
|
57
|
+
* unsubscribe function.
|
|
58
|
+
*/
|
|
59
|
+
onIdentityChange?: (cb: (identity: unknown | null) => void) => () => void;
|
|
60
|
+
/**
|
|
61
|
+
* ALPN string to use for automerge sync streams. defaults to
|
|
62
|
+
* SYNC_ALPN ("iroh/automerge-repo/1").
|
|
63
|
+
*/
|
|
64
|
+
syncAlpn?: string;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/automerge/types.ts"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,YAAY,IAAI,MAAM,CAAC;IACvB,IAAI,IAAI,MAAM,CAAC;IACf,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,kDAAkD;IAClD,YAAY,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC3C,KAAK,IAAI,IAAI,CAAC;IACd;;+CAE2C;IAC3C,WAAW,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACpD,oBAAoB,CAAC,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,MAAM,CAAC;IAClB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAChE,MAAM,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CACxC;AAED,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEpE,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAEzC;;;;OAIG;IACH,WAAW,EAAE,MAAM,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAE3C;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAE1E;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// shared shapes for ./automerge: the structural transport surface this
|
|
2
|
+
// subpath needs from a midden-shaped node speaking iroh bidirectional
|
|
3
|
+
// streams, plus the adapter's own construction options and ui-facing
|
|
4
|
+
// state summaries.
|
|
5
|
+
//
|
|
6
|
+
// like ./transfer, this subpath never imports midden at runtime - only
|
|
7
|
+
// structural types matching its snake_case surface, so any node-shaped
|
|
8
|
+
// object (a real wasm node, a worker-backed facade, a test double) works
|
|
9
|
+
// the same way.
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { BytesBackendName } from "./types.js";
|
|
2
|
+
export interface BytesBackend {
|
|
3
|
+
readonly name: BytesBackendName;
|
|
4
|
+
isAvailable(): Promise<boolean>;
|
|
5
|
+
/** write returns false (never throws) when the backend could not accept
|
|
6
|
+
* the write, so the caller can fall through to the next backend in the
|
|
7
|
+
* chain. */
|
|
8
|
+
write(id: string, data: ArrayBuffer, mime: string): Promise<boolean>;
|
|
9
|
+
read(id: string): Promise<ArrayBuffer | null>;
|
|
10
|
+
has(id: string): Promise<boolean>;
|
|
11
|
+
remove(id: string): Promise<void>;
|
|
12
|
+
/** remove every stored blob's bytes - used by `clearAll()`. */
|
|
13
|
+
clear(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/** true when the origin private file system API is available. */
|
|
16
|
+
export declare function isOPFSSupported(): boolean;
|
|
17
|
+
export declare function createOpfsBackend(): BytesBackend;
|
|
18
|
+
export declare function createCacheBackend(): BytesBackend;
|
|
19
|
+
/** the default resolver chain: OPFS first, Cache API fallback. */
|
|
20
|
+
export declare function defaultBytesChain(): BytesBackend[];
|
|
21
|
+
/**
|
|
22
|
+
* write bytes through the chain, trying each backend in order until one
|
|
23
|
+
* accepts the write. returns the name of the backend that succeeded, or
|
|
24
|
+
* null if every backend in the chain refused/failed - callers must treat
|
|
25
|
+
* that as a hard failure (a record with bytes nowhere is worse than no
|
|
26
|
+
* record at all).
|
|
27
|
+
*/
|
|
28
|
+
export declare function writeThroughChain(chain: BytesBackend[], id: string, data: ArrayBuffer, mime: string): Promise<BytesBackendName | null>;
|
|
29
|
+
/**
|
|
30
|
+
* read bytes given a known backend (from `storage_backend` on the
|
|
31
|
+
* record), or by probing the chain in order when the backend is unknown
|
|
32
|
+
* (legacy records predating that field).
|
|
33
|
+
*/
|
|
34
|
+
export declare function readThroughChain(chain: BytesBackend[], id: string, knownBackend?: BytesBackendName): Promise<ArrayBuffer | null>;
|
|
35
|
+
/** true if any backend in the chain actually holds bytes for `id`. */
|
|
36
|
+
export declare function hasBytesInChain(chain: BytesBackend[], id: string, knownBackend?: BytesBackendName): Promise<boolean>;
|
|
37
|
+
/** remove bytes for `id` from every backend in the chain (a record may
|
|
38
|
+
* have been rewritten across backends over its lifetime; removing from
|
|
39
|
+
* all of them avoids leaking orphaned bytes). */
|
|
40
|
+
export declare function removeFromChain(chain: BytesBackend[], id: string): Promise<void>;
|
|
41
|
+
//# sourceMappingURL=bytes-backend.d.ts.map
|
|
@@ -0,0 +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,kEAAkE;AAClE,wBAAgB,iBAAiB,IAAI,YAAY,EAAE,CAElD;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"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// the bytes side of the resolver chain: where raw blob bytes actually live.
|
|
2
|
+
//
|
|
3
|
+
// two backends, tried in order: OPFS is primary (bytes are content-addressed
|
|
4
|
+
// by blake3 hex filename), the Cache API is a fallback for origins where
|
|
5
|
+
// OPFS isn't available (webkitgtk historically rejects OPFS writes from the
|
|
6
|
+
// main thread) or where a write to OPFS failed for some other reason. a
|
|
7
|
+
// record remembers which backend it landed in (`storage_backend`) so a
|
|
8
|
+
// later read goes straight there instead of probing every backend on every
|
|
9
|
+
// read; records written before that field existed are assumed to be OPFS
|
|
10
|
+
// (the only backend that existed at the time).
|
|
11
|
+
//
|
|
12
|
+
// OPFS writes are content-addressed and shared across every consumer of
|
|
13
|
+
// this package at the origin (the same blake3 hash is the same bytes no
|
|
14
|
+
// matter which app wrote it), so the OPFS directory name and the cache
|
|
15
|
+
// name are fixed, not parameterized per store instance - only the
|
|
16
|
+
// IndexedDB metadata database name varies per app (see `store.ts`).
|
|
17
|
+
//
|
|
18
|
+
// writes go through the blob worker (`../worker`) for OPFS - it uses a
|
|
19
|
+
// `FileSystemSyncAccessHandle` when available, which is dramatically
|
|
20
|
+
// faster than the async writable-stream API and is only usable off the
|
|
21
|
+
// main thread. reads/existence-checks/removals talk to OPFS directly on
|
|
22
|
+
// the calling thread instead - plain `getFile()` reads don't need a sync
|
|
23
|
+
// access handle, and keeping them off the worker avoids a round trip.
|
|
24
|
+
import { BLOB_OPFS_DIR, writeBlobToOpfs as writeBlobToOpfsViaWorker } from "../worker/index.js";
|
|
25
|
+
import { log } from "../utils/log.js";
|
|
26
|
+
const TAG = "blobs.bytes-backend";
|
|
27
|
+
/** true when the origin private file system API is available. */
|
|
28
|
+
export function isOPFSSupported() {
|
|
29
|
+
return (typeof navigator !== "undefined" &&
|
|
30
|
+
"storage" in navigator &&
|
|
31
|
+
typeof navigator.storage.getDirectory === "function");
|
|
32
|
+
}
|
|
33
|
+
async function getOpfsDir(create) {
|
|
34
|
+
if (!isOPFSSupported())
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
const root = await navigator.storage.getDirectory();
|
|
38
|
+
return await root.getDirectoryHandle(BLOB_OPFS_DIR, { create });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function createOpfsBackend() {
|
|
45
|
+
return {
|
|
46
|
+
name: "opfs",
|
|
47
|
+
async isAvailable() {
|
|
48
|
+
return isOPFSSupported();
|
|
49
|
+
},
|
|
50
|
+
async write(id, data) {
|
|
51
|
+
try {
|
|
52
|
+
await writeBlobToOpfsViaWorker(id, data);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// the real underlying failure (a browser-level exception from the
|
|
57
|
+
// worker's OPFS write - e.g. a missing cross-origin-isolation
|
|
58
|
+
// requirement for FileSystemSyncAccessHandle, or a stale lock on
|
|
59
|
+
// this file from an interrupted previous write) must not be
|
|
60
|
+
// silently discarded here - a caller several layers up (storeBlob)
|
|
61
|
+
// only ever sees a generic "no bytes backend accepted it" once
|
|
62
|
+
// every backend in the chain has failed, so this is the last place
|
|
63
|
+
// the real cause is still available to log.
|
|
64
|
+
log.warn(TAG, `opfs write failed for ${id.slice(0, 16)}...:`, err);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
async read(id) {
|
|
69
|
+
try {
|
|
70
|
+
const dir = await getOpfsDir(false);
|
|
71
|
+
if (!dir)
|
|
72
|
+
return null;
|
|
73
|
+
const fileHandle = await dir.getFileHandle(id);
|
|
74
|
+
const file = await fileHandle.getFile();
|
|
75
|
+
return await file.arrayBuffer();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
async has(id) {
|
|
82
|
+
try {
|
|
83
|
+
const dir = await getOpfsDir(false);
|
|
84
|
+
if (!dir)
|
|
85
|
+
return false;
|
|
86
|
+
await dir.getFileHandle(id, { create: false });
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
async remove(id) {
|
|
94
|
+
try {
|
|
95
|
+
const dir = await getOpfsDir(false);
|
|
96
|
+
if (dir)
|
|
97
|
+
await dir.removeEntry(id);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// missing entry is fine - remove is best-effort
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async clear() {
|
|
104
|
+
try {
|
|
105
|
+
const root = await navigator.storage.getDirectory();
|
|
106
|
+
await root.removeEntry(BLOB_OPFS_DIR, { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// directory may not exist - that's fine
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** cache name for the Cache API fallback backend. a synthetic https origin
|
|
115
|
+
* is used for cache keys (`https://blob.local/<id>`) because webkitgtk
|
|
116
|
+
* rejects non-http(s) URLs in the Cache API. */
|
|
117
|
+
const CACHE_NAME = "reliquary-blobs";
|
|
118
|
+
const CACHE_URL_ORIGIN = "https://blob.local";
|
|
119
|
+
function cacheUrlFor(id) {
|
|
120
|
+
return `${CACHE_URL_ORIGIN}/${id}`;
|
|
121
|
+
}
|
|
122
|
+
function isCacheApiSupported() {
|
|
123
|
+
return typeof caches !== "undefined";
|
|
124
|
+
}
|
|
125
|
+
export function createCacheBackend() {
|
|
126
|
+
return {
|
|
127
|
+
name: "cache",
|
|
128
|
+
async isAvailable() {
|
|
129
|
+
return isCacheApiSupported();
|
|
130
|
+
},
|
|
131
|
+
async write(id, data, mime) {
|
|
132
|
+
if (!isCacheApiSupported())
|
|
133
|
+
return false;
|
|
134
|
+
try {
|
|
135
|
+
const cache = await caches.open(CACHE_NAME);
|
|
136
|
+
const response = new Response(data, { headers: { "Content-Type": mime } });
|
|
137
|
+
await cache.put(cacheUrlFor(id), response);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
log.warn(TAG, `cache-api write failed for ${id.slice(0, 16)}...:`, err);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
async read(id) {
|
|
146
|
+
if (!isCacheApiSupported())
|
|
147
|
+
return null;
|
|
148
|
+
try {
|
|
149
|
+
const cache = await caches.open(CACHE_NAME);
|
|
150
|
+
const response = await cache.match(cacheUrlFor(id));
|
|
151
|
+
if (!response)
|
|
152
|
+
return null;
|
|
153
|
+
return await response.arrayBuffer();
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
async has(id) {
|
|
160
|
+
if (!isCacheApiSupported())
|
|
161
|
+
return false;
|
|
162
|
+
try {
|
|
163
|
+
const cache = await caches.open(CACHE_NAME);
|
|
164
|
+
return (await cache.match(cacheUrlFor(id))) !== undefined;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
async remove(id) {
|
|
171
|
+
if (!isCacheApiSupported())
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
const cache = await caches.open(CACHE_NAME);
|
|
175
|
+
await cache.delete(cacheUrlFor(id));
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// missing entry is fine - remove is best-effort
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
async clear() {
|
|
182
|
+
if (!isCacheApiSupported())
|
|
183
|
+
return;
|
|
184
|
+
try {
|
|
185
|
+
await caches.delete(CACHE_NAME);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// cache may not exist - that's fine
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** the default resolver chain: OPFS first, Cache API fallback. */
|
|
194
|
+
export function defaultBytesChain() {
|
|
195
|
+
return [createOpfsBackend(), createCacheBackend()];
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* write bytes through the chain, trying each backend in order until one
|
|
199
|
+
* accepts the write. returns the name of the backend that succeeded, or
|
|
200
|
+
* null if every backend in the chain refused/failed - callers must treat
|
|
201
|
+
* that as a hard failure (a record with bytes nowhere is worse than no
|
|
202
|
+
* record at all).
|
|
203
|
+
*/
|
|
204
|
+
export async function writeThroughChain(chain, id, data, mime) {
|
|
205
|
+
for (const backend of chain) {
|
|
206
|
+
if (!(await backend.isAvailable()))
|
|
207
|
+
continue;
|
|
208
|
+
if (await backend.write(id, data, mime))
|
|
209
|
+
return backend.name;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* read bytes given a known backend (from `storage_backend` on the
|
|
215
|
+
* record), or by probing the chain in order when the backend is unknown
|
|
216
|
+
* (legacy records predating that field).
|
|
217
|
+
*/
|
|
218
|
+
export async function readThroughChain(chain, id, knownBackend) {
|
|
219
|
+
if (knownBackend) {
|
|
220
|
+
const backend = chain.find((b) => b.name === knownBackend);
|
|
221
|
+
if (backend) {
|
|
222
|
+
const data = await backend.read(id);
|
|
223
|
+
if (data)
|
|
224
|
+
return data;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const backend of chain) {
|
|
228
|
+
const data = await backend.read(id);
|
|
229
|
+
if (data)
|
|
230
|
+
return data;
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
/** true if any backend in the chain actually holds bytes for `id`. */
|
|
235
|
+
export async function hasBytesInChain(chain, id, knownBackend) {
|
|
236
|
+
if (knownBackend) {
|
|
237
|
+
const backend = chain.find((b) => b.name === knownBackend);
|
|
238
|
+
if (backend && (await backend.has(id)))
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
for (const backend of chain) {
|
|
242
|
+
if (await backend.has(id))
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
/** remove bytes for `id` from every backend in the chain (a record may
|
|
248
|
+
* have been rewritten across backends over its lifetime; removing from
|
|
249
|
+
* all of them avoids leaking orphaned bytes). */
|
|
250
|
+
export async function removeFromChain(chain, id) {
|
|
251
|
+
await Promise.all(chain.map((backend) => backend.remove(id)));
|
|
252
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bytes-backend.test.d.ts","sourceRoot":"","sources":["../../src/blobs/bytes-backend.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// exercises the bytes side of the resolver chain: OPFS backend (fake
|
|
4
|
+
// directory/file handles - sync access handles are worker/chromium-only
|
|
5
|
+
// and not worth faking here, same call as blob-worker-logic.test.ts),
|
|
6
|
+
// Cache API backend (fake CacheStorage), and the chain helpers that tie
|
|
7
|
+
// multiple backends together.
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { log } from "../utils/log.js";
|
|
10
|
+
// ---- fake OPFS (async writable-stream path only, matches
|
|
11
|
+
// blob-worker-logic.test.ts's fake) ----
|
|
12
|
+
class FakeWritable {
|
|
13
|
+
file;
|
|
14
|
+
constructor(file) {
|
|
15
|
+
this.file = file;
|
|
16
|
+
}
|
|
17
|
+
async write(data) {
|
|
18
|
+
this.file.bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer);
|
|
19
|
+
}
|
|
20
|
+
async close() { }
|
|
21
|
+
}
|
|
22
|
+
class FakeFileHandle {
|
|
23
|
+
bytes = new Uint8Array(0);
|
|
24
|
+
async getFile() {
|
|
25
|
+
const bytes = this.bytes;
|
|
26
|
+
return { arrayBuffer: async () => bytes.buffer };
|
|
27
|
+
}
|
|
28
|
+
async createWritable() {
|
|
29
|
+
return new FakeWritable(this);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
class FakeDirHandle {
|
|
33
|
+
files = new Map();
|
|
34
|
+
async getFileHandle(name, opts) {
|
|
35
|
+
let handle = this.files.get(name);
|
|
36
|
+
if (!handle) {
|
|
37
|
+
if (!opts?.create)
|
|
38
|
+
throw new Error(`not found: ${name}`);
|
|
39
|
+
handle = new FakeFileHandle();
|
|
40
|
+
this.files.set(name, handle);
|
|
41
|
+
}
|
|
42
|
+
return handle;
|
|
43
|
+
}
|
|
44
|
+
async getDirectoryHandle(_name, opts) {
|
|
45
|
+
void opts;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
async removeEntry(name) {
|
|
49
|
+
if (!this.files.delete(name))
|
|
50
|
+
throw new Error(`not found: ${name}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function installFakeOpfs() {
|
|
54
|
+
const root = new FakeDirHandle();
|
|
55
|
+
vi.stubGlobal("navigator", {
|
|
56
|
+
storage: {
|
|
57
|
+
getDirectory: async () => root,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
return root;
|
|
61
|
+
}
|
|
62
|
+
// the worker write path is mocked to write through the same fake
|
|
63
|
+
// directory the direct-read path uses, so both sides of a
|
|
64
|
+
// write-then-read round trip agree on where bytes live - exactly like
|
|
65
|
+
// the real worker (sync-access-handle writes) and the real direct OPFS
|
|
66
|
+
// reads share one physical directory.
|
|
67
|
+
vi.mock("../worker/index.js", () => ({
|
|
68
|
+
BLOB_OPFS_DIR: "reliquary-blobs",
|
|
69
|
+
writeBlobToOpfs: vi.fn(async (id, data) => {
|
|
70
|
+
const root = await navigator.storage.getDirectory();
|
|
71
|
+
const dir = await root.getDirectoryHandle("reliquary-blobs", { create: true });
|
|
72
|
+
const file = await dir.getFileHandle(id, { create: true });
|
|
73
|
+
const writable = await file.createWritable();
|
|
74
|
+
await writable.write(data);
|
|
75
|
+
await writable.close();
|
|
76
|
+
}),
|
|
77
|
+
}));
|
|
78
|
+
// ---- fake Cache API ----
|
|
79
|
+
class FakeCache {
|
|
80
|
+
entries = new Map();
|
|
81
|
+
async put(url, response) {
|
|
82
|
+
this.entries.set(url, response);
|
|
83
|
+
}
|
|
84
|
+
async match(url) {
|
|
85
|
+
const found = this.entries.get(url);
|
|
86
|
+
return found ? found.clone() : undefined;
|
|
87
|
+
}
|
|
88
|
+
async delete(url) {
|
|
89
|
+
return this.entries.delete(url);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
class FakeCacheStorage {
|
|
93
|
+
caches = new Map();
|
|
94
|
+
async open(name) {
|
|
95
|
+
let cache = this.caches.get(name);
|
|
96
|
+
if (!cache) {
|
|
97
|
+
cache = new FakeCache();
|
|
98
|
+
this.caches.set(name, cache);
|
|
99
|
+
}
|
|
100
|
+
return cache;
|
|
101
|
+
}
|
|
102
|
+
async delete(name) {
|
|
103
|
+
return this.caches.delete(name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function installFakeCaches() {
|
|
107
|
+
const storage = new FakeCacheStorage();
|
|
108
|
+
vi.stubGlobal("caches", storage);
|
|
109
|
+
return storage;
|
|
110
|
+
}
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
vi.unstubAllGlobals();
|
|
113
|
+
});
|
|
114
|
+
const { createOpfsBackend, createCacheBackend, isOPFSSupported, defaultBytesChain, writeThroughChain, readThroughChain, hasBytesInChain, removeFromChain, } = await import("./bytes-backend.js");
|
|
115
|
+
describe("isOPFSSupported", () => {
|
|
116
|
+
it("is false with no navigator.storage.getDirectory", () => {
|
|
117
|
+
expect(isOPFSSupported()).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
it("is true once a fake OPFS is installed", () => {
|
|
120
|
+
installFakeOpfs();
|
|
121
|
+
expect(isOPFSSupported()).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe("createOpfsBackend", () => {
|
|
125
|
+
it("round-trips bytes through write/read/has/remove", async () => {
|
|
126
|
+
installFakeOpfs();
|
|
127
|
+
const backend = createOpfsBackend();
|
|
128
|
+
const data = new TextEncoder().encode("hello opfs").buffer;
|
|
129
|
+
expect(await backend.write("id-1", data, "text/plain")).toBe(true);
|
|
130
|
+
expect(await backend.has("id-1")).toBe(true);
|
|
131
|
+
const readBack = await backend.read("id-1");
|
|
132
|
+
expect(new TextDecoder().decode(readBack)).toBe("hello opfs");
|
|
133
|
+
await backend.remove("id-1");
|
|
134
|
+
expect(await backend.has("id-1")).toBe(false);
|
|
135
|
+
expect(await backend.read("id-1")).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
it("read/has return null/false, write returns false, with no OPFS available", async () => {
|
|
138
|
+
const backend = createOpfsBackend();
|
|
139
|
+
expect(await backend.isAvailable()).toBe(false);
|
|
140
|
+
expect(await backend.write("id-1", new ArrayBuffer(4), "text/plain")).toBe(false);
|
|
141
|
+
expect(await backend.read("id-1")).toBeNull();
|
|
142
|
+
expect(await backend.has("id-1")).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
it("write returns false (not throw) when OPFS is available but the underlying write rejects - e.g. a real browser where createSyncAccessHandle throws because the page isn't cross-origin isolated, or a stale lock from a crashed session still holds the file - and logs the real underlying error instead of silently discarding it", async () => {
|
|
145
|
+
installFakeOpfs();
|
|
146
|
+
const backend = createOpfsBackend();
|
|
147
|
+
const worker = await import("../worker/index.js");
|
|
148
|
+
const realError = new Error("NotAllowedError: Access Handles cannot be created");
|
|
149
|
+
vi.mocked(worker.writeBlobToOpfs).mockRejectedValueOnce(realError);
|
|
150
|
+
const warnSpy = vi.spyOn(log, "warn");
|
|
151
|
+
// isAvailable() only checks that the OPFS *api* exists (navigator.
|
|
152
|
+
// storage.getDirectory is a function) - it says nothing about whether
|
|
153
|
+
// an actual write will succeed, so a real per-write failure surfaces
|
|
154
|
+
// here, at write() itself, not at isAvailable().
|
|
155
|
+
expect(await backend.isAvailable()).toBe(true);
|
|
156
|
+
expect(await backend.write("id-1", new ArrayBuffer(4), "text/plain")).toBe(false);
|
|
157
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.anything(), expect.any(String), realError);
|
|
158
|
+
});
|
|
159
|
+
it("clear() removes the whole opfs directory", async () => {
|
|
160
|
+
const root = installFakeOpfs();
|
|
161
|
+
const backend = createOpfsBackend();
|
|
162
|
+
await backend.write("id-1", new ArrayBuffer(4), "text/plain");
|
|
163
|
+
await backend.clear();
|
|
164
|
+
// the fake directory's removeEntry on the root only throws for a
|
|
165
|
+
// missing entry - a second clear() should stay a no-op, not throw.
|
|
166
|
+
await expect(backend.clear()).resolves.toBeUndefined();
|
|
167
|
+
void root;
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe("createCacheBackend", () => {
|
|
171
|
+
it("round-trips bytes through write/read/has/remove", async () => {
|
|
172
|
+
installFakeCaches();
|
|
173
|
+
const backend = createCacheBackend();
|
|
174
|
+
const data = new TextEncoder().encode("hello cache").buffer;
|
|
175
|
+
expect(await backend.write("id-2", data, "text/plain")).toBe(true);
|
|
176
|
+
expect(await backend.has("id-2")).toBe(true);
|
|
177
|
+
const readBack = await backend.read("id-2");
|
|
178
|
+
expect(new TextDecoder().decode(readBack)).toBe("hello cache");
|
|
179
|
+
await backend.remove("id-2");
|
|
180
|
+
expect(await backend.has("id-2")).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
it("read/has/write degrade gracefully with no Cache API available", async () => {
|
|
183
|
+
const backend = createCacheBackend();
|
|
184
|
+
expect(await backend.isAvailable()).toBe(false);
|
|
185
|
+
expect(await backend.write("id-2", new ArrayBuffer(4), "text/plain")).toBe(false);
|
|
186
|
+
expect(await backend.read("id-2")).toBeNull();
|
|
187
|
+
expect(await backend.has("id-2")).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe("chain helpers", () => {
|
|
191
|
+
it("writeThroughChain falls through to the next backend when the first is unavailable", async () => {
|
|
192
|
+
installFakeCaches(); // opfs unavailable, cache available
|
|
193
|
+
const chain = defaultBytesChain();
|
|
194
|
+
const backendName = await writeThroughChain(chain, "id-3", new ArrayBuffer(4), "text/plain");
|
|
195
|
+
expect(backendName).toBe("cache");
|
|
196
|
+
});
|
|
197
|
+
it("writeThroughChain returns null when every backend is unavailable", async () => {
|
|
198
|
+
const chain = defaultBytesChain();
|
|
199
|
+
expect(await writeThroughChain(chain, "id-3", new ArrayBuffer(4), "text/plain")).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
it("writeThroughChain returns null when OPFS is available but rejects the write and there is no fallback backend in the chain - reproduces the real 'no bytes backend accepted it' failure a caller sees when the cache-api fallback is disabled (allowCacheFallback: false)", async () => {
|
|
202
|
+
installFakeOpfs();
|
|
203
|
+
const worker = await import("../worker/index.js");
|
|
204
|
+
vi.mocked(worker.writeBlobToOpfs).mockRejectedValueOnce(new Error("NotAllowedError"));
|
|
205
|
+
const opfsOnlyChain = [createOpfsBackend()];
|
|
206
|
+
expect(await writeThroughChain(opfsOnlyChain, "id-3", new ArrayBuffer(4), "text/plain")).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
it("readThroughChain goes straight to the known backend", async () => {
|
|
209
|
+
installFakeOpfs();
|
|
210
|
+
installFakeCaches();
|
|
211
|
+
const chain = defaultBytesChain();
|
|
212
|
+
const data = new TextEncoder().encode("known backend").buffer;
|
|
213
|
+
await writeThroughChain(chain, "id-4", data, "text/plain");
|
|
214
|
+
const readBack = await readThroughChain(chain, "id-4", "opfs");
|
|
215
|
+
expect(new TextDecoder().decode(readBack)).toBe("known backend");
|
|
216
|
+
});
|
|
217
|
+
it("readThroughChain probes every backend when the backend is unknown", async () => {
|
|
218
|
+
installFakeCaches(); // only cache available - opfs was never installed
|
|
219
|
+
const chain = defaultBytesChain();
|
|
220
|
+
const data = new TextEncoder().encode("legacy record").buffer;
|
|
221
|
+
await writeThroughChain(chain, "id-5", data, "text/plain");
|
|
222
|
+
// no `knownBackend` passed - simulates a record predating the field
|
|
223
|
+
const readBack = await readThroughChain(chain, "id-5");
|
|
224
|
+
expect(new TextDecoder().decode(readBack)).toBe("legacy record");
|
|
225
|
+
});
|
|
226
|
+
it("hasBytesInChain checks every backend", async () => {
|
|
227
|
+
installFakeCaches();
|
|
228
|
+
const chain = defaultBytesChain();
|
|
229
|
+
expect(await hasBytesInChain(chain, "missing")).toBe(false);
|
|
230
|
+
await writeThroughChain(chain, "id-6", new ArrayBuffer(4), "text/plain");
|
|
231
|
+
expect(await hasBytesInChain(chain, "id-6")).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
it("removeFromChain removes from every backend that has the id", async () => {
|
|
234
|
+
installFakeOpfs();
|
|
235
|
+
installFakeCaches();
|
|
236
|
+
const chain = defaultBytesChain();
|
|
237
|
+
await writeThroughChain(chain, "id-7", new ArrayBuffer(4), "text/plain");
|
|
238
|
+
await removeFromChain(chain, "id-7");
|
|
239
|
+
expect(await hasBytesInChain(chain, "id-7")).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BlobRecord } from "./types.js";
|
|
2
|
+
export declare function putRecord(dbName: string, record: BlobRecord): Promise<void>;
|
|
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
|
+
export declare function getRecordByBlake3(dbName: string, blake3: string): Promise<BlobRecord | null>;
|
|
6
|
+
export declare function deleteRecord(dbName: string, blobId: string): Promise<void>;
|
|
7
|
+
export declare function clearRecords(dbName: string): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=db.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/blobs/db.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAkE7C,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAcjF;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAS1F;AAkBD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAE5F;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAE5F;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAchF;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAchE"}
|