@bcts/hubert 1.0.0-alpha.17
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/LICENSE +48 -0
- package/README.md +18 -0
- package/dist/arid-derivation-1CJuU-kZ.cjs +150 -0
- package/dist/arid-derivation-1CJuU-kZ.cjs.map +1 -0
- package/dist/arid-derivation-CbqACjdg.mjs +126 -0
- package/dist/arid-derivation-CbqACjdg.mjs.map +1 -0
- package/dist/bin/hubert.cjs +384 -0
- package/dist/bin/hubert.cjs.map +1 -0
- package/dist/bin/hubert.d.cts +1 -0
- package/dist/bin/hubert.d.mts +1 -0
- package/dist/bin/hubert.mjs +383 -0
- package/dist/bin/hubert.mjs.map +1 -0
- package/dist/chunk-CbDLau6x.cjs +34 -0
- package/dist/hybrid/index.cjs +14 -0
- package/dist/hybrid/index.d.cts +3 -0
- package/dist/hybrid/index.d.mts +3 -0
- package/dist/hybrid/index.mjs +6 -0
- package/dist/hybrid-BZhumygj.mjs +356 -0
- package/dist/hybrid-BZhumygj.mjs.map +1 -0
- package/dist/hybrid-dX5JLumO.cjs +410 -0
- package/dist/hybrid-dX5JLumO.cjs.map +1 -0
- package/dist/index-BEzpUC7r.d.mts +380 -0
- package/dist/index-BEzpUC7r.d.mts.map +1 -0
- package/dist/index-C2F6ugLL.d.mts +210 -0
- package/dist/index-C2F6ugLL.d.mts.map +1 -0
- package/dist/index-CUnDouMb.d.mts +215 -0
- package/dist/index-CUnDouMb.d.mts.map +1 -0
- package/dist/index-CV6lZJqY.d.cts +380 -0
- package/dist/index-CV6lZJqY.d.cts.map +1 -0
- package/dist/index-CY3TCzIm.d.cts +217 -0
- package/dist/index-CY3TCzIm.d.cts.map +1 -0
- package/dist/index-DEr4SR1J.d.cts +215 -0
- package/dist/index-DEr4SR1J.d.cts.map +1 -0
- package/dist/index-T1LHanIb.d.mts +217 -0
- package/dist/index-T1LHanIb.d.mts.map +1 -0
- package/dist/index-jyzuOhFB.d.cts +210 -0
- package/dist/index-jyzuOhFB.d.cts.map +1 -0
- package/dist/index.cjs +60 -0
- package/dist/index.d.cts +161 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +161 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +10 -0
- package/dist/ipfs/index.cjs +13 -0
- package/dist/ipfs/index.d.cts +3 -0
- package/dist/ipfs/index.d.mts +3 -0
- package/dist/ipfs/index.mjs +5 -0
- package/dist/ipfs-BRMMCBjv.mjs +1 -0
- package/dist/ipfs-CetOVQcO.cjs +0 -0
- package/dist/kv-BAmhmMOo.cjs +425 -0
- package/dist/kv-BAmhmMOo.cjs.map +1 -0
- package/dist/kv-C-emxv0w.mjs +375 -0
- package/dist/kv-C-emxv0w.mjs.map +1 -0
- package/dist/kv-DJiKvypY.mjs +403 -0
- package/dist/kv-DJiKvypY.mjs.map +1 -0
- package/dist/kv-store-DmngWWuw.d.mts +183 -0
- package/dist/kv-store-DmngWWuw.d.mts.map +1 -0
- package/dist/kv-store-ww-AUyLd.d.cts +183 -0
- package/dist/kv-store-ww-AUyLd.d.cts.map +1 -0
- package/dist/kv-yjvQa_LH.cjs +457 -0
- package/dist/kv-yjvQa_LH.cjs.map +1 -0
- package/dist/logging-hmzNzifq.mjs +158 -0
- package/dist/logging-hmzNzifq.mjs.map +1 -0
- package/dist/logging-qc9uMgil.cjs +212 -0
- package/dist/logging-qc9uMgil.cjs.map +1 -0
- package/dist/mainline/index.cjs +12 -0
- package/dist/mainline/index.d.cts +3 -0
- package/dist/mainline/index.d.mts +3 -0
- package/dist/mainline/index.mjs +5 -0
- package/dist/mainline-D_jfeFMh.cjs +0 -0
- package/dist/mainline-cFIuXbo-.mjs +1 -0
- package/dist/server/index.cjs +14 -0
- package/dist/server/index.d.cts +3 -0
- package/dist/server/index.d.mts +3 -0
- package/dist/server/index.mjs +3 -0
- package/dist/server-BBNRZ30D.cjs +912 -0
- package/dist/server-BBNRZ30D.cjs.map +1 -0
- package/dist/server-DVyk9gqU.mjs +836 -0
- package/dist/server-DVyk9gqU.mjs.map +1 -0
- package/package.json +125 -0
- package/src/arid-derivation.ts +155 -0
- package/src/bin/hubert.ts +667 -0
- package/src/error.ts +89 -0
- package/src/hybrid/error.ts +77 -0
- package/src/hybrid/index.ts +24 -0
- package/src/hybrid/kv.ts +236 -0
- package/src/hybrid/reference.ts +176 -0
- package/src/index.ts +145 -0
- package/src/ipfs/error.ts +83 -0
- package/src/ipfs/index.ts +24 -0
- package/src/ipfs/kv.ts +476 -0
- package/src/ipfs/value.ts +85 -0
- package/src/kv-store.ts +128 -0
- package/src/logging.ts +88 -0
- package/src/mainline/error.ts +108 -0
- package/src/mainline/index.ts +23 -0
- package/src/mainline/kv.ts +411 -0
- package/src/server/error.ts +83 -0
- package/src/server/index.ts +29 -0
- package/src/server/kv.ts +211 -0
- package/src/server/memory-kv.ts +191 -0
- package/src/server/server-kv.ts +92 -0
- package/src/server/server.ts +369 -0
- package/src/server/sqlite-kv.ts +295 -0
package/src/error.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level error types for the hubert library.
|
|
3
|
+
*
|
|
4
|
+
* Port of error.rs from hubert-rust.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Base error class for all Hubert errors.
|
|
11
|
+
*
|
|
12
|
+
* @category Errors
|
|
13
|
+
*/
|
|
14
|
+
export class HubertError extends Error {
|
|
15
|
+
constructor(message: string) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "HubertError";
|
|
18
|
+
// Maintains proper stack trace for where error was thrown (V8 only)
|
|
19
|
+
if (Error.captureStackTrace) {
|
|
20
|
+
Error.captureStackTrace(this, this.constructor);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Error thrown when attempting to store at an ARID that already exists.
|
|
27
|
+
*
|
|
28
|
+
* Port of `Error::AlreadyExists { arid }` from error.rs line 6.
|
|
29
|
+
*
|
|
30
|
+
* @category Errors
|
|
31
|
+
*/
|
|
32
|
+
export class AlreadyExistsError extends HubertError {
|
|
33
|
+
/** The ARID that already exists */
|
|
34
|
+
readonly arid: string;
|
|
35
|
+
|
|
36
|
+
constructor(arid: string) {
|
|
37
|
+
super(`${arid} already exists`);
|
|
38
|
+
this.name = "AlreadyExistsError";
|
|
39
|
+
this.arid = arid;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Error thrown when an ARID is not found.
|
|
45
|
+
*
|
|
46
|
+
* Port of `Error::NotFound` from error.rs line 8.
|
|
47
|
+
*
|
|
48
|
+
* @category Errors
|
|
49
|
+
*/
|
|
50
|
+
export class NotFoundError extends HubertError {
|
|
51
|
+
constructor() {
|
|
52
|
+
super("Not found");
|
|
53
|
+
this.name = "NotFoundError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Error thrown when an ARID format is invalid.
|
|
59
|
+
*
|
|
60
|
+
* Port of `Error::InvalidArid` from error.rs line 11.
|
|
61
|
+
*
|
|
62
|
+
* @category Errors
|
|
63
|
+
*/
|
|
64
|
+
export class InvalidAridError extends HubertError {
|
|
65
|
+
constructor() {
|
|
66
|
+
super("Invalid ARID format");
|
|
67
|
+
this.name = "InvalidAridError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Error thrown for I/O operations.
|
|
73
|
+
*
|
|
74
|
+
* Port of `Error::Io(e)` from error.rs line 35.
|
|
75
|
+
*
|
|
76
|
+
* @category Errors
|
|
77
|
+
*/
|
|
78
|
+
export class IoError extends HubertError {
|
|
79
|
+
/** The underlying error */
|
|
80
|
+
override readonly cause?: Error;
|
|
81
|
+
|
|
82
|
+
constructor(message: string, cause?: Error) {
|
|
83
|
+
super(`IO error: ${message}`);
|
|
84
|
+
this.name = "IoError";
|
|
85
|
+
if (cause !== undefined) {
|
|
86
|
+
this.cause = cause;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid-specific errors.
|
|
3
|
+
*
|
|
4
|
+
* Port of hybrid/error.rs from hubert-rust.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { HubertError } from "../error.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Base class for Hybrid-specific errors.
|
|
13
|
+
*
|
|
14
|
+
* @category Hybrid
|
|
15
|
+
*/
|
|
16
|
+
export class HybridError extends HubertError {
|
|
17
|
+
constructor(message: string) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "HybridError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Referenced IPFS content not found.
|
|
25
|
+
*
|
|
26
|
+
* Port of `Error::ContentNotFound` from hybrid/error.rs line 4-5.
|
|
27
|
+
*
|
|
28
|
+
* @category Hybrid
|
|
29
|
+
*/
|
|
30
|
+
export class ContentNotFoundError extends HybridError {
|
|
31
|
+
constructor() {
|
|
32
|
+
super("Referenced IPFS content not found");
|
|
33
|
+
this.name = "ContentNotFoundError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Not a reference envelope.
|
|
39
|
+
*
|
|
40
|
+
* Port of `Error::NotReferenceEnvelope` from hybrid/error.rs line 7-8.
|
|
41
|
+
*
|
|
42
|
+
* @category Hybrid
|
|
43
|
+
*/
|
|
44
|
+
export class NotReferenceEnvelopeError extends HybridError {
|
|
45
|
+
constructor() {
|
|
46
|
+
super("Not a reference envelope");
|
|
47
|
+
this.name = "NotReferenceEnvelopeError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Invalid ARID in reference envelope.
|
|
53
|
+
*
|
|
54
|
+
* Port of `Error::InvalidReferenceArid` from hybrid/error.rs line 10-11.
|
|
55
|
+
*
|
|
56
|
+
* @category Hybrid
|
|
57
|
+
*/
|
|
58
|
+
export class InvalidReferenceAridError extends HybridError {
|
|
59
|
+
constructor() {
|
|
60
|
+
super("Invalid ARID in reference envelope");
|
|
61
|
+
this.name = "InvalidReferenceAridError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* No id assertion found in reference envelope.
|
|
67
|
+
*
|
|
68
|
+
* Port of `Error::NoIdAssertion` from hybrid/error.rs line 13-14.
|
|
69
|
+
*
|
|
70
|
+
* @category Hybrid
|
|
71
|
+
*/
|
|
72
|
+
export class NoIdAssertionError extends HybridError {
|
|
73
|
+
constructor() {
|
|
74
|
+
super("No id assertion found in reference envelope");
|
|
75
|
+
this.name = "NoIdAssertionError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid module for Hubert distributed storage.
|
|
3
|
+
*
|
|
4
|
+
* This module combines Mainline DHT and IPFS for optimal storage.
|
|
5
|
+
*
|
|
6
|
+
* Port of hybrid/mod.rs from hubert-rust.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Error types
|
|
12
|
+
export {
|
|
13
|
+
HybridError,
|
|
14
|
+
ContentNotFoundError,
|
|
15
|
+
NotReferenceEnvelopeError,
|
|
16
|
+
InvalidReferenceAridError,
|
|
17
|
+
NoIdAssertionError,
|
|
18
|
+
} from "./error.js";
|
|
19
|
+
|
|
20
|
+
// Reference envelope utilities
|
|
21
|
+
export { createReferenceEnvelope, isReferenceEnvelope, extractReferenceArid } from "./reference.js";
|
|
22
|
+
|
|
23
|
+
// Hybrid KvStore implementation
|
|
24
|
+
export { HybridKv } from "./kv.js";
|
package/src/hybrid/kv.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid storage layer combining Mainline DHT and IPFS.
|
|
3
|
+
*
|
|
4
|
+
* Port of hybrid/kv.rs from hubert-rust.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ARID } from "@bcts/components";
|
|
10
|
+
import { type Envelope } from "@bcts/envelope";
|
|
11
|
+
|
|
12
|
+
import { type KvStore } from "../kv-store.js";
|
|
13
|
+
import { verbosePrintln } from "../logging.js";
|
|
14
|
+
import { IpfsKv } from "../ipfs/kv.js";
|
|
15
|
+
import { MainlineDhtKv } from "../mainline/kv.js";
|
|
16
|
+
import { ContentNotFoundError } from "./error.js";
|
|
17
|
+
import { createReferenceEnvelope, extractReferenceArid, isReferenceEnvelope } from "./reference.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hybrid storage layer combining Mainline DHT and IPFS.
|
|
21
|
+
*
|
|
22
|
+
* Automatically optimizes storage based on envelope size:
|
|
23
|
+
* - **Small envelopes (≤1000 bytes)**: Stored directly in DHT
|
|
24
|
+
* - **Large envelopes (>1000 bytes)**: Reference in DHT → actual envelope in IPFS
|
|
25
|
+
*
|
|
26
|
+
* This provides the best of both worlds:
|
|
27
|
+
* - Fast lookups for small messages via DHT
|
|
28
|
+
* - Large capacity for big messages via IPFS
|
|
29
|
+
* - Transparent indirection handled automatically
|
|
30
|
+
*
|
|
31
|
+
* Port of `struct HybridKv` from hybrid/kv.rs lines 59-63.
|
|
32
|
+
*
|
|
33
|
+
* # Requirements
|
|
34
|
+
*
|
|
35
|
+
* - No external daemon for DHT (embedded client)
|
|
36
|
+
* - Requires Kubo daemon for IPFS (http://127.0.0.1:5001)
|
|
37
|
+
*
|
|
38
|
+
* @category Hybrid Backend
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const store = await HybridKv.create("http://127.0.0.1:5001");
|
|
43
|
+
*
|
|
44
|
+
* // Small envelope → DHT only
|
|
45
|
+
* const arid1 = ARID.new();
|
|
46
|
+
* const small = Envelope.new("Small message");
|
|
47
|
+
* await store.put(arid1, small);
|
|
48
|
+
*
|
|
49
|
+
* // Large envelope → DHT reference + IPFS
|
|
50
|
+
* const arid2 = ARID.new();
|
|
51
|
+
* const large = Envelope.new("x".repeat(2000));
|
|
52
|
+
* await store.put(arid2, large);
|
|
53
|
+
*
|
|
54
|
+
* // Get works the same for both
|
|
55
|
+
* const retrieved1 = await store.get(arid1);
|
|
56
|
+
* const retrieved2 = await store.get(arid2);
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export class HybridKv implements KvStore {
|
|
60
|
+
private readonly dht: MainlineDhtKv;
|
|
61
|
+
private ipfs: IpfsKv;
|
|
62
|
+
private dhtSizeLimit: number;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Private constructor - use `create()` factory method.
|
|
66
|
+
*/
|
|
67
|
+
private constructor(dht: MainlineDhtKv, ipfs: IpfsKv) {
|
|
68
|
+
this.dht = dht;
|
|
69
|
+
this.ipfs = ipfs;
|
|
70
|
+
this.dhtSizeLimit = 1000; // Conservative DHT limit
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a new Hybrid KV store with default settings.
|
|
75
|
+
*
|
|
76
|
+
* Port of `HybridKv::new()` from hybrid/kv.rs lines 75-84.
|
|
77
|
+
*
|
|
78
|
+
* @param ipfsRpcUrl - IPFS RPC endpoint (e.g., "http://127.0.0.1:5001")
|
|
79
|
+
*/
|
|
80
|
+
static async create(ipfsRpcUrl: string): Promise<HybridKv> {
|
|
81
|
+
const dht = await MainlineDhtKv.create();
|
|
82
|
+
const ipfs = new IpfsKv(ipfsRpcUrl);
|
|
83
|
+
|
|
84
|
+
return new HybridKv(dht, ipfs);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Set custom DHT size limit (default: 1000 bytes).
|
|
89
|
+
*
|
|
90
|
+
* Envelopes larger than this will use IPFS indirection.
|
|
91
|
+
*
|
|
92
|
+
* Port of `HybridKv::with_dht_size_limit()` from hybrid/kv.rs lines 89-92.
|
|
93
|
+
*/
|
|
94
|
+
withDhtSizeLimit(limit: number): this {
|
|
95
|
+
this.dhtSizeLimit = limit;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Set whether to pin content in IPFS (default: false).
|
|
101
|
+
*
|
|
102
|
+
* Only affects envelopes stored in IPFS (when larger than DHT limit).
|
|
103
|
+
*
|
|
104
|
+
* Port of `HybridKv::with_pin_content()` from hybrid/kv.rs lines 97-100.
|
|
105
|
+
*/
|
|
106
|
+
withPinContent(pin: boolean): this {
|
|
107
|
+
this.ipfs = this.ipfs.withPinContent(pin);
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if an envelope fits in the DHT.
|
|
113
|
+
*
|
|
114
|
+
* Port of `HybridKv::fits_in_dht()` from hybrid/kv.rs lines 103-106.
|
|
115
|
+
*
|
|
116
|
+
* @internal
|
|
117
|
+
*/
|
|
118
|
+
private fitsInDht(envelope: Envelope): boolean {
|
|
119
|
+
const serialized = envelope.taggedCborData;
|
|
120
|
+
return serialized.length <= this.dhtSizeLimit;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Store an envelope at the given ARID.
|
|
125
|
+
*
|
|
126
|
+
* Port of `KvStore::put()` implementation from hybrid/kv.rs lines 109-168.
|
|
127
|
+
*/
|
|
128
|
+
async put(
|
|
129
|
+
arid: ARID,
|
|
130
|
+
envelope: Envelope,
|
|
131
|
+
ttlSeconds?: number,
|
|
132
|
+
verbose?: boolean,
|
|
133
|
+
): Promise<string> {
|
|
134
|
+
// Check if it fits in DHT
|
|
135
|
+
if (this.fitsInDht(envelope)) {
|
|
136
|
+
// Store directly in DHT (DHT handles obfuscation)
|
|
137
|
+
if (verbose) {
|
|
138
|
+
verbosePrintln(`Storing envelope in DHT (size ≤ ${this.dhtSizeLimit} bytes)`);
|
|
139
|
+
}
|
|
140
|
+
await this.dht.put(arid, envelope, ttlSeconds, verbose);
|
|
141
|
+
return `Stored in DHT at ARID: ${arid.urString()}`;
|
|
142
|
+
} else {
|
|
143
|
+
// Use IPFS with DHT reference
|
|
144
|
+
if (verbose) {
|
|
145
|
+
verbosePrintln("Envelope too large for DHT, using IPFS indirection");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 1. Store actual envelope in IPFS with a new ARID
|
|
149
|
+
// (IPFS handles obfuscation with reference_arid)
|
|
150
|
+
const referenceArid = ARID.new();
|
|
151
|
+
if (verbose) {
|
|
152
|
+
verbosePrintln(
|
|
153
|
+
`Storing actual envelope in IPFS with reference ARID: ${referenceArid.urString()}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
await this.ipfs.put(referenceArid, envelope, ttlSeconds, verbose);
|
|
157
|
+
|
|
158
|
+
// 2. Create reference envelope
|
|
159
|
+
const envelopeSize = envelope.taggedCborData.length;
|
|
160
|
+
const reference = createReferenceEnvelope(referenceArid, envelopeSize);
|
|
161
|
+
|
|
162
|
+
// 3. Store reference envelope in DHT at original ARID
|
|
163
|
+
// (DHT handles obfuscation with original arid)
|
|
164
|
+
if (verbose) {
|
|
165
|
+
verbosePrintln("Storing reference envelope in DHT at original ARID");
|
|
166
|
+
}
|
|
167
|
+
await this.dht.put(arid, reference, ttlSeconds, verbose);
|
|
168
|
+
|
|
169
|
+
return `Stored in IPFS (ref: ${referenceArid.urString()}) via DHT at ARID: ${arid.urString()}`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Retrieve an envelope for the given ARID.
|
|
175
|
+
*
|
|
176
|
+
* Port of `KvStore::get()` implementation from hybrid/kv.rs lines 171-230.
|
|
177
|
+
*/
|
|
178
|
+
async get(arid: ARID, timeoutSeconds?: number, verbose?: boolean): Promise<Envelope | null> {
|
|
179
|
+
// 1. Try to get from DHT (DHT handles deobfuscation)
|
|
180
|
+
const dhtEnvelope = await this.dht.get(arid, timeoutSeconds, verbose);
|
|
181
|
+
|
|
182
|
+
if (dhtEnvelope === null) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 2. Check if the envelope is a reference envelope
|
|
187
|
+
if (isReferenceEnvelope(dhtEnvelope)) {
|
|
188
|
+
if (verbose) {
|
|
189
|
+
verbosePrintln("Found reference envelope, fetching actual envelope from IPFS");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 3. Extract reference ARID
|
|
193
|
+
const referenceArid = extractReferenceArid(dhtEnvelope);
|
|
194
|
+
|
|
195
|
+
if (verbose) {
|
|
196
|
+
verbosePrintln(`Reference ARID: ${referenceArid.urString()}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 4. Retrieve actual envelope from IPFS
|
|
200
|
+
// (IPFS handles deobfuscation with reference_arid)
|
|
201
|
+
const ipfsEnvelope = await this.ipfs.get(referenceArid, timeoutSeconds, verbose);
|
|
202
|
+
|
|
203
|
+
if (ipfsEnvelope === null) {
|
|
204
|
+
throw new ContentNotFoundError();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (verbose) {
|
|
208
|
+
verbosePrintln("Successfully retrieved actual envelope from IPFS");
|
|
209
|
+
}
|
|
210
|
+
return ipfsEnvelope;
|
|
211
|
+
} else {
|
|
212
|
+
// Not a reference envelope, return it directly
|
|
213
|
+
if (verbose) {
|
|
214
|
+
verbosePrintln("Envelope is not a reference, treating as direct payload");
|
|
215
|
+
}
|
|
216
|
+
return dhtEnvelope;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check if an envelope exists at the given ARID.
|
|
222
|
+
*
|
|
223
|
+
* Port of `KvStore::exists()` implementation from hybrid/kv.rs lines 254-257.
|
|
224
|
+
*/
|
|
225
|
+
async exists(arid: ARID): Promise<boolean> {
|
|
226
|
+
// Check DHT only (references count as existing)
|
|
227
|
+
return await this.dht.exists(arid);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Destroy the hybrid store and release resources.
|
|
232
|
+
*/
|
|
233
|
+
async destroy(): Promise<void> {
|
|
234
|
+
await this.dht.destroy();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference envelope utilities for hybrid storage.
|
|
3
|
+
*
|
|
4
|
+
* Port of hybrid/reference.rs from hubert-rust.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ARID } from "@bcts/components";
|
|
10
|
+
import { Envelope } from "@bcts/envelope";
|
|
11
|
+
import { KnownValue } from "@bcts/known-values";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
InvalidReferenceAridError,
|
|
15
|
+
NoIdAssertionError,
|
|
16
|
+
NotReferenceEnvelopeError,
|
|
17
|
+
} from "./error.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Known value for dereferenceVia predicate.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
const DEREFERENCE_VIA = new KnownValue(8);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Known value for id predicate.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
const ID = new KnownValue(2);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a reference envelope that points to content stored in IPFS.
|
|
33
|
+
*
|
|
34
|
+
* Reference envelopes are small envelopes stored in the DHT that contain
|
|
35
|
+
* a pointer to the actual envelope stored in IPFS. This allows the hybrid
|
|
36
|
+
* storage layer to transparently handle large envelopes that exceed the
|
|
37
|
+
* DHT size limit.
|
|
38
|
+
*
|
|
39
|
+
* Port of `create_reference_envelope()` from hybrid/reference.rs lines 31-39.
|
|
40
|
+
*
|
|
41
|
+
* # Format
|
|
42
|
+
*
|
|
43
|
+
* ```text
|
|
44
|
+
* '' [
|
|
45
|
+
* 'dereferenceVia': "ipfs",
|
|
46
|
+
* 'id': <ARID>,
|
|
47
|
+
* "size": <number>
|
|
48
|
+
* ]
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @param referenceArid - The ARID used to look up the actual envelope in IPFS
|
|
52
|
+
* @param actualSize - Size of the actual envelope in bytes (for diagnostics)
|
|
53
|
+
* @returns A reference envelope that can be stored in the DHT
|
|
54
|
+
*
|
|
55
|
+
* @category Hybrid
|
|
56
|
+
*/
|
|
57
|
+
export function createReferenceEnvelope(referenceArid: ARID, actualSize: number): Envelope {
|
|
58
|
+
return Envelope.unit()
|
|
59
|
+
.addAssertion(DEREFERENCE_VIA, "ipfs")
|
|
60
|
+
.addAssertion(ID, referenceArid)
|
|
61
|
+
.addAssertion("size", actualSize);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Checks if an envelope is a reference envelope.
|
|
66
|
+
*
|
|
67
|
+
* A reference envelope contains `dereferenceVia: "ipfs"` and an `id` assertion.
|
|
68
|
+
*
|
|
69
|
+
* Port of `is_reference_envelope()` from hybrid/reference.rs lines 53-91.
|
|
70
|
+
*
|
|
71
|
+
* @param envelope - The envelope to check
|
|
72
|
+
* @returns `true` if this is a reference envelope, `false` otherwise
|
|
73
|
+
*
|
|
74
|
+
* @category Hybrid
|
|
75
|
+
*/
|
|
76
|
+
export function isReferenceEnvelope(envelope: Envelope): boolean {
|
|
77
|
+
// Check if subject is the unit value
|
|
78
|
+
if (!envelope.isSubjectUnit) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const assertions = envelope.assertions();
|
|
83
|
+
|
|
84
|
+
// Check for dereferenceVia: "ipfs" assertion
|
|
85
|
+
let hasDereferenceVia = false;
|
|
86
|
+
let hasId = false;
|
|
87
|
+
|
|
88
|
+
for (const assertion of assertions) {
|
|
89
|
+
try {
|
|
90
|
+
const predicate = assertion.predicate();
|
|
91
|
+
|
|
92
|
+
// Check if predicate is a known value
|
|
93
|
+
const predicateSubject = predicate.subject();
|
|
94
|
+
if (predicateSubject instanceof KnownValue) {
|
|
95
|
+
const kv = predicateSubject;
|
|
96
|
+
|
|
97
|
+
// Check for dereferenceVia
|
|
98
|
+
if (kv.value === DEREFERENCE_VIA.value) {
|
|
99
|
+
const object = assertion.object();
|
|
100
|
+
const objectSubject = object.subject();
|
|
101
|
+
if (typeof objectSubject === "string" && objectSubject === "ipfs") {
|
|
102
|
+
hasDereferenceVia = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check for id
|
|
107
|
+
if (kv.value === ID.value) {
|
|
108
|
+
hasId = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Skip assertions that can't be parsed
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return hasDereferenceVia && hasId;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extracts the reference ARID from a reference envelope.
|
|
122
|
+
*
|
|
123
|
+
* Port of `extract_reference_arid()` from hybrid/reference.rs lines 104-129.
|
|
124
|
+
*
|
|
125
|
+
* @param envelope - The reference envelope
|
|
126
|
+
* @returns The reference ARID
|
|
127
|
+
* @throws {NotReferenceEnvelopeError} If the envelope is not a reference envelope
|
|
128
|
+
* @throws {InvalidReferenceAridError} If the ARID cannot be extracted
|
|
129
|
+
* @throws {NoIdAssertionError} If no id assertion is found
|
|
130
|
+
*
|
|
131
|
+
* @category Hybrid
|
|
132
|
+
*/
|
|
133
|
+
export function extractReferenceArid(envelope: Envelope): ARID {
|
|
134
|
+
if (!isReferenceEnvelope(envelope)) {
|
|
135
|
+
throw new NotReferenceEnvelopeError();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const assertions = envelope.assertions();
|
|
139
|
+
|
|
140
|
+
// Find the id assertion and extract the ARID
|
|
141
|
+
for (const assertion of assertions) {
|
|
142
|
+
try {
|
|
143
|
+
const predicate = assertion.predicate();
|
|
144
|
+
const predicateSubject = predicate.subject();
|
|
145
|
+
|
|
146
|
+
if (predicateSubject instanceof KnownValue) {
|
|
147
|
+
const kv = predicateSubject;
|
|
148
|
+
|
|
149
|
+
// Check for id
|
|
150
|
+
if (kv.value === ID.value) {
|
|
151
|
+
const object = assertion.object();
|
|
152
|
+
const objectSubject = object.subject();
|
|
153
|
+
|
|
154
|
+
if (objectSubject instanceof ARID) {
|
|
155
|
+
return objectSubject;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Try to extract ARID from CBOR
|
|
159
|
+
throw new InvalidReferenceAridError();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (
|
|
164
|
+
error instanceof NotReferenceEnvelopeError ||
|
|
165
|
+
error instanceof InvalidReferenceAridError ||
|
|
166
|
+
error instanceof NoIdAssertionError
|
|
167
|
+
) {
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
// Continue searching
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw new NoIdAssertionError();
|
|
176
|
+
}
|