@freqhole/api-client 0.1.32 → 0.2.1
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/automerge/index.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// automerge transport layer - iroh QUIC network adapter for automerge-repo.
|
|
2
|
-
//
|
|
3
|
-
// re-exports everything needed by consuming packages.
|
|
4
|
-
export {
|
|
5
|
-
IrohNetworkAdapter,
|
|
6
|
-
SYNC_ALPN,
|
|
7
|
-
} from "./IrohNetworkAdapter.js";
|
|
8
|
-
|
|
9
|
-
export type {
|
|
10
|
-
BiStreamLike,
|
|
11
|
-
MiddenStreamNode,
|
|
12
|
-
ConnectionSummary,
|
|
13
|
-
IrohNetworkAdapterOptions,
|
|
14
|
-
} from "./IrohNetworkAdapter.js";
|
package/src/storage/blobs.ts
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
// local blob storage for images and media
|
|
2
|
-
// metadata in IndexedDB, actual blob data in OPFS or Cache API
|
|
3
|
-
|
|
4
|
-
import { openDB, type IDBPDatabase } from "idb";
|
|
5
|
-
|
|
6
|
-
// module-local logger - no-op by default; wire up via setStorageLogger
|
|
7
|
-
let logger: (scope: string, ...args: unknown[]) => void = () => {};
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* set the logger function used by blob storage operations.
|
|
11
|
-
* call this at app startup to opt into logging.
|
|
12
|
-
*/
|
|
13
|
-
export function setStorageLogger(
|
|
14
|
-
fn: (scope: string, ...args: unknown[]) => void,
|
|
15
|
-
): void {
|
|
16
|
-
logger = fn;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const BLOB_DB_NAME = "freqhole_blobs";
|
|
20
|
-
const BLOB_DB_VERSION = 1;
|
|
21
|
-
const STORE_BLOBS = "blobs";
|
|
22
|
-
const CACHE_NAME = "freqhole-blobs";
|
|
23
|
-
|
|
24
|
-
// global blob URL cache - persists for session, cleared on page reload
|
|
25
|
-
const BLOB_URL_CACHE = new Map<string, string>();
|
|
26
|
-
|
|
27
|
-
export type BlobStorageType = "opfs" | "cache";
|
|
28
|
-
|
|
29
|
-
export interface BlobRecord {
|
|
30
|
-
blob_id: string; // sha256 hash of content (primary key)
|
|
31
|
-
storage_type: BlobStorageType;
|
|
32
|
-
storage_path: string; // opfs path or cache key
|
|
33
|
-
mime_type: string;
|
|
34
|
-
file_size: number;
|
|
35
|
-
created_at: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let blobDbInstance: IDBPDatabase | null = null;
|
|
39
|
-
|
|
40
|
-
async function initBlobDB(): Promise<IDBPDatabase> {
|
|
41
|
-
if (blobDbInstance) return blobDbInstance;
|
|
42
|
-
|
|
43
|
-
blobDbInstance = await openDB(BLOB_DB_NAME, BLOB_DB_VERSION, {
|
|
44
|
-
upgrade(db) {
|
|
45
|
-
if (!db.objectStoreNames.contains(STORE_BLOBS)) {
|
|
46
|
-
db.createObjectStore(STORE_BLOBS, { keyPath: "blob_id" });
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return blobDbInstance;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// close database connection (required before deletion)
|
|
55
|
-
export function closeBlobDB(): void {
|
|
56
|
-
if (blobDbInstance) {
|
|
57
|
-
blobDbInstance.close();
|
|
58
|
-
blobDbInstance = null;
|
|
59
|
-
logger("blobs", "blob database connection closed");
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// check if OPFS is available
|
|
64
|
-
async function isOPFSAvailable(): Promise<boolean> {
|
|
65
|
-
try {
|
|
66
|
-
if (!navigator.storage?.getDirectory) return false;
|
|
67
|
-
await navigator.storage.getDirectory();
|
|
68
|
-
return true;
|
|
69
|
-
} catch {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// write blob to OPFS
|
|
75
|
-
async function writeBlobToOPFS(blobId: string, data: Blob): Promise<string> {
|
|
76
|
-
const root = await navigator.storage.getDirectory();
|
|
77
|
-
const blobsDir = await root.getDirectoryHandle("blobs", { create: true });
|
|
78
|
-
const fileHandle = await blobsDir.getFileHandle(blobId, { create: true });
|
|
79
|
-
const writable = await fileHandle.createWritable();
|
|
80
|
-
await writable.write(data);
|
|
81
|
-
await writable.close();
|
|
82
|
-
return `/blobs/${blobId}`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// read blob from OPFS
|
|
86
|
-
async function readBlobFromOPFS(storagePath: string): Promise<Blob | null> {
|
|
87
|
-
try {
|
|
88
|
-
const root = await navigator.storage.getDirectory();
|
|
89
|
-
const blobsDir = await root.getDirectoryHandle("blobs");
|
|
90
|
-
const fileName = storagePath.split("/").pop()!;
|
|
91
|
-
const fileHandle = await blobsDir.getFileHandle(fileName);
|
|
92
|
-
const file = await fileHandle.getFile();
|
|
93
|
-
return file;
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// write blob to Cache API
|
|
100
|
-
async function writeBlobToCache(
|
|
101
|
-
blobId: string,
|
|
102
|
-
data: Blob,
|
|
103
|
-
mimeType: string,
|
|
104
|
-
): Promise<string> {
|
|
105
|
-
const cache = await caches.open(CACHE_NAME);
|
|
106
|
-
// use synthetic HTTP URL - webkitgtk (linux) rejects non-HTTP URLs in Cache API
|
|
107
|
-
const url = `https://blob.local/${blobId}`;
|
|
108
|
-
const response = new Response(data, {
|
|
109
|
-
headers: { "Content-Type": mimeType },
|
|
110
|
-
});
|
|
111
|
-
await cache.put(url, response);
|
|
112
|
-
return url;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// read blob from Cache API
|
|
116
|
-
async function readBlobFromCache(storagePath: string): Promise<Blob | null> {
|
|
117
|
-
try {
|
|
118
|
-
const cache = await caches.open(CACHE_NAME);
|
|
119
|
-
const response = await cache.match(storagePath);
|
|
120
|
-
if (!response) return null;
|
|
121
|
-
return await response.blob();
|
|
122
|
-
} catch {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* store a blob in local storage and return its ID (sha256 hash).
|
|
129
|
-
* automatically chooses OPFS or Cache API based on availability.
|
|
130
|
-
*/
|
|
131
|
-
export async function storeBlob(
|
|
132
|
-
data: Blob,
|
|
133
|
-
mimeType: string,
|
|
134
|
-
): Promise<string> {
|
|
135
|
-
const db = await initBlobDB();
|
|
136
|
-
|
|
137
|
-
// compute sha256 hash of blob data
|
|
138
|
-
const arrayBuffer = await data.arrayBuffer();
|
|
139
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", arrayBuffer);
|
|
140
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
141
|
-
const blobId = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
142
|
-
|
|
143
|
-
// check if already exists
|
|
144
|
-
const existing = await db.get(STORE_BLOBS, blobId);
|
|
145
|
-
if (existing) {
|
|
146
|
-
return blobId;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// determine storage type and write blob data
|
|
150
|
-
const useOPFS = await isOPFSAvailable();
|
|
151
|
-
const storageType: BlobStorageType = useOPFS ? "opfs" : "cache";
|
|
152
|
-
const storagePath = useOPFS
|
|
153
|
-
? await writeBlobToOPFS(blobId, data)
|
|
154
|
-
: await writeBlobToCache(blobId, data, mimeType);
|
|
155
|
-
|
|
156
|
-
// store metadata
|
|
157
|
-
const record: BlobRecord = {
|
|
158
|
-
blob_id: blobId,
|
|
159
|
-
storage_type: storageType,
|
|
160
|
-
storage_path: storagePath,
|
|
161
|
-
mime_type: mimeType,
|
|
162
|
-
file_size: data.size,
|
|
163
|
-
created_at: Date.now(),
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
await db.put(STORE_BLOBS, record);
|
|
167
|
-
return blobId;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* get blob metadata by ID
|
|
172
|
-
*/
|
|
173
|
-
export async function getBlobMetadata(
|
|
174
|
-
blobId: string,
|
|
175
|
-
): Promise<BlobRecord | null> {
|
|
176
|
-
const db = await initBlobDB();
|
|
177
|
-
const record = await db.get(STORE_BLOBS, blobId);
|
|
178
|
-
return record || null;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* get blob data by ID
|
|
183
|
-
*/
|
|
184
|
-
export async function getBlob(blobId: string): Promise<Blob | null> {
|
|
185
|
-
const metadata = await getBlobMetadata(blobId);
|
|
186
|
-
if (!metadata) return null;
|
|
187
|
-
|
|
188
|
-
if (metadata.storage_type === "opfs") {
|
|
189
|
-
return await readBlobFromOPFS(metadata.storage_path);
|
|
190
|
-
} else {
|
|
191
|
-
return await readBlobFromCache(metadata.storage_path);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* get blob object URL by ID, cached to avoid repeated OPFS reads and URL creations.
|
|
197
|
-
* returns an object URL string usable directly in an img src.
|
|
198
|
-
* URLs are cached for the session and revoked on page unload.
|
|
199
|
-
*/
|
|
200
|
-
export async function getBlobObjectURL(
|
|
201
|
-
blobId: string,
|
|
202
|
-
): Promise<string | null> {
|
|
203
|
-
if (!blobId) return null;
|
|
204
|
-
|
|
205
|
-
// check cache first
|
|
206
|
-
const cachedUrl = BLOB_URL_CACHE.get(blobId);
|
|
207
|
-
if (cachedUrl) return cachedUrl;
|
|
208
|
-
|
|
209
|
-
// fetch blob and create object URL
|
|
210
|
-
const blob = await getBlob(blobId);
|
|
211
|
-
if (!blob) return null;
|
|
212
|
-
|
|
213
|
-
const objectUrl = URL.createObjectURL(blob);
|
|
214
|
-
BLOB_URL_CACHE.set(blobId, objectUrl);
|
|
215
|
-
|
|
216
|
-
return objectUrl;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* synchronous blob URL cache lookup (no OPFS read).
|
|
221
|
-
* returns cached object URL if available, null otherwise.
|
|
222
|
-
*/
|
|
223
|
-
export function getCachedBlobObjectURL(blobId: string): string | null {
|
|
224
|
-
if (!blobId) return null;
|
|
225
|
-
return BLOB_URL_CACHE.get(blobId) ?? null;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* clear all cached blob URLs and revoke object URLs
|
|
230
|
-
*/
|
|
231
|
-
export function clearBlobUrlCache(): void {
|
|
232
|
-
BLOB_URL_CACHE.forEach((url) => URL.revokeObjectURL(url));
|
|
233
|
-
BLOB_URL_CACHE.clear();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// revoke object URLs on page unload to avoid memory leaks
|
|
237
|
-
if (typeof window !== "undefined") {
|
|
238
|
-
window.addEventListener("beforeunload", () => {
|
|
239
|
-
clearBlobUrlCache();
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* delete blob by ID from both storage and metadata db
|
|
245
|
-
*/
|
|
246
|
-
export async function deleteBlob(blobId: string): Promise<void> {
|
|
247
|
-
const metadata = await getBlobMetadata(blobId);
|
|
248
|
-
if (!metadata) return;
|
|
249
|
-
|
|
250
|
-
// delete from storage
|
|
251
|
-
if (metadata.storage_type === "opfs") {
|
|
252
|
-
try {
|
|
253
|
-
const root = await navigator.storage.getDirectory();
|
|
254
|
-
const blobsDir = await root.getDirectoryHandle("blobs");
|
|
255
|
-
const fileName = metadata.storage_path.split("/").pop()!;
|
|
256
|
-
await blobsDir.removeEntry(fileName);
|
|
257
|
-
} catch {
|
|
258
|
-
// ignore errors during cleanup
|
|
259
|
-
}
|
|
260
|
-
} else {
|
|
261
|
-
try {
|
|
262
|
-
const cache = await caches.open(CACHE_NAME);
|
|
263
|
-
await cache.delete(metadata.storage_path);
|
|
264
|
-
} catch {
|
|
265
|
-
// ignore errors during cleanup
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// delete metadata
|
|
270
|
-
const db = await initBlobDB();
|
|
271
|
-
await db.delete(STORE_BLOBS, blobId);
|
|
272
|
-
}
|
package/src/storage/identity.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
// p2p identity resolution with read-only access to spume's freqhole_app IndexedDB.
|
|
2
|
-
//
|
|
3
|
-
// policy:
|
|
4
|
-
// - never create or upgrade the "freqhole_app" database
|
|
5
|
-
// - if freqhole_app exists and has an "app_state" store, use it as the primary store
|
|
6
|
-
// - otherwise, use the injected IdentityStore (local, per-app)
|
|
7
|
-
// - when both have an identity, prefer spume's db
|
|
8
|
-
|
|
9
|
-
const FREQHOLE_APP_DB = "freqhole_app";
|
|
10
|
-
const APP_STATE_STORE = "app_state";
|
|
11
|
-
const IDENTITY_KEY = "p2p_identity";
|
|
12
|
-
|
|
13
|
-
// snake_case fields are a wire/storage compatibility contract with spume
|
|
14
|
-
export interface P2PIdentity {
|
|
15
|
-
id: "p2p_identity";
|
|
16
|
-
secret_key: Uint8Array;
|
|
17
|
-
node_id: string;
|
|
18
|
-
created_at: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** per-app fallback store - apps supply their own settings persistence */
|
|
22
|
-
export interface IdentityStore {
|
|
23
|
-
get(): Promise<P2PIdentity | null>;
|
|
24
|
-
set(identity: P2PIdentity): Promise<void>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* check whether freqhole_app exists without creating it.
|
|
29
|
-
*
|
|
30
|
-
* strategy:
|
|
31
|
-
* 1. use indexedDB.databases() when available (Chrome/Firefox 126+)
|
|
32
|
-
* 2. fall back to a versionless open; abort the versionchange transaction
|
|
33
|
-
* if upgrade fires (meaning the db did not exist), then close and delete.
|
|
34
|
-
*/
|
|
35
|
-
async function freqholeAppExists(): Promise<boolean> {
|
|
36
|
-
// prefer the non-destructive databases() API
|
|
37
|
-
if (typeof indexedDB.databases === "function") {
|
|
38
|
-
try {
|
|
39
|
-
const dbs = await indexedDB.databases();
|
|
40
|
-
return dbs.some((d) => d.name === FREQHOLE_APP_DB);
|
|
41
|
-
} catch {
|
|
42
|
-
// fall through to open-based detection
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// open-based detection: if upgradeneeded fires, the db did not exist -
|
|
47
|
-
// abort immediately so IDB never persists it
|
|
48
|
-
return new Promise<boolean>((resolve) => {
|
|
49
|
-
const req = indexedDB.open(FREQHOLE_APP_DB);
|
|
50
|
-
let existed = true;
|
|
51
|
-
|
|
52
|
-
req.onupgradeneeded = (event) => {
|
|
53
|
-
// upgradeneeded on a version-0 open means the db is being created now
|
|
54
|
-
existed = false;
|
|
55
|
-
// abort the versionchange transaction to prevent creation
|
|
56
|
-
(event.target as IDBOpenDBRequest).transaction?.abort();
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
req.onsuccess = () => {
|
|
60
|
-
req.result.close();
|
|
61
|
-
if (!existed) {
|
|
62
|
-
// clean up the empty database that was created before we could abort
|
|
63
|
-
indexedDB.deleteDatabase(FREQHOLE_APP_DB);
|
|
64
|
-
}
|
|
65
|
-
resolve(existed);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
req.onerror = () => {
|
|
69
|
-
// the abort above causes an error - if existed is false this is expected
|
|
70
|
-
resolve(existed);
|
|
71
|
-
};
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* check whether freqhole_app has the app_state object store.
|
|
77
|
-
* caller must have already confirmed the db exists.
|
|
78
|
-
*/
|
|
79
|
-
async function hasAppStateStore(): Promise<boolean> {
|
|
80
|
-
return new Promise<boolean>((resolve) => {
|
|
81
|
-
const req = indexedDB.open(FREQHOLE_APP_DB);
|
|
82
|
-
|
|
83
|
-
req.onupgradeneeded = () => {
|
|
84
|
-
// should not fire here since we know the db exists - if it does, abort
|
|
85
|
-
req.transaction?.abort();
|
|
86
|
-
resolve(false);
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
req.onsuccess = () => {
|
|
90
|
-
const db = req.result;
|
|
91
|
-
const has = db.objectStoreNames.contains(APP_STATE_STORE);
|
|
92
|
-
db.close();
|
|
93
|
-
resolve(has);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
req.onerror = () => resolve(false);
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** read p2p_identity from spume's app_state store */
|
|
101
|
-
function readFromFreqholeApp(): Promise<P2PIdentity | null> {
|
|
102
|
-
return new Promise<P2PIdentity | null>((resolve) => {
|
|
103
|
-
const req = indexedDB.open(FREQHOLE_APP_DB);
|
|
104
|
-
|
|
105
|
-
req.onupgradeneeded = () => {
|
|
106
|
-
req.transaction?.abort();
|
|
107
|
-
resolve(null);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
req.onsuccess = () => {
|
|
111
|
-
const db = req.result;
|
|
112
|
-
const tx = db.transaction(APP_STATE_STORE, "readonly");
|
|
113
|
-
const getReq = tx.objectStore(APP_STATE_STORE).get(IDENTITY_KEY);
|
|
114
|
-
|
|
115
|
-
getReq.onsuccess = () => {
|
|
116
|
-
db.close();
|
|
117
|
-
resolve((getReq.result as P2PIdentity) ?? null);
|
|
118
|
-
};
|
|
119
|
-
getReq.onerror = () => {
|
|
120
|
-
db.close();
|
|
121
|
-
resolve(null);
|
|
122
|
-
};
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
req.onerror = () => resolve(null);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** write p2p_identity to spume's app_state store (no version bump) */
|
|
130
|
-
function writeToFreqholeApp(identity: P2PIdentity): Promise<void> {
|
|
131
|
-
return new Promise<void>((resolve, reject) => {
|
|
132
|
-
const req = indexedDB.open(FREQHOLE_APP_DB);
|
|
133
|
-
|
|
134
|
-
req.onupgradeneeded = () => {
|
|
135
|
-
req.transaction?.abort();
|
|
136
|
-
reject(new Error("freqhole_app unexpectedly absent during write"));
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
req.onsuccess = () => {
|
|
140
|
-
const db = req.result;
|
|
141
|
-
const tx = db.transaction(APP_STATE_STORE, "readwrite");
|
|
142
|
-
const putReq = tx.objectStore(APP_STATE_STORE).put(identity, IDENTITY_KEY);
|
|
143
|
-
|
|
144
|
-
putReq.onsuccess = () => {
|
|
145
|
-
db.close();
|
|
146
|
-
resolve();
|
|
147
|
-
};
|
|
148
|
-
putReq.onerror = () => {
|
|
149
|
-
db.close();
|
|
150
|
-
reject(putReq.error);
|
|
151
|
-
};
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
req.onerror = () => reject(req.error);
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* resolve the p2p identity for this origin.
|
|
160
|
-
* prefers spume's db when both sources have an identity.
|
|
161
|
-
*/
|
|
162
|
-
export async function resolveIdentity(
|
|
163
|
-
local: IdentityStore,
|
|
164
|
-
): Promise<P2PIdentity | null> {
|
|
165
|
-
const exists = await freqholeAppExists();
|
|
166
|
-
if (exists && (await hasAppStateStore())) {
|
|
167
|
-
const spumeIdentity = await readFromFreqholeApp();
|
|
168
|
-
if (spumeIdentity) return spumeIdentity;
|
|
169
|
-
// spume db exists but has no identity - fall through to local
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return local.get();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* persist a p2p identity.
|
|
177
|
-
* writes to spume's db if it exists and has app_state; otherwise uses local.
|
|
178
|
-
*/
|
|
179
|
-
export async function persistIdentity(
|
|
180
|
-
identity: P2PIdentity,
|
|
181
|
-
local: IdentityStore,
|
|
182
|
-
): Promise<void> {
|
|
183
|
-
const exists = await freqholeAppExists();
|
|
184
|
-
if (exists && (await hasAppStateStore())) {
|
|
185
|
-
await writeToFreqholeApp(identity);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
await local.set(identity);
|
|
190
|
-
}
|
package/src/storage/index.ts
DELETED
package/src/storage/webLocks.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// web locks leader election for the iroh node.
|
|
2
|
-
// only one tab per origin runs the midden node at a time.
|
|
3
|
-
|
|
4
|
-
/** lock name used for iroh node leader election */
|
|
5
|
-
export const LOCK_NAME = "freqhole-iroh-node";
|
|
6
|
-
|
|
7
|
-
export interface AcquireNodeLeadershipOpts {
|
|
8
|
-
/** called when this tab becomes the node leader */
|
|
9
|
-
onAcquired: () => void | Promise<void>;
|
|
10
|
-
/** called with state transitions; optional */
|
|
11
|
-
onStateChange?: (state: "leader" | "waiting" | "unsupported") => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* attempt to acquire exclusive leadership of the iroh node.
|
|
16
|
-
*
|
|
17
|
-
* behavior:
|
|
18
|
-
* - tries ifAvailable first; if the lock is free, holds it and calls onAcquired
|
|
19
|
-
* - if already held, reports "waiting" and queues a normal request with an AbortController
|
|
20
|
-
* so leadership transfers when the current leader tab closes
|
|
21
|
-
* - if navigator.locks is absent (e.g. non-secure context), reports "unsupported"
|
|
22
|
-
* and calls onAcquired immediately (single-tab fallback)
|
|
23
|
-
*
|
|
24
|
-
* returns a cancel/release function; call it to give up leadership or cancel the wait.
|
|
25
|
-
*/
|
|
26
|
-
export function acquireNodeLeadership(
|
|
27
|
-
opts: AcquireNodeLeadershipOpts,
|
|
28
|
-
): () => void {
|
|
29
|
-
const { onAcquired, onStateChange } = opts;
|
|
30
|
-
|
|
31
|
-
// single-tab fallback for environments without Web Locks
|
|
32
|
-
if (
|
|
33
|
-
typeof navigator === "undefined" ||
|
|
34
|
-
!navigator.locks
|
|
35
|
-
) {
|
|
36
|
-
onStateChange?.("unsupported");
|
|
37
|
-
void Promise.resolve().then(() => onAcquired());
|
|
38
|
-
return () => {};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let released = false;
|
|
42
|
-
// resolving this promise releases the held lock
|
|
43
|
-
let releaseLock: (() => void) | null = null;
|
|
44
|
-
// used to cancel the waiting queue request
|
|
45
|
-
const waitAbort = new AbortController();
|
|
46
|
-
|
|
47
|
-
function cancel() {
|
|
48
|
-
released = true;
|
|
49
|
-
waitAbort.abort();
|
|
50
|
-
releaseLock?.();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// try to grab the lock immediately without queuing
|
|
54
|
-
navigator.locks.request(
|
|
55
|
-
LOCK_NAME,
|
|
56
|
-
{ ifAvailable: true },
|
|
57
|
-
async (lock: Lock | null) => {
|
|
58
|
-
if (released) return;
|
|
59
|
-
|
|
60
|
-
if (lock !== null) {
|
|
61
|
-
// we are now the leader - call onAcquired, then hold the lock until released
|
|
62
|
-
onStateChange?.("leader");
|
|
63
|
-
await onAcquired();
|
|
64
|
-
await new Promise<void>((resolve) => {
|
|
65
|
-
releaseLock = resolve;
|
|
66
|
-
});
|
|
67
|
-
} else {
|
|
68
|
-
// another tab holds the lock - queue up for leadership
|
|
69
|
-
onStateChange?.("waiting");
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
await navigator.locks.request(
|
|
73
|
-
LOCK_NAME,
|
|
74
|
-
{ signal: waitAbort.signal },
|
|
75
|
-
async () => {
|
|
76
|
-
if (released) return;
|
|
77
|
-
onStateChange?.("leader");
|
|
78
|
-
await onAcquired();
|
|
79
|
-
await new Promise<void>((resolve) => {
|
|
80
|
-
releaseLock = resolve;
|
|
81
|
-
});
|
|
82
|
-
},
|
|
83
|
-
);
|
|
84
|
-
} catch {
|
|
85
|
-
// aborted by cancel() or context destroyed - not an error
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
return cancel;
|
|
92
|
-
}
|