@freqhole/playlistz 0.0.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/.changeset/config.json +11 -0
- package/.changeset/nice-wolves-thank.md +5 -0
- package/.freqhole-versions.json +4 -0
- package/.github/copilot-instructions.md +201 -0
- package/.github/workflows/changesets.yml +50 -0
- package/.github/workflows/npm-publish.yml +124 -0
- package/.github/workflows/pr-checks.yml +103 -0
- package/README.md +30 -0
- package/build-component.js +141 -0
- package/build-zip-bundle-lib.js +44 -0
- package/config/playwright.config.ts +47 -0
- package/config/vite.config.ts +44 -0
- package/config/vitest.config.ts +39 -0
- package/dist/assets/automerge_wasm_bg-Cik4BF9l.wasm +0 -0
- package/dist/assets/index-CbOXzGiA.js +216 -0
- package/dist/assets/index-CbOXzGiA.js.map +1 -0
- package/dist/assets/index-TvJ6RFpy.css +1 -0
- package/dist/assets/midden-DceCrT_L.js +2 -0
- package/dist/assets/midden-DceCrT_L.js.map +1 -0
- package/dist/assets/midden_bg-BLhfGIU-.wasm +0 -0
- package/dist/index.html +55 -0
- package/dist/sw.js +134 -0
- package/docs/AUTOMERGE_P2P_PLAN.md +233 -0
- package/docs/COLLABORATIVE_SHARING_PLAN.md +188 -0
- package/docs/E2E_TESTID_PLAN.md +234 -0
- package/docs/IROH_P2P_PLAN.md +302 -0
- package/docs/ROADMAP.md +695 -0
- package/docs/TODO.md +167 -0
- package/docs/bundle-embedding-plan.md +134 -0
- package/docs/standalone-refactor.md +184 -0
- package/e2e/all-playlists.spec.ts +220 -0
- package/e2e/audio-player.spec.ts +226 -0
- package/e2e/collaborative-features.spec.ts +229 -0
- package/e2e/contexts.ts +238 -0
- package/e2e/edit-panel.spec.ts +87 -0
- package/e2e/fixtures/bare-glitch-1s.m4a +0 -0
- package/e2e/fixtures/bare-glitch-1s.mp3 +0 -0
- package/e2e/fixtures/bare-glitch-1s.ogg +0 -0
- package/e2e/fixtures/chord-stack-3s.wav +0 -0
- package/e2e/fixtures/cover-anim.gif +0 -0
- package/e2e/fixtures/cover-blue.png +0 -0
- package/e2e/fixtures/cover-checkers.png +0 -0
- package/e2e/fixtures/cover-gradient.jpg +0 -0
- package/e2e/fixtures/cover-mono.gif +0 -0
- package/e2e/fixtures/cover-noise.png +0 -0
- package/e2e/fixtures/cover-plasma.webp +0 -0
- package/e2e/fixtures/cover-portrait.jpg +0 -0
- package/e2e/fixtures/cover-red.png +0 -0
- package/e2e/fixtures/cover-thumb.jpg +0 -0
- package/e2e/fixtures/cover-wide.webp +0 -0
- package/e2e/fixtures/generate.mjs +257 -0
- package/e2e/fixtures/long-drone-90s.mp3 +0 -0
- package/e2e/fixtures/noisy-binaural-8s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.m4a +0 -0
- package/e2e/fixtures/tagged-a3-4s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.ogg +0 -0
- package/e2e/fixtures/tagged-c5-3s.m4a +0 -0
- package/e2e/fixtures/tagged-c5-3s.mp3 +0 -0
- package/e2e/fixtures/tagged-c5-3s.ogg +0 -0
- package/e2e/fixtures/tagged-f4-6s.m4a +0 -0
- package/e2e/fixtures/tagged-f4-6s.mp3 +0 -0
- package/e2e/fixtures/tagged-f4-6s.ogg +0 -0
- package/e2e/fixtures/tone-220hz-10s.wav +0 -0
- package/e2e/fixtures/tone-440hz-2s.wav +0 -0
- package/e2e/fixtures/tone-880hz-5s.wav +0 -0
- package/e2e/fixtures/tone-stereo-3s.wav +0 -0
- package/e2e/fixtures/user-provided/README.md +1 -0
- package/e2e/helpers/app.ts +143 -0
- package/e2e/helpers/hooks.ts +133 -0
- package/e2e/helpers/index.ts +12 -0
- package/e2e/helpers/media.ts +125 -0
- package/e2e/helpers.ts +10 -0
- package/e2e/p2p-collaboration.spec.ts +356 -0
- package/e2e/p2p-multi-peer.spec.ts +723 -0
- package/e2e/p2p-states.spec.ts +302 -0
- package/e2e/playback.spec.ts +56 -0
- package/e2e/playlist-crud.spec.ts +126 -0
- package/e2e/share-link-autoplay.spec.ts +129 -0
- package/e2e/sharing-access.spec.ts +205 -0
- package/e2e/sharing.spec.ts +195 -0
- package/e2e/song-cache-state.spec.ts +202 -0
- package/e2e/zip-bundle.spec.ts +855 -0
- package/eslint.config.js +114 -0
- package/index.html +54 -0
- package/package.json +119 -0
- package/public/sw.js +134 -0
- package/scripts/use-local.mjs +37 -0
- package/scripts/use-published.mjs +37 -0
- package/src/App.tsx +9 -0
- package/src/cli/check.ts +164 -0
- package/src/cli/generate.ts +184 -0
- package/src/cli/http.ts +88 -0
- package/src/cli/index.ts +65 -0
- package/src/cli/init.ts +18 -0
- package/src/components/AllPlaylistsPanel.tsx +713 -0
- package/src/components/AudioPlayer.tsx +122 -0
- package/src/components/MarqueeText.tsx +101 -0
- package/src/components/PlaylistCoverModal.tsx +519 -0
- package/src/components/PlaylistEditPanel.tsx +803 -0
- package/src/components/PlaylistSharePanel.tsx +1020 -0
- package/src/components/ShareLinkKnockPanel.tsx +144 -0
- package/src/components/SharePanel.tsx +584 -0
- package/src/components/SongEditModal.tsx +453 -0
- package/src/components/SongEditPanel.tsx +578 -0
- package/src/components/SongRow.tsx +689 -0
- package/src/components/index.tsx +494 -0
- package/src/components/playlist/index.tsx +1203 -0
- package/src/context/PlaylistzContext.tsx +74 -0
- package/src/dev-hooks.ts +35 -0
- package/src/hooks/createDocIndexQuery.ts +53 -0
- package/src/hooks/createDocStore.test.ts +303 -0
- package/src/hooks/createDocStore.ts +90 -0
- package/src/hooks/useDragAndDrop.test.ts +474 -0
- package/src/hooks/useDragAndDrop.ts +400 -0
- package/src/hooks/useImageModal.test.ts +174 -0
- package/src/hooks/useImageModal.ts +201 -0
- package/src/hooks/usePlaylistManager.test.ts +453 -0
- package/src/hooks/usePlaylistManager.ts +685 -0
- package/src/hooks/usePlaylistsQuery.test.tsx +120 -0
- package/src/hooks/usePlaylistsQuery.ts +44 -0
- package/src/hooks/useSongState.test.ts +236 -0
- package/src/hooks/useSongState.ts +114 -0
- package/src/hooks/useUIState.ts +71 -0
- package/src/index.tsx +18 -0
- package/src/services/audioService.dev.ts +22 -0
- package/src/services/audioService.test.ts +1226 -0
- package/src/services/audioService.ts +1395 -0
- package/src/services/automergeRepo.test.ts +269 -0
- package/src/services/automergeRepo.ts +226 -0
- package/src/services/blobTransferService.dev.ts +119 -0
- package/src/services/blobTransferService.test.ts +441 -0
- package/src/services/blobTransferService.ts +702 -0
- package/src/services/docIndexService.test.ts +179 -0
- package/src/services/docIndexService.ts +118 -0
- package/src/services/fileProcessingService.test.ts +554 -0
- package/src/services/fileProcessingService.ts +239 -0
- package/src/services/imageService.test.ts +701 -0
- package/src/services/imageService.ts +365 -0
- package/src/services/indexedDBService.integration.test.ts +104 -0
- package/src/services/indexedDBService.test.ts +202 -0
- package/src/services/indexedDBService.ts +436 -0
- package/src/services/offlineService.test.ts +661 -0
- package/src/services/offlineService.ts +382 -0
- package/src/services/p2pService.test.ts +305 -0
- package/src/services/p2pService.ts +344 -0
- package/src/services/playlistDocService.test.ts +448 -0
- package/src/services/playlistDocService.ts +707 -0
- package/src/services/playlistDownloadService.test.ts +674 -0
- package/src/services/playlistDownloadService.ts +389 -0
- package/src/services/sharingService.test.ts +812 -0
- package/src/services/sharingService.ts +1073 -0
- package/src/services/sharingState.ts +161 -0
- package/src/services/songReactivity.test.ts +620 -0
- package/src/services/songReactivity.ts +145 -0
- package/src/services/standaloneService.test.ts +1025 -0
- package/src/services/standaloneService.ts +588 -0
- package/src/services/streamingAudioService.test.ts +275 -0
- package/src/services/streamingAudioService.ts +166 -0
- package/src/styles.css +428 -0
- package/src/test-setup.ts +547 -0
- package/src/types/global.d.ts +40 -0
- package/src/types/playlist.ts +99 -0
- package/src/utils/hashUtils.ts +41 -0
- package/src/utils/log.ts +97 -0
- package/src/utils/m3u.test.ts +172 -0
- package/src/utils/m3u.ts +136 -0
- package/src/utils/mockData.ts +166 -0
- package/src/utils/standaloneTemplates.test.ts +175 -0
- package/src/utils/standaloneTemplates.ts +83 -0
- package/src/utils/swTemplate.ts +84 -0
- package/src/utils/timeUtils.ts +166 -0
- package/src/utils/typeGuards.ts +171 -0
- package/src/web-component.tsx +98 -0
- package/src/zip-bundle/index.ts +7 -0
- package/src/zip-bundle/m3u.ts +45 -0
- package/src/zip-bundle/types.ts +50 -0
- package/src/zip-bundle/utils.ts +33 -0
- package/src/zip-bundle/zipBuilder.ts +309 -0
- package/tailwind.config.js +55 -0
- package/tsconfig.json +43 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// p2p bootstrap service for playlistz.
|
|
2
|
+
//
|
|
3
|
+
// wires identity resolution (from freqhole-api-client/storage) with midden
|
|
4
|
+
// node lifecycle and web locks leader election.
|
|
5
|
+
//
|
|
6
|
+
// nothing wasm-related runs at import time. call startP2P() explicitly.
|
|
7
|
+
|
|
8
|
+
import { openDB } from "idb";
|
|
9
|
+
import {
|
|
10
|
+
resolveIdentity,
|
|
11
|
+
persistIdentity,
|
|
12
|
+
acquireNodeLeadership,
|
|
13
|
+
type P2PIdentity,
|
|
14
|
+
type IdentityStore,
|
|
15
|
+
} from "@freqhole/api-client/storage";
|
|
16
|
+
import { AUTOMERGE_ALPN, PLAYLISTZ_ALPN } from "@freqhole/api-client/playlistz";
|
|
17
|
+
import type {
|
|
18
|
+
MiddenStreamNode,
|
|
19
|
+
IrohNetworkAdapterOptions,
|
|
20
|
+
} from "@freqhole/api-client/automerge";
|
|
21
|
+
|
|
22
|
+
// --- local settings db for identity fallback ---
|
|
23
|
+
|
|
24
|
+
const SETTINGS_DB_NAME = "freqhole-playlistz-settings";
|
|
25
|
+
const SETTINGS_STORE = "settings";
|
|
26
|
+
const IDENTITY_KEY = "p2p_identity";
|
|
27
|
+
|
|
28
|
+
function createLocalStore(): IdentityStore {
|
|
29
|
+
let db: Awaited<ReturnType<typeof openDB>> | null = null;
|
|
30
|
+
|
|
31
|
+
async function getDb(): Promise<Awaited<ReturnType<typeof openDB>>> {
|
|
32
|
+
if (!db) {
|
|
33
|
+
db = await openDB(SETTINGS_DB_NAME, 1, {
|
|
34
|
+
upgrade(database) {
|
|
35
|
+
if (!database.objectStoreNames.contains(SETTINGS_STORE)) {
|
|
36
|
+
database.createObjectStore(SETTINGS_STORE);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return db;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
async get(): Promise<P2PIdentity | null> {
|
|
46
|
+
const database = await getDb();
|
|
47
|
+
const result = await database.get(SETTINGS_STORE, IDENTITY_KEY);
|
|
48
|
+
return (result as P2PIdentity) ?? null;
|
|
49
|
+
},
|
|
50
|
+
async set(identity: P2PIdentity): Promise<void> {
|
|
51
|
+
const database = await getDb();
|
|
52
|
+
await database.put(SETTINGS_STORE, identity, IDENTITY_KEY);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- module-level singleton state ---
|
|
58
|
+
|
|
59
|
+
let _localStore: IdentityStore | null = null;
|
|
60
|
+
|
|
61
|
+
function getLocalStore(): IdentityStore {
|
|
62
|
+
if (!_localStore) {
|
|
63
|
+
_localStore = createLocalStore();
|
|
64
|
+
}
|
|
65
|
+
return _localStore;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let currentIdentity: P2PIdentity | null = null;
|
|
69
|
+
let currentNode: MiddenStreamNode | null = null;
|
|
70
|
+
let leaderState = false;
|
|
71
|
+
let started = false;
|
|
72
|
+
let cancelLeadership: (() => void) | null = null;
|
|
73
|
+
|
|
74
|
+
// leadership phase: "unknown" until the lock request settles, then
|
|
75
|
+
// "leader" / "waiting" / "unsupported". used by waitForNode to short-circuit
|
|
76
|
+
// in tabs that will never hold the node
|
|
77
|
+
let leadershipPhase: "unknown" | "leader" | "waiting" | "unsupported" =
|
|
78
|
+
"unknown";
|
|
79
|
+
|
|
80
|
+
// resolved when the local node comes up (or definitively won't)
|
|
81
|
+
const nodeWaiters = new Set<(node: MiddenStreamNode | null) => void>();
|
|
82
|
+
|
|
83
|
+
function flushNodeWaiters(node: MiddenStreamNode | null): void {
|
|
84
|
+
for (const resolve of nodeWaiters) resolve(node);
|
|
85
|
+
nodeWaiters.clear();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const identityListeners = new Set<(identity: P2PIdentity | null) => void>();
|
|
89
|
+
const leadershipListeners = new Set<(isLeader: boolean) => void>();
|
|
90
|
+
|
|
91
|
+
function notifyIdentityListeners(): void {
|
|
92
|
+
for (const cb of identityListeners) {
|
|
93
|
+
try {
|
|
94
|
+
cb(currentIdentity);
|
|
95
|
+
} catch {
|
|
96
|
+
// ignore listener errors
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function notifyLeadershipListeners(): void {
|
|
102
|
+
for (const cb of leadershipListeners) {
|
|
103
|
+
try {
|
|
104
|
+
cb(leaderState);
|
|
105
|
+
} catch {
|
|
106
|
+
// ignore listener errors
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- internal helpers ---
|
|
112
|
+
|
|
113
|
+
async function resolveOrCreateIdentity(): Promise<P2PIdentity> {
|
|
114
|
+
const existing = await resolveIdentity(getLocalStore());
|
|
115
|
+
if (existing) return existing;
|
|
116
|
+
|
|
117
|
+
// no identity found anywhere - generate a new one.
|
|
118
|
+
// node_id is filled in with the real iroh public key after midden boots.
|
|
119
|
+
const secretKey = new Uint8Array(32);
|
|
120
|
+
crypto.getRandomValues(secretKey);
|
|
121
|
+
const newIdentity: P2PIdentity = {
|
|
122
|
+
id: "p2p_identity",
|
|
123
|
+
secret_key: secretKey,
|
|
124
|
+
node_id: "",
|
|
125
|
+
created_at: Date.now(),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
await persistIdentity(newIdentity, getLocalStore());
|
|
129
|
+
return newIdentity;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function bootMidden(
|
|
133
|
+
secretKey: Uint8Array
|
|
134
|
+
): Promise<MiddenStreamNode | null> {
|
|
135
|
+
try {
|
|
136
|
+
// bundler target: wasm init happens at import time via vite-plugin-wasm.
|
|
137
|
+
const midden = await import("@freqhole/midden");
|
|
138
|
+
const node = await midden.MiddenNode.create_with_alpns(secretKey, [
|
|
139
|
+
AUTOMERGE_ALPN,
|
|
140
|
+
PLAYLISTZ_ALPN,
|
|
141
|
+
]);
|
|
142
|
+
return node as unknown as MiddenStreamNode;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.warn("[p2p] midden boot failed - p2p unavailable:", err);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- public api ---
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* start the p2p subsystem.
|
|
153
|
+
* resolves identity, acquires leadership, and boots the midden node if leader.
|
|
154
|
+
* safe to call multiple times - no-ops if already started.
|
|
155
|
+
*/
|
|
156
|
+
export async function startP2P(): Promise<void> {
|
|
157
|
+
if (started) return;
|
|
158
|
+
started = true;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
currentIdentity = await resolveOrCreateIdentity();
|
|
162
|
+
notifyIdentityListeners();
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.warn("[p2p] identity resolution failed:", err);
|
|
165
|
+
started = false;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const identityAtStart = currentIdentity;
|
|
170
|
+
|
|
171
|
+
cancelLeadership = acquireNodeLeadership({
|
|
172
|
+
onAcquired: async () => {
|
|
173
|
+
leaderState = true;
|
|
174
|
+
leadershipPhase = "leader";
|
|
175
|
+
notifyLeadershipListeners();
|
|
176
|
+
|
|
177
|
+
const node = await bootMidden(identityAtStart.secret_key);
|
|
178
|
+
if (node) {
|
|
179
|
+
// expose the node BEFORE notifying listeners - the iroh adapter's
|
|
180
|
+
// identity listener calls getNode() and would otherwise throw
|
|
181
|
+
currentNode = node;
|
|
182
|
+
|
|
183
|
+
// update node_id from the real iroh public key
|
|
184
|
+
const realNodeId = node.node_id();
|
|
185
|
+
if (realNodeId !== identityAtStart.node_id) {
|
|
186
|
+
currentIdentity = { ...identityAtStart, node_id: realNodeId };
|
|
187
|
+
try {
|
|
188
|
+
await persistIdentity(currentIdentity, getLocalStore());
|
|
189
|
+
} catch {
|
|
190
|
+
// non-fatal: node_id update will be retried on next boot
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// always notify: listeners subscribed before leadership was acquired
|
|
194
|
+
// need to learn the node is now available
|
|
195
|
+
notifyIdentityListeners();
|
|
196
|
+
}
|
|
197
|
+
flushNodeWaiters(node);
|
|
198
|
+
},
|
|
199
|
+
onStateChange: (state) => {
|
|
200
|
+
if (state === "waiting") {
|
|
201
|
+
leadershipPhase = "waiting";
|
|
202
|
+
// another tab holds the node - local waiters resolve null
|
|
203
|
+
flushNodeWaiters(null);
|
|
204
|
+
} else if (state === "unsupported") {
|
|
205
|
+
leadershipPhase = "unsupported";
|
|
206
|
+
}
|
|
207
|
+
if (state !== "leader" && leaderState) {
|
|
208
|
+
leaderState = false;
|
|
209
|
+
notifyLeadershipListeners();
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* stop p2p: release the leadership lock and clear the node reference.
|
|
217
|
+
*/
|
|
218
|
+
export function stopP2P(): void {
|
|
219
|
+
started = false;
|
|
220
|
+
cancelLeadership?.();
|
|
221
|
+
cancelLeadership = null;
|
|
222
|
+
currentNode = null;
|
|
223
|
+
if (leaderState) {
|
|
224
|
+
leaderState = false;
|
|
225
|
+
notifyLeadershipListeners();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** get the running midden node, or null if not leader or not yet started. */
|
|
230
|
+
export function getNode(): MiddenStreamNode | null {
|
|
231
|
+
return currentNode;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* wait for the local midden node to come up after startP2P.
|
|
236
|
+
* resolves with the node once booted, or null when this tab is not the
|
|
237
|
+
* leader (another tab holds the node), p2p was never started, or the
|
|
238
|
+
* timeout elapses.
|
|
239
|
+
*/
|
|
240
|
+
export function waitForNode(
|
|
241
|
+
timeoutMs = 30000
|
|
242
|
+
): Promise<MiddenStreamNode | null> {
|
|
243
|
+
if (currentNode) return Promise.resolve(currentNode);
|
|
244
|
+
if (!started || leadershipPhase === "waiting") {
|
|
245
|
+
return Promise.resolve(null);
|
|
246
|
+
}
|
|
247
|
+
return new Promise((resolve) => {
|
|
248
|
+
const timer = setTimeout(() => {
|
|
249
|
+
nodeWaiters.delete(waiter);
|
|
250
|
+
resolve(null);
|
|
251
|
+
}, timeoutMs);
|
|
252
|
+
const waiter = (node: MiddenStreamNode | null) => {
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
resolve(node);
|
|
255
|
+
};
|
|
256
|
+
nodeWaiters.add(waiter);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** get the current resolved identity, or null if not yet resolved. */
|
|
261
|
+
export function getIdentity(): P2PIdentity | null {
|
|
262
|
+
return currentIdentity;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* check whether an identity is already persisted (without creating one).
|
|
267
|
+
* used to auto-resume p2p on boot only for users who have enabled it.
|
|
268
|
+
*/
|
|
269
|
+
export async function hasExistingIdentity(): Promise<boolean> {
|
|
270
|
+
try {
|
|
271
|
+
const existing = await resolveIdentity(getLocalStore());
|
|
272
|
+
return existing !== null;
|
|
273
|
+
} catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** returns true if this tab holds the iroh node leadership lock. */
|
|
279
|
+
export function isLeader(): boolean {
|
|
280
|
+
return leaderState;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* subscribe to leadership state changes.
|
|
285
|
+
* calls cb immediately with current state. returns an unsubscribe function.
|
|
286
|
+
*/
|
|
287
|
+
export function onLeadershipChange(
|
|
288
|
+
cb: (isLeader: boolean) => void
|
|
289
|
+
): () => void {
|
|
290
|
+
leadershipListeners.add(cb);
|
|
291
|
+
cb(leaderState);
|
|
292
|
+
return () => {
|
|
293
|
+
leadershipListeners.delete(cb);
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* subscribe to identity changes. returns an unsubscribe function.
|
|
299
|
+
* the signature satisfies IrohNetworkAdapterOptions.onIdentityChange.
|
|
300
|
+
*/
|
|
301
|
+
export function onIdentityChange(
|
|
302
|
+
cb: (identity: P2PIdentity | null) => void
|
|
303
|
+
): () => void {
|
|
304
|
+
identityListeners.add(cb);
|
|
305
|
+
return () => {
|
|
306
|
+
identityListeners.delete(cb);
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* returns options suitable for constructing an IrohNetworkAdapter.
|
|
312
|
+
* pass directly to new IrohNetworkAdapter(getAdapterOptions()).
|
|
313
|
+
*/
|
|
314
|
+
export function getAdapterOptions(): IrohNetworkAdapterOptions {
|
|
315
|
+
return {
|
|
316
|
+
getNode: async () => {
|
|
317
|
+
if (!currentNode) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
"p2p: midden node is not available (not leader or not started)"
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return currentNode;
|
|
323
|
+
},
|
|
324
|
+
getIdentity: async () => currentIdentity,
|
|
325
|
+
onIdentityChange: (cb) => onIdentityChange(cb),
|
|
326
|
+
syncAlpn: AUTOMERGE_ALPN,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* reset all module state. for use in tests only.
|
|
332
|
+
*/
|
|
333
|
+
export function _resetForTests(): void {
|
|
334
|
+
started = false;
|
|
335
|
+
currentIdentity = null;
|
|
336
|
+
currentNode = null;
|
|
337
|
+
leaderState = false;
|
|
338
|
+
leadershipPhase = "unknown";
|
|
339
|
+
cancelLeadership = null;
|
|
340
|
+
_localStore = null;
|
|
341
|
+
identityListeners.clear();
|
|
342
|
+
leadershipListeners.clear();
|
|
343
|
+
nodeWaiters.clear();
|
|
344
|
+
}
|