@le-space/orbitdb-storage-bridge 0.10.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/LICENSE +21 -0
- package/README.md +290 -0
- package/dist/components/StorachaAuth.svelte +801 -0
- package/dist/components/StorachaIntegration.svelte +965 -0
- package/dist/components/WebAuthnDIDProvider.js +562 -0
- package/dist/components/storacha-backup.js +325 -0
- package/dist/components/theme.js +79 -0
- package/lib/backends/aleph-pin.js +161 -0
- package/lib/backends/aleph.js +197 -0
- package/lib/backends/lighthouse.js +325 -0
- package/lib/backends/memory.js +140 -0
- package/lib/backends/pinata.js +330 -0
- package/lib/backends/resolve.js +72 -0
- package/lib/backends/storacha.js +178 -0
- package/lib/backends/types.js +187 -0
- package/lib/backup-car.js +1286 -0
- package/lib/backup-helpers.js +87 -0
- package/lib/backup-metadata.js +40 -0
- package/lib/backup.js +249 -0
- package/lib/block-bytes.js +39 -0
- package/lib/car-storage.js +342 -0
- package/lib/courier-sync.js +886 -0
- package/lib/dehydrate.js +146 -0
- package/lib/extract-blocks.js +258 -0
- package/lib/gateway-fetch.js +116 -0
- package/lib/ipns-helpers.js +324 -0
- package/lib/logger.js +128 -0
- package/lib/memory-courier.js +105 -0
- package/lib/orbitdb-storacha-bridge.js +2789 -0
- package/lib/pointer-ipns.js +234 -0
- package/lib/restore-cid.js +302 -0
- package/lib/ucan-bridge.js +921 -0
- package/lib/utils.js +316 -0
- package/package.json +172 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPNS Helper Functions for OrbitDB Restoration
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to publish and resolve OrbitDB metadata via IPNS
|
|
5
|
+
* Uses the ipns package (already available as transitive dependency) and Helia's libp2p DHT
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createIPNSRecord, marshalIPNSRecord, unmarshalIPNSRecord } from "ipns";
|
|
9
|
+
import { generateKeyPair } from "@libp2p/crypto/keys";
|
|
10
|
+
import { CID } from "multiformats/cid";
|
|
11
|
+
import { logger } from "./logger.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create an IPNS key pair
|
|
15
|
+
* @returns {Promise<Object>} - { privateKey, publicKey, peerId, ipnsKey }
|
|
16
|
+
*/
|
|
17
|
+
export async function createIPNSKeyPair() {
|
|
18
|
+
const keyPair = await generateKeyPair("Ed25519");
|
|
19
|
+
const peerId = await keyPair.publicKey.toPeerId();
|
|
20
|
+
|
|
21
|
+
logger.info(`🔑 Created IPNS key pair: ${peerId.toString()}`);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
privateKey: keyPair.privateKey,
|
|
25
|
+
publicKey: keyPair.publicKey,
|
|
26
|
+
peerId: peerId,
|
|
27
|
+
ipnsKey: `/ipns/${peerId.toString()}`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Publish metadata to IPNS using Helia's libp2p DHT
|
|
33
|
+
* @param {Object} helia - Helia instance
|
|
34
|
+
* @param {Object} privateKey - Private key for signing
|
|
35
|
+
* @param {Object} metadata - Metadata object to publish
|
|
36
|
+
* @param {number} lifetime - Lifetime in nanoseconds (default: 1 hour = 3600000000000ns)
|
|
37
|
+
* @returns {Promise<Object>} - { ipnsKey, metadataCID, ipnsRecord }
|
|
38
|
+
*/
|
|
39
|
+
export async function publishMetadataToIPNS(
|
|
40
|
+
helia,
|
|
41
|
+
privateKey,
|
|
42
|
+
metadata,
|
|
43
|
+
lifetime = 3600000000000,
|
|
44
|
+
) {
|
|
45
|
+
// Convert metadata to JSON and store in IPFS
|
|
46
|
+
const { unixfs: unixfsModule } = await import("@helia/unixfs");
|
|
47
|
+
const fs = unixfsModule(helia);
|
|
48
|
+
|
|
49
|
+
const jsonString = JSON.stringify(metadata, null, 2);
|
|
50
|
+
const jsonBytes = new TextEncoder().encode(jsonString);
|
|
51
|
+
|
|
52
|
+
// Create async iterable
|
|
53
|
+
async function* dataGenerator() {
|
|
54
|
+
yield jsonBytes;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Add to IPFS
|
|
58
|
+
const cid = await fs.addByteStream(dataGenerator());
|
|
59
|
+
const cidString = cid.toString();
|
|
60
|
+
|
|
61
|
+
logger.info(`📤 Stored metadata in IPFS: ${cidString}`);
|
|
62
|
+
|
|
63
|
+
// Parse CID for IPNS
|
|
64
|
+
const metadataCID = CID.parse(cidString);
|
|
65
|
+
|
|
66
|
+
// Create IPNS record
|
|
67
|
+
const sequence = 0;
|
|
68
|
+
const ipnsRecord = await createIPNSRecord(
|
|
69
|
+
privateKey,
|
|
70
|
+
metadataCID.bytes,
|
|
71
|
+
sequence,
|
|
72
|
+
lifetime,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Marshal IPNS record
|
|
76
|
+
const ipnsRecordBytes = marshalIPNSRecord(ipnsRecord);
|
|
77
|
+
|
|
78
|
+
// Publish to DHT using libp2p
|
|
79
|
+
const _libp2p = helia.libp2p;
|
|
80
|
+
const peerId = await privateKey.publicKey.toPeerId();
|
|
81
|
+
|
|
82
|
+
// Get IPNS key (peer ID as IPNS key)
|
|
83
|
+
const ipnsKey = `/ipns/${peerId.toString()}`;
|
|
84
|
+
|
|
85
|
+
// Store IPNS record
|
|
86
|
+
// For now, we'll store it in the local datastore for testing
|
|
87
|
+
// In production, this would be published to DHT
|
|
88
|
+
try {
|
|
89
|
+
logger.info(`📤 Publishing IPNS record: ${ipnsKey}`);
|
|
90
|
+
|
|
91
|
+
// Try to use @helia/ipns if available
|
|
92
|
+
let published = false;
|
|
93
|
+
try {
|
|
94
|
+
// Try @helia/ipns first
|
|
95
|
+
const { ipns: ipnsModule } = await import("@helia/ipns");
|
|
96
|
+
const name = ipnsModule(helia);
|
|
97
|
+
await name.publish(peerId, metadataCID);
|
|
98
|
+
published = true;
|
|
99
|
+
logger.info(
|
|
100
|
+
`✅ Published IPNS record using @helia/ipns: ${ipnsKey} -> ${cidString}`,
|
|
101
|
+
);
|
|
102
|
+
} catch {
|
|
103
|
+
// @helia/ipns not available, store IPNS record in IPFS for cross-node access
|
|
104
|
+
logger.info(`@helia/ipns not available, storing IPNS record in IPFS`);
|
|
105
|
+
|
|
106
|
+
// Store IPNS record in IPFS so it can be accessed by other nodes
|
|
107
|
+
const { unixfs: unixfsModule } = await import("@helia/unixfs");
|
|
108
|
+
const fs = unixfsModule(helia);
|
|
109
|
+
|
|
110
|
+
const recordGenerator = async function* () {
|
|
111
|
+
yield ipnsRecordBytes;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const recordCID = await fs.addByteStream(recordGenerator());
|
|
115
|
+
const recordCIDString = recordCID.toString();
|
|
116
|
+
|
|
117
|
+
// Store mapping in IPFS as well, so other nodes can find it
|
|
118
|
+
// Create a mapping object: { peerId: recordCID }
|
|
119
|
+
const mapping = {
|
|
120
|
+
peerId: peerId.toString(),
|
|
121
|
+
recordCID: recordCIDString,
|
|
122
|
+
metadataCID: cidString,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const mappingGenerator = async function* () {
|
|
126
|
+
yield new TextEncoder().encode(JSON.stringify(mapping));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const mappingCID = await fs.addByteStream(mappingGenerator());
|
|
130
|
+
const mappingCIDString = mappingCID.toString();
|
|
131
|
+
|
|
132
|
+
// Store mapping CID in local datastore for quick lookup
|
|
133
|
+
const datastore = helia.datastore;
|
|
134
|
+
const ipnsKeyBytes = new TextEncoder().encode(
|
|
135
|
+
`/ipns/${peerId.toString()}`,
|
|
136
|
+
);
|
|
137
|
+
const mappingCIDBytes = new TextEncoder().encode(mappingCIDString);
|
|
138
|
+
await datastore.put(ipnsKeyBytes, mappingCIDBytes);
|
|
139
|
+
|
|
140
|
+
published = true;
|
|
141
|
+
logger.info(
|
|
142
|
+
`✅ Stored IPNS record in IPFS: ${ipnsKey} -> ${cidString} (record: ${recordCIDString}, mapping: ${mappingCIDString})`,
|
|
143
|
+
);
|
|
144
|
+
logger.info(
|
|
145
|
+
`⚠️ Note: Using IPFS storage for testing. For production, use DHT or @helia/ipns.`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Return mapping CID for cross-node access
|
|
149
|
+
return {
|
|
150
|
+
ipnsKey,
|
|
151
|
+
metadataCID: cidString,
|
|
152
|
+
ipnsRecord: ipnsRecordBytes,
|
|
153
|
+
published,
|
|
154
|
+
mappingCID: mappingCIDString, // For cross-node access
|
|
155
|
+
recordCID: recordCIDString,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
ipnsKey,
|
|
161
|
+
metadataCID: cidString,
|
|
162
|
+
ipnsRecord: ipnsRecordBytes,
|
|
163
|
+
published,
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
logger.error(`Error publishing to IPNS: ${error.message}`);
|
|
167
|
+
// Fallback: return the record even if publish fails
|
|
168
|
+
logger.warn(
|
|
169
|
+
`⚠️ IPNS publish failed, but IPNS record created: ${error.message}`,
|
|
170
|
+
);
|
|
171
|
+
return {
|
|
172
|
+
ipnsKey,
|
|
173
|
+
metadataCID: cidString,
|
|
174
|
+
ipnsRecord: ipnsRecordBytes,
|
|
175
|
+
published: false,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolve IPNS key to get metadata CID
|
|
182
|
+
* @param {Object} helia - Helia instance
|
|
183
|
+
* @param {Object} publicKey - Public key or peer ID
|
|
184
|
+
* @returns {Promise<string>} - Metadata CID
|
|
185
|
+
*/
|
|
186
|
+
export async function resolveIPNS(helia, publicKey) {
|
|
187
|
+
const _libp2p = helia.libp2p;
|
|
188
|
+
const peerId = await publicKey.toPeerId();
|
|
189
|
+
const ipnsKey = `/ipns/${peerId.toString()}`;
|
|
190
|
+
|
|
191
|
+
logger.info(`📥 Resolving IPNS key: ${ipnsKey}`);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Try to use @helia/ipns if available, otherwise use local datastore
|
|
195
|
+
try {
|
|
196
|
+
// Try @helia/ipns first
|
|
197
|
+
const { ipns: ipnsModule } = await import("@helia/ipns");
|
|
198
|
+
const name = ipnsModule(helia);
|
|
199
|
+
const resolvedCID = await name.resolve(peerId);
|
|
200
|
+
const cidString = resolvedCID.toString();
|
|
201
|
+
logger.info(`✅ Resolved IPNS using @helia/ipns to CID: ${cidString}`);
|
|
202
|
+
return cidString;
|
|
203
|
+
} catch (importError) {
|
|
204
|
+
// @helia/ipns not available, read from IPFS
|
|
205
|
+
logger.info(`@helia/ipns not available, reading from IPFS`);
|
|
206
|
+
|
|
207
|
+
// Try to get mapping CID from local datastore first
|
|
208
|
+
let mappingCIDString = null;
|
|
209
|
+
try {
|
|
210
|
+
const datastore = helia.datastore;
|
|
211
|
+
const ipnsKeyBytes = new TextEncoder().encode(
|
|
212
|
+
`/ipns/${peerId.toString()}`,
|
|
213
|
+
);
|
|
214
|
+
const mappingCIDBytes = await datastore.get(ipnsKeyBytes);
|
|
215
|
+
if (mappingCIDBytes && mappingCIDBytes.length > 0) {
|
|
216
|
+
mappingCIDString = new TextDecoder().decode(mappingCIDBytes);
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// Mapping not in local datastore, will need to search IPFS
|
|
220
|
+
logger.info(`Mapping not in local datastore, will search IPFS`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// If we have mapping CID, use it; otherwise we need to find it
|
|
224
|
+
// For now, if mapping is not found, throw error
|
|
225
|
+
// In production, you would search DHT or use a different discovery mechanism
|
|
226
|
+
if (!mappingCIDString) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`IPNS record mapping not found for ${ipnsKey}. The mapping must be published first.`, { cause: importError },
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Download mapping from IPFS
|
|
233
|
+
const { unixfs: unixfsModule } = await import("@helia/unixfs");
|
|
234
|
+
const fs = unixfsModule(helia);
|
|
235
|
+
const mappingCID = CID.parse(mappingCIDString);
|
|
236
|
+
|
|
237
|
+
const mappingChunks = [];
|
|
238
|
+
for await (const chunk of fs.cat(mappingCID)) {
|
|
239
|
+
mappingChunks.push(chunk);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const mappingLength = mappingChunks.reduce(
|
|
243
|
+
(acc, chunk) => acc + chunk.length,
|
|
244
|
+
0,
|
|
245
|
+
);
|
|
246
|
+
const mappingBytes = new Uint8Array(mappingLength);
|
|
247
|
+
let mappingOffset = 0;
|
|
248
|
+
for (const chunk of mappingChunks) {
|
|
249
|
+
mappingBytes.set(chunk, mappingOffset);
|
|
250
|
+
mappingOffset += chunk.length;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const mapping = JSON.parse(new TextDecoder().decode(mappingBytes));
|
|
254
|
+
const recordCIDString = mapping.recordCID;
|
|
255
|
+
const recordCID = CID.parse(recordCIDString);
|
|
256
|
+
|
|
257
|
+
// Download IPNS record from IPFS
|
|
258
|
+
const chunks = [];
|
|
259
|
+
for await (const chunk of fs.cat(recordCID)) {
|
|
260
|
+
chunks.push(chunk);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Combine chunks
|
|
264
|
+
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
265
|
+
const ipnsRecordBytes = new Uint8Array(totalLength);
|
|
266
|
+
let offset = 0;
|
|
267
|
+
for (const chunk of chunks) {
|
|
268
|
+
ipnsRecordBytes.set(chunk, offset);
|
|
269
|
+
offset += chunk.length;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Unmarshal IPNS record
|
|
273
|
+
const ipnsRecord = unmarshalIPNSRecord(ipnsRecordBytes);
|
|
274
|
+
|
|
275
|
+
// Extract CID from IPNS record
|
|
276
|
+
const cidBytes = ipnsRecord.value;
|
|
277
|
+
const cid = CID.decode(cidBytes);
|
|
278
|
+
const cidString = cid.toString();
|
|
279
|
+
|
|
280
|
+
logger.info(`✅ Resolved IPNS from IPFS to CID: ${cidString}`);
|
|
281
|
+
|
|
282
|
+
return cidString;
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
logger.error(`Error resolving IPNS: ${error.message}`);
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Download and parse metadata from IPFS
|
|
292
|
+
* @param {Object} helia - Helia instance
|
|
293
|
+
* @param {string} cid - CID of metadata
|
|
294
|
+
* @returns {Promise<Object>} - Parsed metadata object
|
|
295
|
+
*/
|
|
296
|
+
export async function getMetadataFromIPFS(helia, cid) {
|
|
297
|
+
const { unixfs: unixfsModule } = await import("@helia/unixfs");
|
|
298
|
+
const fs = unixfsModule(helia);
|
|
299
|
+
|
|
300
|
+
const cidObj = CID.parse(cid);
|
|
301
|
+
|
|
302
|
+
// Download from IPFS network
|
|
303
|
+
const chunks = [];
|
|
304
|
+
for await (const chunk of fs.cat(cidObj)) {
|
|
305
|
+
chunks.push(chunk);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Combine chunks
|
|
309
|
+
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
310
|
+
const allBytes = new Uint8Array(totalLength);
|
|
311
|
+
let offset = 0;
|
|
312
|
+
for (const chunk of chunks) {
|
|
313
|
+
allBytes.set(chunk, offset);
|
|
314
|
+
offset += chunk.length;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Parse JSON
|
|
318
|
+
const jsonString = new TextDecoder().decode(allBytes);
|
|
319
|
+
const metadata = JSON.parse(jsonString);
|
|
320
|
+
|
|
321
|
+
logger.info(`📄 Retrieved metadata from IPFS`);
|
|
322
|
+
|
|
323
|
+
return metadata;
|
|
324
|
+
}
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrbitDB Storage Bridge - Logger
|
|
3
|
+
*
|
|
4
|
+
* Uses @libp2p/logger for consistent logging across the libp2p ecosystem.
|
|
5
|
+
*
|
|
6
|
+
* To enable logging:
|
|
7
|
+
* - Node.js: DEBUG=libp2p:orbitdb-storacha:* node script.js
|
|
8
|
+
* - Browser: localStorage.setItem('debug', 'libp2p:orbitdb-storacha:*')
|
|
9
|
+
*
|
|
10
|
+
* Available formatters:
|
|
11
|
+
* - %s - string
|
|
12
|
+
* - %o - object
|
|
13
|
+
* - %d - number
|
|
14
|
+
* - %p - peer ID
|
|
15
|
+
* - %b - base58btc encoded data
|
|
16
|
+
* - %t - base32 encoded data
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { logger as libp2pLogger } from "@libp2p/logger";
|
|
20
|
+
|
|
21
|
+
// The default logger. Its namespace keeps the package's old name, so existing DEBUG settings still work.
|
|
22
|
+
export const logger = libp2pLogger("libp2p:orbitdb-storacha:bridge");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a child logger with a specific namespace
|
|
26
|
+
* @param {string} namespace - Namespace for the child logger (will be appended to libp2p:orbitdb-storacha:)
|
|
27
|
+
* @returns {Function} Logger function
|
|
28
|
+
*/
|
|
29
|
+
export function createChildLogger(namespace) {
|
|
30
|
+
return libp2pLogger(`libp2p:orbitdb-storacha:${namespace}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a logger for a specific component
|
|
35
|
+
* @param {string} component - Component name
|
|
36
|
+
* @returns {Function} Logger function
|
|
37
|
+
*/
|
|
38
|
+
export function createLogger(component) {
|
|
39
|
+
return libp2pLogger(`libp2p:orbitdb-storacha:${component}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const LOG_LEVELS = {
|
|
43
|
+
TRACE: "trace",
|
|
44
|
+
DEBUG: "debug",
|
|
45
|
+
INFO: "info",
|
|
46
|
+
WARN: "warn",
|
|
47
|
+
ERROR: "error",
|
|
48
|
+
FATAL: "fatal",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const logUtils = {
|
|
52
|
+
/**
|
|
53
|
+
* Log function entry with parameters
|
|
54
|
+
* @param {string} functionName - Name of the function
|
|
55
|
+
* @param {Object} params - Function parameters
|
|
56
|
+
* @param {Function} childLogger - Optional child logger
|
|
57
|
+
*/
|
|
58
|
+
functionEntry: (functionName, params = {}, childLogger = logger) => {
|
|
59
|
+
childLogger(`Entering ${functionName} with params: %o`, params);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Log function exit with result
|
|
64
|
+
* @param {string} functionName - Name of the function
|
|
65
|
+
* @param {*} result - Function result
|
|
66
|
+
* @param {Function} childLogger - Optional child logger
|
|
67
|
+
*/
|
|
68
|
+
functionExit: (functionName, result, childLogger = logger) => {
|
|
69
|
+
childLogger(`Exiting ${functionName} with result: %o`, result);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Log operation progress
|
|
74
|
+
* @param {string} operation - Operation name
|
|
75
|
+
* @param {number} current - Current progress
|
|
76
|
+
* @param {number} total - Total items
|
|
77
|
+
* @param {Function} childLogger - Optional child logger
|
|
78
|
+
*/
|
|
79
|
+
progress: (operation, current, total, childLogger = logger) => {
|
|
80
|
+
const percentage = Math.round((current / total) * 100);
|
|
81
|
+
childLogger(`${operation}: ${current}/${total} (${percentage}%)`);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Log timing information
|
|
86
|
+
* @param {string} operation - Operation name
|
|
87
|
+
* @param {number} startTime - Start time (from Date.now())
|
|
88
|
+
* @param {Function} childLogger - Optional child logger
|
|
89
|
+
*/
|
|
90
|
+
timing: (operation, startTime, childLogger = logger) => {
|
|
91
|
+
const duration = Date.now() - startTime;
|
|
92
|
+
childLogger(`${operation} completed in ${duration}ms`);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Compatibility wrappers for common log levels
|
|
98
|
+
* libp2p logger is a function, so we add these for convenience
|
|
99
|
+
*/
|
|
100
|
+
logger.info = logger;
|
|
101
|
+
logger.debug = logger;
|
|
102
|
+
logger.error = logger.error || logger;
|
|
103
|
+
logger.warn = logger;
|
|
104
|
+
logger.trace = logger;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* No-op functions for compatibility
|
|
108
|
+
* libp2p logger is controlled via DEBUG environment variable
|
|
109
|
+
*/
|
|
110
|
+
export function setLogLevel(level) {
|
|
111
|
+
// No-op: libp2p logger uses DEBUG env var
|
|
112
|
+
logger(
|
|
113
|
+
`setLogLevel called with ${level} - use DEBUG environment variable instead`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function disableLogging() {
|
|
118
|
+
// No-op: libp2p logger uses DEBUG env var
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function enableLogging(level = "info") {
|
|
122
|
+
// No-op: libp2p logger uses DEBUG env var
|
|
123
|
+
logger(
|
|
124
|
+
`enableLogging called with ${level} - use DEBUG environment variable instead`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default logger;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Courier — an in-memory pair of byte couriers for testing courier-sync
|
|
3
|
+
* without any radio, network or filesystem.
|
|
4
|
+
*
|
|
5
|
+
* Implements the seam contract from courier-sync.js and misbehaves on request:
|
|
6
|
+
* messages can be dropped, duplicated and delivered out of order, because a
|
|
7
|
+
* real courier (a LoRa mesh above all) does all three. The GPL-side Meshtastic
|
|
8
|
+
* courier is expected to test its protocol against this same contract.
|
|
9
|
+
*
|
|
10
|
+
* Deliveries are asynchronous (macrotask) so both ends always see the same
|
|
11
|
+
* causality a real courier would give them; `pair.idle()` resolves when no
|
|
12
|
+
* message is in flight on either side.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {Object} [options]
|
|
17
|
+
* @param {"fifo"|"lifo"} [options.order="fifo"] "lifo" reverses each drained
|
|
18
|
+
* batch — a deterministic worst case for reordering tolerance.
|
|
19
|
+
* @param {(info: {seq: number, from: "a"|"b", bytes: Uint8Array}) => boolean} [options.dropFn]
|
|
20
|
+
* Return true to lose that message.
|
|
21
|
+
* @param {(info: {seq: number, from: "a"|"b", bytes: Uint8Array}) => boolean} [options.duplicateFn]
|
|
22
|
+
* Return true to deliver that message twice.
|
|
23
|
+
* @returns {{a: Object, b: Object, idle: () => Promise<void>, stats: Object}}
|
|
24
|
+
*/
|
|
25
|
+
export function createMemoryCourierPair(options = {}) {
|
|
26
|
+
const { order = "fifo", dropFn = null, duplicateFn = null } = options;
|
|
27
|
+
|
|
28
|
+
const stats = { sent: 0, delivered: 0, dropped: 0, duplicated: 0 };
|
|
29
|
+
let seq = 0;
|
|
30
|
+
let inFlight = 0;
|
|
31
|
+
const idleWaiters = [];
|
|
32
|
+
|
|
33
|
+
const settle = () => {
|
|
34
|
+
if (inFlight === 0) {
|
|
35
|
+
while (idleWaiters.length > 0) idleWaiters.shift()();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const makeEnd = (name) => {
|
|
40
|
+
const listeners = new Set();
|
|
41
|
+
const queue = [];
|
|
42
|
+
let drainScheduled = false;
|
|
43
|
+
|
|
44
|
+
const drain = () => {
|
|
45
|
+
drainScheduled = false;
|
|
46
|
+
const batch =
|
|
47
|
+
order === "lifo" ? queue.splice(0).reverse() : queue.splice(0);
|
|
48
|
+
for (const bytes of batch) {
|
|
49
|
+
stats.delivered++;
|
|
50
|
+
for (const cb of listeners) cb(bytes);
|
|
51
|
+
inFlight--;
|
|
52
|
+
}
|
|
53
|
+
settle();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
listeners,
|
|
58
|
+
deliver(bytes) {
|
|
59
|
+
inFlight++;
|
|
60
|
+
queue.push(bytes);
|
|
61
|
+
if (!drainScheduled) {
|
|
62
|
+
drainScheduled = true;
|
|
63
|
+
setTimeout(drain, 0);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
name,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const endA = makeEnd("a");
|
|
71
|
+
const endB = makeEnd("b");
|
|
72
|
+
|
|
73
|
+
const makeCourier = (from, remote) => ({
|
|
74
|
+
async send(bytes) {
|
|
75
|
+
stats.sent++;
|
|
76
|
+
const info = { seq: seq++, from, bytes };
|
|
77
|
+
if (dropFn && dropFn(info)) {
|
|
78
|
+
stats.dropped++;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
remote.deliver(bytes);
|
|
82
|
+
if (duplicateFn && duplicateFn(info)) {
|
|
83
|
+
stats.duplicated++;
|
|
84
|
+
remote.deliver(bytes);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
onPayload(cb) {
|
|
88
|
+
const local = from === "a" ? endA : endB;
|
|
89
|
+
local.listeners.add(cb);
|
|
90
|
+
return () => local.listeners.delete(cb);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
a: makeCourier("a", endB),
|
|
96
|
+
b: makeCourier("b", endA),
|
|
97
|
+
stats,
|
|
98
|
+
idle() {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
idleWaiters.push(resolve);
|
|
101
|
+
settle();
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|