@freqhole/api-client 0.1.32 → 0.2.2
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/package.json +7 -16
- package/src/CharnelTransport.ts +7 -7
- package/src/WasmTransport.ts +23 -22
- package/src/permissions.ts +16 -29
- package/src/pollingJobEvents.test.ts +192 -0
- package/src/test/permissions.ts +185 -0
- package/src/test.ts +5 -14
- package/src/transport.ts +10 -3
- package/vitest.config.ts +8 -0
- package/src/automerge/IrohNetworkAdapter.ts +0 -759
- package/src/automerge/index.ts +0 -14
- package/src/storage/blobs.ts +0 -272
- package/src/storage/identity.ts +0 -190
- package/src/storage/index.ts +0 -5
- package/src/storage/webLocks.ts +0 -92
- package/src/test/automerge.ts +0 -796
- package/src/test/storage.ts +0 -443
package/src/test/storage.ts
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
// tests for storage/identity.ts and storage/webLocks.ts
|
|
2
|
-
//
|
|
3
|
-
// these tests run in node (tsx) which has no real IndexedDB or navigator.locks.
|
|
4
|
-
// they stub those globals with minimal in-memory implementations to exercise the
|
|
5
|
-
// policy logic without a browser environment.
|
|
6
|
-
|
|
7
|
-
import type { IdentityStore, P2PIdentity } from "../storage/identity.js";
|
|
8
|
-
import {
|
|
9
|
-
resolveIdentity,
|
|
10
|
-
persistIdentity,
|
|
11
|
-
} from "../storage/identity.js";
|
|
12
|
-
import { acquireNodeLeadership, LOCK_NAME } from "../storage/webLocks.js";
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// helpers
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
function makeIdentity(nodeId = "node-abc"): P2PIdentity {
|
|
19
|
-
return {
|
|
20
|
-
id: "p2p_identity",
|
|
21
|
-
secret_key: new Uint8Array([1, 2, 3]),
|
|
22
|
-
node_id: nodeId,
|
|
23
|
-
created_at: 1000,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function makeLocalStore(
|
|
28
|
-
initial: P2PIdentity | null = null,
|
|
29
|
-
): IdentityStore & { stored: P2PIdentity | null } {
|
|
30
|
-
let stored: P2PIdentity | null = initial;
|
|
31
|
-
return {
|
|
32
|
-
get stored() {
|
|
33
|
-
return stored;
|
|
34
|
-
},
|
|
35
|
-
async get() {
|
|
36
|
-
return stored;
|
|
37
|
-
},
|
|
38
|
-
async set(identity) {
|
|
39
|
-
stored = identity;
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type StoreName = string;
|
|
45
|
-
|
|
46
|
-
/** minimal fake IDB: one database, configurable stores and records */
|
|
47
|
-
function stubIndexedDB(opts: {
|
|
48
|
-
databases: Array<{ name: string; version: number }>;
|
|
49
|
-
stores: Record<string, Record<string, unknown>>;
|
|
50
|
-
}) {
|
|
51
|
-
const dbData = opts.stores;
|
|
52
|
-
|
|
53
|
-
const fakeIDB = {
|
|
54
|
-
databases: async () => opts.databases,
|
|
55
|
-
open: (name: string) => {
|
|
56
|
-
const storeNames = Object.keys(dbData);
|
|
57
|
-
const req: Partial<IDBOpenDBRequest> & {
|
|
58
|
-
onsuccess: ((ev: Event) => void) | null;
|
|
59
|
-
onerror: ((ev: Event) => void) | null;
|
|
60
|
-
onupgradeneeded: ((ev: IDBVersionChangeEvent) => void) | null;
|
|
61
|
-
transaction: IDBTransaction | null;
|
|
62
|
-
result: IDBDatabase;
|
|
63
|
-
} = {
|
|
64
|
-
onsuccess: null,
|
|
65
|
-
onerror: null,
|
|
66
|
-
onupgradeneeded: null,
|
|
67
|
-
transaction: null,
|
|
68
|
-
result: undefined as unknown as IDBDatabase,
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const fakeDB: Partial<IDBDatabase> = {
|
|
72
|
-
objectStoreNames: {
|
|
73
|
-
contains: (storeName: StoreName) =>
|
|
74
|
-
storeNames.includes(storeName),
|
|
75
|
-
} as DOMStringList,
|
|
76
|
-
close() {},
|
|
77
|
-
transaction(storeName: string | string[], _mode?: IDBTransactionMode) {
|
|
78
|
-
const sName = Array.isArray(storeName) ? storeName[0] : storeName;
|
|
79
|
-
const store = dbData[sName] ?? {};
|
|
80
|
-
const os: Partial<IDBObjectStore> = {
|
|
81
|
-
get(key: IDBValidKey) {
|
|
82
|
-
const getReq: Partial<IDBRequest> & {
|
|
83
|
-
onsuccess: ((ev: Event) => void) | null;
|
|
84
|
-
onerror: ((ev: Event) => void) | null;
|
|
85
|
-
result: unknown;
|
|
86
|
-
error: DOMException | null;
|
|
87
|
-
} = {
|
|
88
|
-
onsuccess: null,
|
|
89
|
-
onerror: null,
|
|
90
|
-
result: store[key as string] ?? undefined,
|
|
91
|
-
error: null,
|
|
92
|
-
};
|
|
93
|
-
// fire async so caller can set handlers first
|
|
94
|
-
queueMicrotask(() => getReq.onsuccess?.({} as Event));
|
|
95
|
-
return getReq as IDBRequest;
|
|
96
|
-
},
|
|
97
|
-
put(value: unknown, key?: IDBValidKey) {
|
|
98
|
-
if (key !== undefined) store[key as string] = value;
|
|
99
|
-
const putReq: Partial<IDBRequest> & {
|
|
100
|
-
onsuccess: ((ev: Event) => void) | null;
|
|
101
|
-
onerror: ((ev: Event) => void) | null;
|
|
102
|
-
result: unknown;
|
|
103
|
-
error: DOMException | null;
|
|
104
|
-
} = {
|
|
105
|
-
onsuccess: null,
|
|
106
|
-
onerror: null,
|
|
107
|
-
result: key,
|
|
108
|
-
error: null,
|
|
109
|
-
};
|
|
110
|
-
queueMicrotask(() => putReq.onsuccess?.({} as Event));
|
|
111
|
-
return putReq as IDBRequest;
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
const tx: Partial<IDBTransaction> = {
|
|
115
|
-
objectStore: () => os as IDBObjectStore,
|
|
116
|
-
};
|
|
117
|
-
return tx as IDBTransaction;
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
req.result = fakeDB as IDBDatabase;
|
|
122
|
-
|
|
123
|
-
if (!opts.databases.some((d) => d.name === name)) {
|
|
124
|
-
// db does not exist - fire upgradeneeded then let abort handler run
|
|
125
|
-
queueMicrotask(() => {
|
|
126
|
-
if (req.onupgradeneeded) {
|
|
127
|
-
const ev = {
|
|
128
|
-
target: { transaction: null },
|
|
129
|
-
} as unknown as IDBVersionChangeEvent;
|
|
130
|
-
req.onupgradeneeded(ev);
|
|
131
|
-
}
|
|
132
|
-
req.onerror?.({} as Event);
|
|
133
|
-
});
|
|
134
|
-
} else {
|
|
135
|
-
queueMicrotask(() => req.onsuccess?.({} as Event));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return req as IDBOpenDBRequest;
|
|
139
|
-
},
|
|
140
|
-
deleteDatabase: (_name: string) => ({} as IDBOpenDBRequest),
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
return fakeIDB;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
// test runner helpers (matches existing test/ conventions)
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
export async function runStorageTests(): Promise<{
|
|
151
|
-
passed: number;
|
|
152
|
-
failed: number;
|
|
153
|
-
}> {
|
|
154
|
-
let passed = 0;
|
|
155
|
-
let failed = 0;
|
|
156
|
-
|
|
157
|
-
async function test(name: string, fn: () => void | Promise<void>) {
|
|
158
|
-
try {
|
|
159
|
-
await fn();
|
|
160
|
-
console.log(`✓ ${name}`);
|
|
161
|
-
passed++;
|
|
162
|
-
} catch (err) {
|
|
163
|
-
console.log(
|
|
164
|
-
`✗ ${name}: ${err instanceof Error ? err.message : String(err)}`,
|
|
165
|
-
);
|
|
166
|
-
failed++;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function assert(condition: boolean, msg: string) {
|
|
171
|
-
if (!condition) throw new Error(msg);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
console.log("running storage tests...\n");
|
|
175
|
-
|
|
176
|
-
// ---------------------------------------------------------------------------
|
|
177
|
-
// identity - freqhole_app absent
|
|
178
|
-
// ---------------------------------------------------------------------------
|
|
179
|
-
|
|
180
|
-
await test(
|
|
181
|
-
"resolveIdentity - returns local identity when freqhole_app absent",
|
|
182
|
-
async () => {
|
|
183
|
-
const identity = makeIdentity("node-local");
|
|
184
|
-
const local = makeLocalStore(identity);
|
|
185
|
-
|
|
186
|
-
// stub indexedDB with no databases
|
|
187
|
-
const fakeIDB = stubIndexedDB({ databases: [], stores: {} });
|
|
188
|
-
const origIDB = (globalThis as Record<string, unknown>).indexedDB;
|
|
189
|
-
(globalThis as Record<string, unknown>).indexedDB = fakeIDB;
|
|
190
|
-
try {
|
|
191
|
-
const result = await resolveIdentity(local);
|
|
192
|
-
assert(result?.node_id === "node-local", "expected local identity");
|
|
193
|
-
} finally {
|
|
194
|
-
(globalThis as Record<string, unknown>).indexedDB = origIDB;
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
await test(
|
|
200
|
-
"persistIdentity - writes to local store when freqhole_app absent",
|
|
201
|
-
async () => {
|
|
202
|
-
const identity = makeIdentity("node-new");
|
|
203
|
-
const local = makeLocalStore(null);
|
|
204
|
-
|
|
205
|
-
const fakeIDB = stubIndexedDB({ databases: [], stores: {} });
|
|
206
|
-
const origIDB = (globalThis as Record<string, unknown>).indexedDB;
|
|
207
|
-
(globalThis as Record<string, unknown>).indexedDB = fakeIDB;
|
|
208
|
-
try {
|
|
209
|
-
await persistIdentity(identity, local);
|
|
210
|
-
assert(local.stored?.node_id === "node-new", "expected identity in local store");
|
|
211
|
-
} finally {
|
|
212
|
-
(globalThis as Record<string, unknown>).indexedDB = origIDB;
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
// ---------------------------------------------------------------------------
|
|
218
|
-
// identity - freqhole_app present
|
|
219
|
-
// ---------------------------------------------------------------------------
|
|
220
|
-
|
|
221
|
-
await test(
|
|
222
|
-
"resolveIdentity - prefers spume identity when freqhole_app has one",
|
|
223
|
-
async () => {
|
|
224
|
-
const spumeIdentity = makeIdentity("node-spume");
|
|
225
|
-
const localIdentity = makeIdentity("node-local");
|
|
226
|
-
const local = makeLocalStore(localIdentity);
|
|
227
|
-
|
|
228
|
-
const fakeIDB = stubIndexedDB({
|
|
229
|
-
databases: [{ name: "freqhole_app", version: 1 }],
|
|
230
|
-
stores: {
|
|
231
|
-
app_state: { p2p_identity: spumeIdentity },
|
|
232
|
-
},
|
|
233
|
-
});
|
|
234
|
-
const origIDB = (globalThis as Record<string, unknown>).indexedDB;
|
|
235
|
-
(globalThis as Record<string, unknown>).indexedDB = fakeIDB;
|
|
236
|
-
try {
|
|
237
|
-
const result = await resolveIdentity(local);
|
|
238
|
-
assert(result?.node_id === "node-spume", "expected spume identity to win");
|
|
239
|
-
} finally {
|
|
240
|
-
(globalThis as Record<string, unknown>).indexedDB = origIDB;
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
await test(
|
|
246
|
-
"resolveIdentity - falls back to local when freqhole_app has no identity",
|
|
247
|
-
async () => {
|
|
248
|
-
const localIdentity = makeIdentity("node-local");
|
|
249
|
-
const local = makeLocalStore(localIdentity);
|
|
250
|
-
|
|
251
|
-
const fakeIDB = stubIndexedDB({
|
|
252
|
-
databases: [{ name: "freqhole_app", version: 1 }],
|
|
253
|
-
stores: { app_state: {} }, // store exists but no identity
|
|
254
|
-
});
|
|
255
|
-
const origIDB = (globalThis as Record<string, unknown>).indexedDB;
|
|
256
|
-
(globalThis as Record<string, unknown>).indexedDB = fakeIDB;
|
|
257
|
-
try {
|
|
258
|
-
const result = await resolveIdentity(local);
|
|
259
|
-
assert(result?.node_id === "node-local", "expected local fallback");
|
|
260
|
-
} finally {
|
|
261
|
-
(globalThis as Record<string, unknown>).indexedDB = origIDB;
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
await test(
|
|
267
|
-
"persistIdentity - writes to freqhole_app when it exists",
|
|
268
|
-
async () => {
|
|
269
|
-
const identity = makeIdentity("node-new");
|
|
270
|
-
const local = makeLocalStore(null);
|
|
271
|
-
const storeData: Record<string, unknown> = {};
|
|
272
|
-
|
|
273
|
-
const fakeIDB = stubIndexedDB({
|
|
274
|
-
databases: [{ name: "freqhole_app", version: 1 }],
|
|
275
|
-
stores: { app_state: storeData },
|
|
276
|
-
});
|
|
277
|
-
const origIDB = (globalThis as Record<string, unknown>).indexedDB;
|
|
278
|
-
(globalThis as Record<string, unknown>).indexedDB = fakeIDB;
|
|
279
|
-
try {
|
|
280
|
-
await persistIdentity(identity, local);
|
|
281
|
-
assert(
|
|
282
|
-
(storeData["p2p_identity"] as P2PIdentity | undefined)?.node_id === "node-new",
|
|
283
|
-
"expected identity written to freqhole_app",
|
|
284
|
-
);
|
|
285
|
-
assert(local.stored === null, "expected local store untouched");
|
|
286
|
-
} finally {
|
|
287
|
-
(globalThis as Record<string, unknown>).indexedDB = origIDB;
|
|
288
|
-
}
|
|
289
|
-
},
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
// ---------------------------------------------------------------------------
|
|
293
|
-
// webLocks - unsupported fallback
|
|
294
|
-
// ---------------------------------------------------------------------------
|
|
295
|
-
|
|
296
|
-
// navigator is read-only on globalThis in Node.js; use defineProperty to stub it
|
|
297
|
-
function stubNavigator(value: unknown): () => void {
|
|
298
|
-
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "navigator");
|
|
299
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
300
|
-
configurable: true,
|
|
301
|
-
writable: true,
|
|
302
|
-
value,
|
|
303
|
-
});
|
|
304
|
-
return () => {
|
|
305
|
-
if (descriptor) {
|
|
306
|
-
Object.defineProperty(globalThis, "navigator", descriptor);
|
|
307
|
-
} else {
|
|
308
|
-
// was not originally defined - remove our stub
|
|
309
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
310
|
-
configurable: true,
|
|
311
|
-
writable: true,
|
|
312
|
-
value: undefined,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
await test(
|
|
319
|
-
"acquireNodeLeadership - calls onAcquired immediately when navigator.locks absent",
|
|
320
|
-
async () => {
|
|
321
|
-
const restore = stubNavigator(undefined);
|
|
322
|
-
try {
|
|
323
|
-
const states: string[] = [];
|
|
324
|
-
let acquired = false;
|
|
325
|
-
|
|
326
|
-
const cancel = acquireNodeLeadership({
|
|
327
|
-
onAcquired: () => {
|
|
328
|
-
acquired = true;
|
|
329
|
-
},
|
|
330
|
-
onStateChange: (s) => states.push(s),
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// give the microtask queue a tick
|
|
334
|
-
await Promise.resolve();
|
|
335
|
-
|
|
336
|
-
assert(states[0] === "unsupported", "expected unsupported state");
|
|
337
|
-
assert(acquired, "expected onAcquired to be called");
|
|
338
|
-
cancel(); // no-op
|
|
339
|
-
} finally {
|
|
340
|
-
restore();
|
|
341
|
-
}
|
|
342
|
-
},
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
await test(
|
|
346
|
-
"acquireNodeLeadership - exports LOCK_NAME constant",
|
|
347
|
-
() => {
|
|
348
|
-
assert(LOCK_NAME === "freqhole-iroh-node", "wrong lock name");
|
|
349
|
-
},
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
// ---------------------------------------------------------------------------
|
|
353
|
-
// webLocks - navigator.locks stub (granted immediately via ifAvailable)
|
|
354
|
-
// ---------------------------------------------------------------------------
|
|
355
|
-
|
|
356
|
-
await test(
|
|
357
|
-
"acquireNodeLeadership - calls onAcquired when lock granted",
|
|
358
|
-
async () => {
|
|
359
|
-
const states: string[] = [];
|
|
360
|
-
let acquired = false;
|
|
361
|
-
|
|
362
|
-
// stub navigator.locks where ifAvailable returns a non-null lock
|
|
363
|
-
const restore = stubNavigator({
|
|
364
|
-
locks: {
|
|
365
|
-
request: async (
|
|
366
|
-
_name: string,
|
|
367
|
-
opts: { ifAvailable?: boolean },
|
|
368
|
-
cb: (lock: Lock | null) => Promise<void>,
|
|
369
|
-
) => {
|
|
370
|
-
if (opts.ifAvailable) {
|
|
371
|
-
// grant immediately
|
|
372
|
-
await cb({ name: _name, mode: "exclusive" } as Lock);
|
|
373
|
-
}
|
|
374
|
-
},
|
|
375
|
-
},
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
try {
|
|
379
|
-
const cancel = acquireNodeLeadership({
|
|
380
|
-
onAcquired: () => {
|
|
381
|
-
acquired = true;
|
|
382
|
-
},
|
|
383
|
-
onStateChange: (s) => states.push(s),
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// the lock request is fire-and-forget; wait a few microtasks
|
|
387
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
388
|
-
cancel();
|
|
389
|
-
|
|
390
|
-
assert(states.includes("leader"), "expected leader state");
|
|
391
|
-
assert(acquired, "expected onAcquired to be called");
|
|
392
|
-
} finally {
|
|
393
|
-
restore();
|
|
394
|
-
}
|
|
395
|
-
},
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
await test(
|
|
399
|
-
"acquireNodeLeadership - reports waiting when lock not available",
|
|
400
|
-
async () => {
|
|
401
|
-
const states: string[] = [];
|
|
402
|
-
|
|
403
|
-
// stub: ifAvailable returns null (lock held elsewhere), queued request never resolves
|
|
404
|
-
const restore = stubNavigator({
|
|
405
|
-
locks: {
|
|
406
|
-
request: async (
|
|
407
|
-
_name: string,
|
|
408
|
-
opts: { ifAvailable?: boolean; signal?: AbortSignal },
|
|
409
|
-
cb: (lock: Lock | null) => Promise<void>,
|
|
410
|
-
) => {
|
|
411
|
-
if (opts.ifAvailable) {
|
|
412
|
-
// lock held - return null
|
|
413
|
-
await cb(null);
|
|
414
|
-
} else {
|
|
415
|
-
// wait forever (or until signal aborts)
|
|
416
|
-
await new Promise<void>((_, reject) => {
|
|
417
|
-
opts.signal?.addEventListener("abort", () =>
|
|
418
|
-
reject(new DOMException("aborted", "AbortError")),
|
|
419
|
-
);
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
},
|
|
423
|
-
},
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
try {
|
|
427
|
-
const cancel = acquireNodeLeadership({
|
|
428
|
-
onAcquired: () => {},
|
|
429
|
-
onStateChange: (s) => states.push(s),
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
433
|
-
assert(states.includes("waiting"), "expected waiting state");
|
|
434
|
-
cancel(); // trigger abort
|
|
435
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
436
|
-
} finally {
|
|
437
|
-
restore();
|
|
438
|
-
}
|
|
439
|
-
},
|
|
440
|
-
);
|
|
441
|
-
|
|
442
|
-
return { passed, failed };
|
|
443
|
-
}
|