@hashtree/worker 0.2.0 → 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 -3
- package/src/app-runtime.ts +393 -0
- package/src/capabilities/blossomBandwidthTracker.ts +74 -0
- package/src/capabilities/blossomTransport.ts +179 -0
- package/src/capabilities/connectivity.ts +54 -0
- package/src/capabilities/idbStorage.ts +94 -0
- package/src/capabilities/meshRouterStore.ts +426 -0
- package/src/capabilities/rootResolver.ts +497 -0
- package/src/client-id.ts +137 -0
- package/src/client.ts +501 -0
- package/{dist/entry.js → src/entry.ts} +1 -1
- package/src/htree-path.ts +53 -0
- package/src/htree-url.ts +156 -0
- package/src/index.ts +76 -0
- package/src/mediaStreaming.ts +64 -0
- package/src/p2p/boundedQueue.ts +168 -0
- package/src/p2p/errorMessage.ts +6 -0
- package/src/p2p/index.ts +48 -0
- package/src/p2p/lruCache.ts +78 -0
- package/src/p2p/meshQueryRouter.ts +361 -0
- package/src/p2p/protocol.ts +11 -0
- package/src/p2p/queryForwardingMachine.ts +197 -0
- package/src/p2p/signaling.ts +284 -0
- package/src/p2p/uploadRateLimiter.ts +85 -0
- package/src/p2p/webrtcController.ts +1168 -0
- package/src/p2p/webrtcProxy.ts +519 -0
- package/src/privacyGuards.ts +31 -0
- package/src/protocol.ts +124 -0
- package/src/relay/identity.ts +86 -0
- package/src/relay/mediaHandler.ts +1633 -0
- package/src/relay/ndk.ts +590 -0
- package/{dist/relay/nostr-wasm.js → src/relay/nostr-wasm.ts} +4 -1
- package/src/relay/nostr.ts +249 -0
- package/src/relay/protocol.ts +361 -0
- package/src/relay/publicAssetUrl.ts +25 -0
- package/src/relay/rootPathResolver.ts +50 -0
- package/src/relay/shims.d.ts +17 -0
- package/src/relay/signing.ts +332 -0
- package/src/relay/treeRootCache.ts +354 -0
- package/src/relay/treeRootSubscription.ts +577 -0
- package/src/relay/utils/constants.ts +139 -0
- package/src/relay/utils/errorMessage.ts +7 -0
- package/src/relay/utils/lruCache.ts +79 -0
- package/src/relay/webrtc.ts +5 -0
- package/src/relay/webrtcSignaling.ts +108 -0
- package/src/relay/worker.ts +1787 -0
- package/src/relay-client.ts +265 -0
- package/src/relay-entry.ts +1 -0
- package/src/runtime-network.ts +134 -0
- package/src/runtime.ts +153 -0
- package/{dist/transferableBytes.js → src/transferableBytes.ts} +2 -3
- package/src/tree-root.ts +851 -0
- package/src/types.ts +8 -0
- package/src/worker.ts +975 -0
- package/dist/app-runtime.d.ts +0 -60
- package/dist/app-runtime.d.ts.map +0 -1
- package/dist/app-runtime.js +0 -271
- package/dist/app-runtime.js.map +0 -1
- package/dist/capabilities/blossomBandwidthTracker.d.ts +0 -26
- package/dist/capabilities/blossomBandwidthTracker.d.ts.map +0 -1
- package/dist/capabilities/blossomBandwidthTracker.js +0 -53
- package/dist/capabilities/blossomBandwidthTracker.js.map +0 -1
- package/dist/capabilities/blossomTransport.d.ts +0 -22
- package/dist/capabilities/blossomTransport.d.ts.map +0 -1
- package/dist/capabilities/blossomTransport.js +0 -139
- package/dist/capabilities/blossomTransport.js.map +0 -1
- package/dist/capabilities/connectivity.d.ts +0 -3
- package/dist/capabilities/connectivity.d.ts.map +0 -1
- package/dist/capabilities/connectivity.js +0 -49
- package/dist/capabilities/connectivity.js.map +0 -1
- package/dist/capabilities/idbStorage.d.ts +0 -25
- package/dist/capabilities/idbStorage.d.ts.map +0 -1
- package/dist/capabilities/idbStorage.js +0 -73
- package/dist/capabilities/idbStorage.js.map +0 -1
- package/dist/capabilities/meshRouterStore.d.ts +0 -71
- package/dist/capabilities/meshRouterStore.d.ts.map +0 -1
- package/dist/capabilities/meshRouterStore.js +0 -316
- package/dist/capabilities/meshRouterStore.js.map +0 -1
- package/dist/capabilities/rootResolver.d.ts +0 -10
- package/dist/capabilities/rootResolver.d.ts.map +0 -1
- package/dist/capabilities/rootResolver.js +0 -392
- package/dist/capabilities/rootResolver.js.map +0 -1
- package/dist/client-id.d.ts +0 -18
- package/dist/client-id.d.ts.map +0 -1
- package/dist/client-id.js +0 -98
- package/dist/client-id.js.map +0 -1
- package/dist/client.d.ts +0 -61
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -417
- package/dist/client.js.map +0 -1
- package/dist/entry.d.ts +0 -2
- package/dist/entry.d.ts.map +0 -1
- package/dist/entry.js.map +0 -1
- package/dist/htree-path.d.ts +0 -13
- package/dist/htree-path.d.ts.map +0 -1
- package/dist/htree-path.js +0 -38
- package/dist/htree-path.js.map +0 -1
- package/dist/htree-url.d.ts +0 -22
- package/dist/htree-url.d.ts.map +0 -1
- package/dist/htree-url.js +0 -118
- package/dist/htree-url.js.map +0 -1
- package/dist/index.d.ts +0 -17
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -8
- package/dist/index.js.map +0 -1
- package/dist/mediaStreaming.d.ts +0 -7
- package/dist/mediaStreaming.d.ts.map +0 -1
- package/dist/mediaStreaming.js +0 -48
- package/dist/mediaStreaming.js.map +0 -1
- package/dist/p2p/boundedQueue.d.ts +0 -79
- package/dist/p2p/boundedQueue.d.ts.map +0 -1
- package/dist/p2p/boundedQueue.js +0 -134
- package/dist/p2p/boundedQueue.js.map +0 -1
- package/dist/p2p/errorMessage.d.ts +0 -5
- package/dist/p2p/errorMessage.d.ts.map +0 -1
- package/dist/p2p/errorMessage.js +0 -7
- package/dist/p2p/errorMessage.js.map +0 -1
- package/dist/p2p/index.d.ts +0 -8
- package/dist/p2p/index.d.ts.map +0 -1
- package/dist/p2p/index.js +0 -6
- package/dist/p2p/index.js.map +0 -1
- package/dist/p2p/lruCache.d.ts +0 -26
- package/dist/p2p/lruCache.d.ts.map +0 -1
- package/dist/p2p/lruCache.js +0 -65
- package/dist/p2p/lruCache.js.map +0 -1
- package/dist/p2p/meshQueryRouter.d.ts +0 -57
- package/dist/p2p/meshQueryRouter.d.ts.map +0 -1
- package/dist/p2p/meshQueryRouter.js +0 -264
- package/dist/p2p/meshQueryRouter.js.map +0 -1
- package/dist/p2p/protocol.d.ts +0 -10
- package/dist/p2p/protocol.d.ts.map +0 -1
- package/dist/p2p/protocol.js +0 -2
- package/dist/p2p/protocol.js.map +0 -1
- package/dist/p2p/queryForwardingMachine.d.ts +0 -46
- package/dist/p2p/queryForwardingMachine.d.ts.map +0 -1
- package/dist/p2p/queryForwardingMachine.js +0 -144
- package/dist/p2p/queryForwardingMachine.js.map +0 -1
- package/dist/p2p/signaling.d.ts +0 -63
- package/dist/p2p/signaling.d.ts.map +0 -1
- package/dist/p2p/signaling.js +0 -185
- package/dist/p2p/signaling.js.map +0 -1
- package/dist/p2p/uploadRateLimiter.d.ts +0 -21
- package/dist/p2p/uploadRateLimiter.d.ts.map +0 -1
- package/dist/p2p/uploadRateLimiter.js +0 -62
- package/dist/p2p/uploadRateLimiter.js.map +0 -1
- package/dist/p2p/webrtcController.d.ts +0 -176
- package/dist/p2p/webrtcController.d.ts.map +0 -1
- package/dist/p2p/webrtcController.js +0 -938
- package/dist/p2p/webrtcController.js.map +0 -1
- package/dist/p2p/webrtcProxy.d.ts +0 -62
- package/dist/p2p/webrtcProxy.d.ts.map +0 -1
- package/dist/p2p/webrtcProxy.js +0 -447
- package/dist/p2p/webrtcProxy.js.map +0 -1
- package/dist/privacyGuards.d.ts +0 -14
- package/dist/privacyGuards.d.ts.map +0 -1
- package/dist/privacyGuards.js +0 -27
- package/dist/privacyGuards.js.map +0 -1
- package/dist/protocol.d.ts +0 -225
- package/dist/protocol.d.ts.map +0 -1
- package/dist/protocol.js +0 -2
- package/dist/protocol.js.map +0 -1
- package/dist/relay/identity.d.ts +0 -36
- package/dist/relay/identity.d.ts.map +0 -1
- package/dist/relay/identity.js +0 -78
- package/dist/relay/identity.js.map +0 -1
- package/dist/relay/mediaHandler.d.ts +0 -64
- package/dist/relay/mediaHandler.d.ts.map +0 -1
- package/dist/relay/mediaHandler.js +0 -1285
- package/dist/relay/mediaHandler.js.map +0 -1
- package/dist/relay/ndk.d.ts +0 -96
- package/dist/relay/ndk.d.ts.map +0 -1
- package/dist/relay/ndk.js +0 -502
- package/dist/relay/ndk.js.map +0 -1
- package/dist/relay/nostr-wasm.d.ts +0 -14
- package/dist/relay/nostr-wasm.d.ts.map +0 -1
- package/dist/relay/nostr-wasm.js.map +0 -1
- package/dist/relay/nostr.d.ts +0 -60
- package/dist/relay/nostr.d.ts.map +0 -1
- package/dist/relay/nostr.js +0 -207
- package/dist/relay/nostr.js.map +0 -1
- package/dist/relay/protocol.d.ts +0 -592
- package/dist/relay/protocol.d.ts.map +0 -1
- package/dist/relay/protocol.js +0 -16
- package/dist/relay/protocol.js.map +0 -1
- package/dist/relay/publicAssetUrl.d.ts +0 -6
- package/dist/relay/publicAssetUrl.d.ts.map +0 -1
- package/dist/relay/publicAssetUrl.js +0 -14
- package/dist/relay/publicAssetUrl.js.map +0 -1
- package/dist/relay/rootPathResolver.d.ts +0 -9
- package/dist/relay/rootPathResolver.d.ts.map +0 -1
- package/dist/relay/rootPathResolver.js +0 -32
- package/dist/relay/rootPathResolver.js.map +0 -1
- package/dist/relay/signing.d.ts +0 -50
- package/dist/relay/signing.d.ts.map +0 -1
- package/dist/relay/signing.js +0 -299
- package/dist/relay/signing.js.map +0 -1
- package/dist/relay/treeRootCache.d.ts +0 -86
- package/dist/relay/treeRootCache.d.ts.map +0 -1
- package/dist/relay/treeRootCache.js +0 -269
- package/dist/relay/treeRootCache.js.map +0 -1
- package/dist/relay/treeRootSubscription.d.ts +0 -55
- package/dist/relay/treeRootSubscription.d.ts.map +0 -1
- package/dist/relay/treeRootSubscription.js +0 -478
- package/dist/relay/treeRootSubscription.js.map +0 -1
- package/dist/relay/utils/constants.d.ts +0 -76
- package/dist/relay/utils/constants.d.ts.map +0 -1
- package/dist/relay/utils/constants.js +0 -113
- package/dist/relay/utils/constants.js.map +0 -1
- package/dist/relay/utils/errorMessage.d.ts +0 -5
- package/dist/relay/utils/errorMessage.d.ts.map +0 -1
- package/dist/relay/utils/errorMessage.js +0 -8
- package/dist/relay/utils/errorMessage.js.map +0 -1
- package/dist/relay/utils/lruCache.d.ts +0 -26
- package/dist/relay/utils/lruCache.d.ts.map +0 -1
- package/dist/relay/utils/lruCache.js +0 -66
- package/dist/relay/utils/lruCache.js.map +0 -1
- package/dist/relay/webrtc.d.ts +0 -2
- package/dist/relay/webrtc.d.ts.map +0 -1
- package/dist/relay/webrtc.js +0 -3
- package/dist/relay/webrtc.js.map +0 -1
- package/dist/relay/webrtcSignaling.d.ts +0 -37
- package/dist/relay/webrtcSignaling.d.ts.map +0 -1
- package/dist/relay/webrtcSignaling.js +0 -86
- package/dist/relay/webrtcSignaling.js.map +0 -1
- package/dist/relay/worker.d.ts +0 -12
- package/dist/relay/worker.d.ts.map +0 -1
- package/dist/relay/worker.js +0 -1540
- package/dist/relay/worker.js.map +0 -1
- package/dist/relay-client.d.ts +0 -31
- package/dist/relay-client.d.ts.map +0 -1
- package/dist/relay-client.js +0 -197
- package/dist/relay-client.js.map +0 -1
- package/dist/relay-entry.d.ts +0 -2
- package/dist/relay-entry.d.ts.map +0 -1
- package/dist/relay-entry.js +0 -2
- package/dist/relay-entry.js.map +0 -1
- package/dist/runtime-network.d.ts +0 -23
- package/dist/runtime-network.d.ts.map +0 -1
- package/dist/runtime-network.js +0 -105
- package/dist/runtime-network.js.map +0 -1
- package/dist/runtime.d.ts +0 -24
- package/dist/runtime.d.ts.map +0 -1
- package/dist/runtime.js +0 -126
- package/dist/runtime.js.map +0 -1
- package/dist/transferableBytes.d.ts +0 -2
- package/dist/transferableBytes.d.ts.map +0 -1
- package/dist/transferableBytes.js.map +0 -1
- package/dist/tree-root.d.ts +0 -201
- package/dist/tree-root.d.ts.map +0 -1
- package/dist/tree-root.js +0 -632
- package/dist/tree-root.js.map +0 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/worker.d.ts +0 -9
- package/dist/worker.d.ts.map +0 -1
- package/dist/worker.js +0 -792
- package/dist/worker.js.map +0 -1
package/src/client.ts
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { generateRequestId, type CID } from '@hashtree/core';
|
|
2
|
+
import type {
|
|
3
|
+
BlossomBandwidthState,
|
|
4
|
+
BlossomServerConfig,
|
|
5
|
+
BlobSource,
|
|
6
|
+
ConnectivityState,
|
|
7
|
+
RootResolveOptions,
|
|
8
|
+
WorkerDiagnosticEvent,
|
|
9
|
+
UploadProgressState,
|
|
10
|
+
WorkerConfig,
|
|
11
|
+
WorkerRequest,
|
|
12
|
+
WorkerResponse,
|
|
13
|
+
} from './protocol.js';
|
|
14
|
+
|
|
15
|
+
type PendingRequest = {
|
|
16
|
+
resolve: (value: WorkerResponse) => void;
|
|
17
|
+
reject: (error: Error) => void;
|
|
18
|
+
timeoutId: ReturnType<typeof setTimeout>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type WorkerFactory = URL | string | (new () => Worker);
|
|
22
|
+
export type P2PFetchHandler = (hashHex: string) => Promise<Uint8Array | null>;
|
|
23
|
+
|
|
24
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
25
|
+
const PUT_BLOB_TIMEOUT_MS = 15 * 60_000;
|
|
26
|
+
const STREAM_APPEND_TIMEOUT_MS = 60_000;
|
|
27
|
+
type WorkerRequestPayload = WorkerRequest extends infer T
|
|
28
|
+
? T extends { id: string }
|
|
29
|
+
? Omit<T, 'id'>
|
|
30
|
+
: never
|
|
31
|
+
: never;
|
|
32
|
+
|
|
33
|
+
export class HashtreeWorkerClient {
|
|
34
|
+
private readonly workerFactory: WorkerFactory;
|
|
35
|
+
private readonly config: WorkerConfig;
|
|
36
|
+
private worker: Worker | null = null;
|
|
37
|
+
private initPromise: Promise<void> | null = null;
|
|
38
|
+
private pending = new Map<string, PendingRequest>();
|
|
39
|
+
private connectivityListeners = new Set<(state: ConnectivityState) => void>();
|
|
40
|
+
private uploadProgressListeners = new Set<(progress: UploadProgressState) => void>();
|
|
41
|
+
private blossomBandwidthListeners = new Set<(stats: BlossomBandwidthState) => void>();
|
|
42
|
+
private diagnosticListeners = new Set<(event: WorkerDiagnosticEvent) => void>();
|
|
43
|
+
private rootWatchListeners = new Map<string, (cid: CID | null) => void>();
|
|
44
|
+
private pendingRootWatchUpdates = new Map<string, CID | null>();
|
|
45
|
+
private p2pFetchHandler: P2PFetchHandler | null = null;
|
|
46
|
+
|
|
47
|
+
constructor(workerFactory: WorkerFactory, config: WorkerConfig = {}) {
|
|
48
|
+
this.workerFactory = workerFactory;
|
|
49
|
+
this.config = config;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async init(): Promise<void> {
|
|
53
|
+
if (this.initPromise) return this.initPromise;
|
|
54
|
+
try {
|
|
55
|
+
this.spawnWorker();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.initPromise = new Promise<void>((resolve, reject) => {
|
|
61
|
+
if (!this.worker) {
|
|
62
|
+
reject(new Error('Failed to create worker'));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const id = generateRequestId();
|
|
67
|
+
const timeoutId = setTimeout(() => {
|
|
68
|
+
this.pending.delete(id);
|
|
69
|
+
reject(new Error('Worker init timed out'));
|
|
70
|
+
}, REQUEST_TIMEOUT_MS);
|
|
71
|
+
|
|
72
|
+
this.pending.set(id, {
|
|
73
|
+
resolve: (message) => {
|
|
74
|
+
if (message.type === 'ready') {
|
|
75
|
+
resolve();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
reject(new Error('Unexpected init response'));
|
|
79
|
+
},
|
|
80
|
+
reject: (error) => reject(error),
|
|
81
|
+
timeoutId,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.worker.postMessage({
|
|
85
|
+
type: 'init',
|
|
86
|
+
id,
|
|
87
|
+
config: this.config,
|
|
88
|
+
} as WorkerRequest);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return this.initPromise;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private spawnWorker(): void {
|
|
95
|
+
if (this.workerFactory instanceof URL) {
|
|
96
|
+
this.worker = new Worker(this.workerFactory, { type: 'module' });
|
|
97
|
+
} else if (typeof this.workerFactory === 'string') {
|
|
98
|
+
this.worker = new Worker(this.workerFactory, { type: 'module' });
|
|
99
|
+
} else {
|
|
100
|
+
this.worker = new this.workerFactory();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
|
|
104
|
+
const message = event.data;
|
|
105
|
+
|
|
106
|
+
if (message.type === 'connectivityUpdate') {
|
|
107
|
+
this.connectivityListeners.forEach(listener => listener(message.state));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (message.type === 'uploadProgress') {
|
|
112
|
+
this.uploadProgressListeners.forEach(listener => listener(message.progress));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (message.type === 'blossomBandwidth') {
|
|
117
|
+
this.blossomBandwidthListeners.forEach(listener => listener(message.stats));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (message.type === 'diagnostic') {
|
|
122
|
+
this.diagnosticListeners.forEach(listener => listener(message.event));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (message.type === 'rootUpdate') {
|
|
127
|
+
const listener = this.rootWatchListeners.get(message.watchId);
|
|
128
|
+
if (listener) {
|
|
129
|
+
listener(message.cid ?? null);
|
|
130
|
+
} else {
|
|
131
|
+
this.pendingRootWatchUpdates.set(message.watchId, message.cid ?? null);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (message.type === 'p2pFetch') {
|
|
137
|
+
void this.handleP2PFetch(message.requestId, message.hashHex);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (message.type === 'error' && message.id) {
|
|
142
|
+
this.rejectPending(message.id, new Error(message.error));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if ('id' in message && typeof message.id === 'string') {
|
|
147
|
+
this.resolvePending(message.id, message);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
this.worker.onerror = (event) => {
|
|
152
|
+
const errorMessage = event instanceof ErrorEvent ? event.message : 'Worker error';
|
|
153
|
+
this.rejectAllPending(new Error(errorMessage));
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private rejectAllPending(error: Error): void {
|
|
158
|
+
for (const [id, pending] of this.pending.entries()) {
|
|
159
|
+
clearTimeout(pending.timeoutId);
|
|
160
|
+
pending.reject(error);
|
|
161
|
+
this.pending.delete(id);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private resolvePending(id: string, message: WorkerResponse): void {
|
|
166
|
+
const pending = this.pending.get(id);
|
|
167
|
+
if (!pending) return;
|
|
168
|
+
clearTimeout(pending.timeoutId);
|
|
169
|
+
pending.resolve(message);
|
|
170
|
+
this.pending.delete(id);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private rejectPending(id: string, error: Error): void {
|
|
174
|
+
const pending = this.pending.get(id);
|
|
175
|
+
if (!pending) return;
|
|
176
|
+
clearTimeout(pending.timeoutId);
|
|
177
|
+
pending.reject(error);
|
|
178
|
+
this.pending.delete(id);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async handleP2PFetch(requestId: string, hashHex: string): Promise<void> {
|
|
182
|
+
if (!this.worker) return;
|
|
183
|
+
const id = generateRequestId();
|
|
184
|
+
|
|
185
|
+
if (!this.p2pFetchHandler) {
|
|
186
|
+
this.worker.postMessage({
|
|
187
|
+
type: 'p2pFetchResult',
|
|
188
|
+
id,
|
|
189
|
+
requestId,
|
|
190
|
+
} as WorkerRequest);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const data = await this.p2pFetchHandler(hashHex);
|
|
196
|
+
if (data && data.byteLength > 0) {
|
|
197
|
+
this.worker.postMessage(
|
|
198
|
+
{
|
|
199
|
+
type: 'p2pFetchResult',
|
|
200
|
+
id,
|
|
201
|
+
requestId,
|
|
202
|
+
data,
|
|
203
|
+
} as WorkerRequest,
|
|
204
|
+
[data.buffer]
|
|
205
|
+
);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.worker.postMessage({
|
|
210
|
+
type: 'p2pFetchResult',
|
|
211
|
+
id,
|
|
212
|
+
requestId,
|
|
213
|
+
} as WorkerRequest);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
216
|
+
this.worker.postMessage({
|
|
217
|
+
type: 'p2pFetchResult',
|
|
218
|
+
id,
|
|
219
|
+
requestId,
|
|
220
|
+
error,
|
|
221
|
+
} as WorkerRequest);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async request(
|
|
226
|
+
payload: WorkerRequestPayload,
|
|
227
|
+
timeoutMs = REQUEST_TIMEOUT_MS,
|
|
228
|
+
transfer: Transferable[] = []
|
|
229
|
+
): Promise<WorkerResponse> {
|
|
230
|
+
await this.initIfNeeded();
|
|
231
|
+
if (!this.worker) {
|
|
232
|
+
throw new Error('Worker not initialized');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const id = generateRequestId();
|
|
236
|
+
const message = { ...payload, id } as WorkerRequest;
|
|
237
|
+
|
|
238
|
+
return new Promise<WorkerResponse>((resolve, reject) => {
|
|
239
|
+
const timeoutId = setTimeout(() => {
|
|
240
|
+
this.pending.delete(id);
|
|
241
|
+
reject(new Error(`Worker request timed out: ${payload.type}`));
|
|
242
|
+
}, timeoutMs);
|
|
243
|
+
|
|
244
|
+
this.pending.set(id, { resolve, reject, timeoutId });
|
|
245
|
+
this.worker?.postMessage(message, transfer);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private async initIfNeeded(): Promise<void> {
|
|
250
|
+
if (!this.initPromise) {
|
|
251
|
+
await this.init();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
await this.initPromise;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async putBlob(data: Uint8Array, mimeType?: string, upload = true): Promise<{ hashHex: string; nhash: string }> {
|
|
258
|
+
const res = await this.request({ type: 'putBlob', data, mimeType, upload }, PUT_BLOB_TIMEOUT_MS);
|
|
259
|
+
if (res.type !== 'blobStored') {
|
|
260
|
+
throw new Error('Unexpected response for putBlob');
|
|
261
|
+
}
|
|
262
|
+
if (!res.hashHex || !res.nhash) {
|
|
263
|
+
throw new Error('Failed to store blob');
|
|
264
|
+
}
|
|
265
|
+
return { hashHex: res.hashHex, nhash: res.nhash };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async beginPutBlobStream(mimeType?: string, upload = true): Promise<string> {
|
|
269
|
+
const res = await this.request({ type: 'beginPutBlobStream', mimeType, upload });
|
|
270
|
+
if (res.type !== 'blobStreamStarted') {
|
|
271
|
+
throw new Error('Unexpected response for beginPutBlobStream');
|
|
272
|
+
}
|
|
273
|
+
if (!res.streamId) {
|
|
274
|
+
throw new Error('Failed to start blob stream');
|
|
275
|
+
}
|
|
276
|
+
return res.streamId;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async appendPutBlobStream(streamId: string, chunk: Uint8Array): Promise<void> {
|
|
280
|
+
const res = await this.request(
|
|
281
|
+
{ type: 'appendPutBlobStream', streamId, chunk },
|
|
282
|
+
STREAM_APPEND_TIMEOUT_MS,
|
|
283
|
+
[chunk.buffer]
|
|
284
|
+
);
|
|
285
|
+
if (res.type !== 'void') {
|
|
286
|
+
throw new Error('Unexpected response for appendPutBlobStream');
|
|
287
|
+
}
|
|
288
|
+
if (res.error) {
|
|
289
|
+
throw new Error(res.error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async finishPutBlobStream(streamId: string): Promise<{ hashHex: string; nhash: string }> {
|
|
294
|
+
const res = await this.request({ type: 'finishPutBlobStream', streamId }, PUT_BLOB_TIMEOUT_MS);
|
|
295
|
+
if (res.type !== 'blobStored') {
|
|
296
|
+
throw new Error('Unexpected response for finishPutBlobStream');
|
|
297
|
+
}
|
|
298
|
+
if (!res.hashHex || !res.nhash) {
|
|
299
|
+
throw new Error('Failed to finalize blob stream');
|
|
300
|
+
}
|
|
301
|
+
return { hashHex: res.hashHex, nhash: res.nhash };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async cancelPutBlobStream(streamId: string): Promise<void> {
|
|
305
|
+
const res = await this.request({ type: 'cancelPutBlobStream', streamId });
|
|
306
|
+
if (res.type !== 'void') {
|
|
307
|
+
throw new Error('Unexpected response for cancelPutBlobStream');
|
|
308
|
+
}
|
|
309
|
+
if (res.error) {
|
|
310
|
+
throw new Error(res.error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async getBlob(hashHex: string): Promise<{ data: Uint8Array; source: BlobSource }> {
|
|
315
|
+
const res = await this.request({ type: 'getBlob', hashHex });
|
|
316
|
+
if (res.type !== 'blob') {
|
|
317
|
+
throw new Error('Unexpected response for getBlob');
|
|
318
|
+
}
|
|
319
|
+
if (res.error || !res.data || !res.source) {
|
|
320
|
+
throw new Error(res.error || 'Blob not found');
|
|
321
|
+
}
|
|
322
|
+
return { data: res.data, source: res.source };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async getBlobForPeer(hashHex: string): Promise<Uint8Array | null> {
|
|
326
|
+
const res = await this.request({ type: 'getBlob', hashHex, forPeer: true });
|
|
327
|
+
if (res.type !== 'blob') {
|
|
328
|
+
throw new Error('Unexpected response for getBlobForPeer');
|
|
329
|
+
}
|
|
330
|
+
if (res.error || !res.data) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
return res.data;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async setBlossomServers(servers: BlossomServerConfig[]): Promise<void> {
|
|
337
|
+
const res = await this.request({ type: 'setBlossomServers', servers });
|
|
338
|
+
if (res.type !== 'void') {
|
|
339
|
+
throw new Error('Unexpected response for setBlossomServers');
|
|
340
|
+
}
|
|
341
|
+
if (res.error) {
|
|
342
|
+
throw new Error(res.error);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async registerMediaPort(port: MessagePort): Promise<void> {
|
|
347
|
+
const res = await this.request({ type: 'registerMediaPort', port }, REQUEST_TIMEOUT_MS, [port]);
|
|
348
|
+
if (res.type !== 'void') {
|
|
349
|
+
throw new Error('Unexpected response for registerMediaPort');
|
|
350
|
+
}
|
|
351
|
+
if (res.error) {
|
|
352
|
+
throw new Error(res.error);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async setStorageMaxBytes(maxBytes: number): Promise<void> {
|
|
357
|
+
const res = await this.request({ type: 'setStorageMaxBytes', maxBytes });
|
|
358
|
+
if (res.type !== 'void') {
|
|
359
|
+
throw new Error('Unexpected response for setStorageMaxBytes');
|
|
360
|
+
}
|
|
361
|
+
if (res.error) {
|
|
362
|
+
throw new Error(res.error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async getStorageStats(): Promise<{ items: number; bytes: number; maxBytes: number }> {
|
|
367
|
+
const res = await this.request({ type: 'getStorageStats' });
|
|
368
|
+
if (res.type !== 'storageStats') {
|
|
369
|
+
throw new Error('Unexpected response for getStorageStats');
|
|
370
|
+
}
|
|
371
|
+
if (res.error) {
|
|
372
|
+
throw new Error(res.error);
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
items: res.items,
|
|
376
|
+
bytes: res.bytes,
|
|
377
|
+
maxBytes: res.maxBytes,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async probeConnectivity(): Promise<ConnectivityState> {
|
|
382
|
+
const res = await this.request({ type: 'probeConnectivity' });
|
|
383
|
+
if (res.type !== 'connectivity') {
|
|
384
|
+
throw new Error('Unexpected response for probeConnectivity');
|
|
385
|
+
}
|
|
386
|
+
if (res.error || !res.state) {
|
|
387
|
+
throw new Error(res.error || 'Connectivity probe failed');
|
|
388
|
+
}
|
|
389
|
+
return res.state;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async resolveRoot(npub: string, path?: string, options: RootResolveOptions = {}): Promise<CID | null> {
|
|
393
|
+
const res = await this.request({
|
|
394
|
+
type: 'resolveRoot',
|
|
395
|
+
npub,
|
|
396
|
+
path,
|
|
397
|
+
timeoutMs: options.timeoutMs,
|
|
398
|
+
settleMs: options.settleMs,
|
|
399
|
+
});
|
|
400
|
+
if (res.type !== 'cid') {
|
|
401
|
+
throw new Error('Unexpected response for resolveRoot');
|
|
402
|
+
}
|
|
403
|
+
if (res.error) {
|
|
404
|
+
throw new Error(res.error);
|
|
405
|
+
}
|
|
406
|
+
return res.cid ?? null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async watchRoot(
|
|
410
|
+
npub: string,
|
|
411
|
+
path: string | undefined,
|
|
412
|
+
listener: (cid: CID | null) => void,
|
|
413
|
+
options: RootResolveOptions = {},
|
|
414
|
+
): Promise<() => Promise<void>> {
|
|
415
|
+
const res = await this.request({
|
|
416
|
+
type: 'watchRoot',
|
|
417
|
+
npub,
|
|
418
|
+
path,
|
|
419
|
+
timeoutMs: options.timeoutMs,
|
|
420
|
+
settleMs: options.settleMs,
|
|
421
|
+
});
|
|
422
|
+
if (res.type !== 'rootWatchStarted') {
|
|
423
|
+
throw new Error('Unexpected response for watchRoot');
|
|
424
|
+
}
|
|
425
|
+
if (res.error) {
|
|
426
|
+
throw new Error(res.error);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
this.rootWatchListeners.set(res.watchId, listener);
|
|
430
|
+
if ('cid' in res) {
|
|
431
|
+
listener(res.cid ?? null);
|
|
432
|
+
}
|
|
433
|
+
if (this.pendingRootWatchUpdates.has(res.watchId)) {
|
|
434
|
+
const pendingCid = this.pendingRootWatchUpdates.get(res.watchId) ?? null;
|
|
435
|
+
this.pendingRootWatchUpdates.delete(res.watchId);
|
|
436
|
+
listener(pendingCid);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return async () => {
|
|
440
|
+
this.rootWatchListeners.delete(res.watchId);
|
|
441
|
+
this.pendingRootWatchUpdates.delete(res.watchId);
|
|
442
|
+
try {
|
|
443
|
+
const stopRes = await this.request({ type: 'unwatchRoot', watchId: res.watchId });
|
|
444
|
+
if (stopRes.type !== 'void') {
|
|
445
|
+
throw new Error('Unexpected response for unwatchRoot');
|
|
446
|
+
}
|
|
447
|
+
if (stopRes.error) {
|
|
448
|
+
throw new Error(stopRes.error);
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
// Ignore cleanup failures after local listener removal.
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
onConnectivityUpdate(listener: (state: ConnectivityState) => void): () => void {
|
|
457
|
+
this.connectivityListeners.add(listener);
|
|
458
|
+
return () => {
|
|
459
|
+
this.connectivityListeners.delete(listener);
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
onUploadProgress(listener: (progress: UploadProgressState) => void): () => void {
|
|
464
|
+
this.uploadProgressListeners.add(listener);
|
|
465
|
+
return () => {
|
|
466
|
+
this.uploadProgressListeners.delete(listener);
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
onBlossomBandwidth(listener: (stats: BlossomBandwidthState) => void): () => void {
|
|
471
|
+
this.blossomBandwidthListeners.add(listener);
|
|
472
|
+
return () => {
|
|
473
|
+
this.blossomBandwidthListeners.delete(listener);
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
onDiagnostic(listener: (event: WorkerDiagnosticEvent) => void): () => void {
|
|
478
|
+
this.diagnosticListeners.add(listener);
|
|
479
|
+
return () => {
|
|
480
|
+
this.diagnosticListeners.delete(listener);
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
setP2PFetchHandler(handler: P2PFetchHandler | null): void {
|
|
485
|
+
this.p2pFetchHandler = handler;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async close(): Promise<void> {
|
|
489
|
+
try {
|
|
490
|
+
await this.request({ type: 'close' });
|
|
491
|
+
} catch {
|
|
492
|
+
// Ignore close errors.
|
|
493
|
+
}
|
|
494
|
+
this.rootWatchListeners.clear();
|
|
495
|
+
this.pendingRootWatchUpdates.clear();
|
|
496
|
+
this.worker?.terminate();
|
|
497
|
+
this.worker = null;
|
|
498
|
+
this.initPromise = null;
|
|
499
|
+
this.rejectAllPending(new Error('Worker closed'));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface ParsedMutableHtreePath {
|
|
2
|
+
npub: string;
|
|
3
|
+
treeName: string;
|
|
4
|
+
filePath: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ParsedImmutableHtreePath {
|
|
8
|
+
nhash: string;
|
|
9
|
+
filePath: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function safeDecodePathSegment(segment: string): string {
|
|
13
|
+
try {
|
|
14
|
+
return decodeURIComponent(segment);
|
|
15
|
+
} catch {
|
|
16
|
+
return segment;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getRawHtreePath(url: URL): string {
|
|
21
|
+
const pathMatch = url.href.match(/^[^:]+:\/\/[^/]+(.*)$/);
|
|
22
|
+
return pathMatch ? (pathMatch[1]?.split('?')[0] || '') : url.pathname;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseMutableHtreePath(rawPath: string): ParsedMutableHtreePath | null {
|
|
26
|
+
const parts = rawPath.replace(/^\/+/, '').split('/');
|
|
27
|
+
if (parts[0] !== 'htree' || parts.length < 3 || !parts[1]?.startsWith('npub1')) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const treeName = safeDecodePathSegment(parts[2] || '');
|
|
32
|
+
if (!treeName) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
npub: parts[1],
|
|
38
|
+
treeName,
|
|
39
|
+
filePath: parts.slice(3).map(safeDecodePathSegment).join('/'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseImmutableHtreePath(rawPath: string): ParsedImmutableHtreePath | null {
|
|
44
|
+
const parts = rawPath.replace(/^\/+/, '').split('/');
|
|
45
|
+
if (parts[0] !== 'htree' || parts.length < 2 || !parts[1]?.startsWith('nhash1')) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
nhash: parts[1],
|
|
51
|
+
filePath: parts.slice(2).map(safeDecodePathSegment).join('/'),
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/htree-url.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canUseInjectedHtreeServerUrl,
|
|
3
|
+
resolveRuntimeHtreeBaseUrl,
|
|
4
|
+
type HtreeRuntimeWindowLike,
|
|
5
|
+
} from './runtime.js';
|
|
6
|
+
|
|
7
|
+
export type ParsedHtreeUrl =
|
|
8
|
+
| { kind: 'mutable'; npub: string; treeName: string; path: string }
|
|
9
|
+
| { kind: 'immutable'; nhash: string; path: string };
|
|
10
|
+
|
|
11
|
+
export type MutableHtreeRequestStyle = 'htree' | 'gateway';
|
|
12
|
+
|
|
13
|
+
export interface ResolveHtreeRequestUrlOptions {
|
|
14
|
+
windowLike?: HtreeRuntimeWindowLike;
|
|
15
|
+
fallbackBaseUrl?: string | null;
|
|
16
|
+
baseUrl?: string | null;
|
|
17
|
+
mutableStyle?: MutableHtreeRequestStyle;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function safeDecodePathSegment(segment: string): string {
|
|
21
|
+
try {
|
|
22
|
+
return decodeURIComponent(segment);
|
|
23
|
+
} catch {
|
|
24
|
+
return segment;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function encodePath(path: string): string {
|
|
29
|
+
return path
|
|
30
|
+
.split('/')
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.map((segment) => encodeURIComponent(segment))
|
|
33
|
+
.join('/');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function stripQueryAndHash(path: string): string {
|
|
37
|
+
const hashIndex = path.indexOf('#');
|
|
38
|
+
const pathWithoutHash = hashIndex === -1 ? path : path.slice(0, hashIndex);
|
|
39
|
+
const queryIndex = pathWithoutHash.indexOf('?');
|
|
40
|
+
return queryIndex === -1 ? pathWithoutHash : pathWithoutHash.slice(0, queryIndex);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeRelativePath(path: string): string {
|
|
44
|
+
return stripQueryAndHash(path).replace(/^\/+/, '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveMutableRequestStyle(
|
|
48
|
+
windowLike: HtreeRuntimeWindowLike | undefined,
|
|
49
|
+
baseUrl: string,
|
|
50
|
+
explicitStyle?: MutableHtreeRequestStyle,
|
|
51
|
+
): MutableHtreeRequestStyle {
|
|
52
|
+
if (explicitStyle) return explicitStyle;
|
|
53
|
+
if (!baseUrl) return 'htree';
|
|
54
|
+
if (canUseInjectedHtreeServerUrl(windowLike)) return 'htree';
|
|
55
|
+
return 'gateway';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseHtreeUrl(input: string): ParsedHtreeUrl | null {
|
|
59
|
+
const trimmed = input.trim();
|
|
60
|
+
if (!trimmed) return null;
|
|
61
|
+
|
|
62
|
+
if (trimmed.startsWith('htree://')) {
|
|
63
|
+
const withoutScheme = trimmed.slice('htree://'.length);
|
|
64
|
+
const firstSlash = withoutScheme.indexOf('/');
|
|
65
|
+
const head = firstSlash === -1 ? withoutScheme : withoutScheme.slice(0, firstSlash);
|
|
66
|
+
const tail = firstSlash === -1 ? '' : withoutScheme.slice(firstSlash + 1);
|
|
67
|
+
const rawSegments = normalizeRelativePath(tail).split('/').filter(Boolean);
|
|
68
|
+
|
|
69
|
+
if (head.startsWith('npub1')) {
|
|
70
|
+
const [rawTreeName = '', ...rawPathSegments] = rawSegments;
|
|
71
|
+
const treeName = safeDecodePathSegment(rawTreeName);
|
|
72
|
+
if (!treeName) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
kind: 'mutable',
|
|
77
|
+
npub: head,
|
|
78
|
+
treeName,
|
|
79
|
+
path: rawPathSegments.map(safeDecodePathSegment).join('/'),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (head.startsWith('nhash1')) {
|
|
84
|
+
return {
|
|
85
|
+
kind: 'immutable',
|
|
86
|
+
nhash: head,
|
|
87
|
+
path: rawSegments.map(safeDecodePathSegment).join('/'),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (trimmed.startsWith('nhash1')) {
|
|
95
|
+
return {
|
|
96
|
+
kind: 'immutable',
|
|
97
|
+
nhash: stripQueryAndHash(trimmed),
|
|
98
|
+
path: '',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function buildHtreeRequestPath(
|
|
106
|
+
input: string | ParsedHtreeUrl,
|
|
107
|
+
mutableStyle: MutableHtreeRequestStyle = 'htree',
|
|
108
|
+
): string | null {
|
|
109
|
+
if (typeof input === 'string') {
|
|
110
|
+
const trimmed = input.trim();
|
|
111
|
+
if (!trimmed) return null;
|
|
112
|
+
if (trimmed.startsWith('/htree/')) {
|
|
113
|
+
return trimmed;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const parsed = typeof input === 'string' ? parseHtreeUrl(input) : input;
|
|
118
|
+
if (!parsed) return typeof input === 'string' ? input.trim() : null;
|
|
119
|
+
|
|
120
|
+
if (parsed.kind === 'mutable') {
|
|
121
|
+
const encodedTreeName = encodeURIComponent(parsed.treeName);
|
|
122
|
+
const encodedPath = encodePath(parsed.path);
|
|
123
|
+
const prefix = mutableStyle === 'htree' ? `/htree/${parsed.npub}` : `/${parsed.npub}`;
|
|
124
|
+
return `${prefix}/${encodedTreeName}${encodedPath ? `/${encodedPath}` : ''}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const encodedPath = encodePath(parsed.path);
|
|
128
|
+
return `/htree/${parsed.nhash}${encodedPath ? `/${encodedPath}` : ''}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function resolveHtreeRequestUrl(
|
|
132
|
+
input: string | ParsedHtreeUrl,
|
|
133
|
+
options: ResolveHtreeRequestUrlOptions = {},
|
|
134
|
+
): string {
|
|
135
|
+
const trimmedInput = typeof input === 'string' ? input.trim() : '';
|
|
136
|
+
if (trimmedInput && /^https?:\/\//i.test(trimmedInput)) {
|
|
137
|
+
return trimmedInput;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const baseUrl = options.baseUrl?.trim()
|
|
141
|
+
? options.baseUrl.trim().replace(/\/$/, '')
|
|
142
|
+
: resolveRuntimeHtreeBaseUrl({
|
|
143
|
+
windowLike: options.windowLike,
|
|
144
|
+
fallbackBaseUrl: options.fallbackBaseUrl,
|
|
145
|
+
});
|
|
146
|
+
const mutableStyle = resolveMutableRequestStyle(options.windowLike, baseUrl, options.mutableStyle);
|
|
147
|
+
const requestPath = buildHtreeRequestPath(input, mutableStyle);
|
|
148
|
+
|
|
149
|
+
if (!requestPath) {
|
|
150
|
+
return trimmedInput;
|
|
151
|
+
}
|
|
152
|
+
if (!baseUrl) {
|
|
153
|
+
return requestPath;
|
|
154
|
+
}
|
|
155
|
+
return `${baseUrl}${requestPath}`;
|
|
156
|
+
}
|