@wovin/core 0.2.2 → 0.3.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/dist/applog/applog-utils.d.ts +25 -1
- package/dist/applog/applog-utils.d.ts.map +1 -1
- package/dist/applog/datom-types.d.ts.map +1 -1
- package/dist/applog.js +5 -3
- package/dist/blockstore.js +2 -0
- package/dist/blockstore.js.map +1 -1
- package/dist/{chunk-SHUHRHOT.js → chunk-4MKPGQIM.js} +17 -18
- package/dist/chunk-4MKPGQIM.js.map +1 -0
- package/dist/chunk-64EJIJAJ.js +17 -0
- package/dist/chunk-64EJIJAJ.js.map +1 -0
- package/dist/{chunk-HUIQ54TT.js → chunk-6CSJTSQP.js} +3 -3
- package/dist/chunk-7QEGHKR4.js +17 -0
- package/dist/chunk-7QEGHKR4.js.map +1 -0
- package/dist/{chunk-BLF5MAWU.js → chunk-BIYQEX3N.js} +2 -2
- package/dist/{chunk-6ALNRM3J.js → chunk-H3JNNTVP.js} +21 -8
- package/dist/chunk-H3JNNTVP.js.map +1 -0
- package/dist/{chunk-OC6Z6CQW.js → chunk-H4YVJKB7.js} +2 -2
- package/dist/chunk-ICBK7NC4.js +27 -0
- package/dist/chunk-ICBK7NC4.js.map +1 -0
- package/dist/chunk-N5QPZNKD.js +288 -0
- package/dist/chunk-N5QPZNKD.js.map +1 -0
- package/dist/{chunk-3SUFNJEZ.js → chunk-N7SEGHU4.js} +7 -2
- package/dist/{chunk-3SUFNJEZ.js.map → chunk-N7SEGHU4.js.map} +1 -1
- package/dist/chunk-XF4DWOAE.js +25 -0
- package/dist/chunk-XF4DWOAE.js.map +1 -0
- package/dist/index.js +20 -10
- package/dist/ipfs/car.d.ts.map +1 -1
- package/dist/ipfs.js +4 -4
- package/dist/ipns/gateway-resolver.d.ts +21 -0
- package/dist/ipns/gateway-resolver.d.ts.map +1 -0
- package/dist/ipns/ipns-record.d.ts +46 -7
- package/dist/ipns/ipns-record.d.ts.map +1 -1
- package/dist/ipns/ipns-w3name.d.ts +45 -0
- package/dist/ipns/ipns-w3name.d.ts.map +1 -0
- package/dist/ipns/ipns-watcher.d.ts +190 -0
- package/dist/ipns/ipns-watcher.d.ts.map +1 -0
- package/dist/ipns.d.ts +3 -0
- package/dist/ipns.d.ts.map +1 -1
- package/dist/ipns.js +591 -14
- package/dist/ipns.js.map +1 -1
- package/dist/pubsub/snap-push.d.ts +2 -2
- package/dist/pubsub/snap-push.d.ts.map +1 -1
- package/dist/pubsub.js +4 -4
- package/dist/query/epoch-snapshot.d.ts +205 -0
- package/dist/query/epoch-snapshot.d.ts.map +1 -0
- package/dist/query.d.ts +1 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +12 -4
- package/dist/retrieve.js +4 -4
- package/dist/thread.js +1 -1
- package/dist/viewmodel/adapters/arktype.d.ts +33 -0
- package/dist/viewmodel/adapters/arktype.d.ts.map +1 -0
- package/dist/viewmodel/adapters/arktype.js +7 -0
- package/dist/viewmodel/adapters/arktype.js.map +1 -0
- package/dist/viewmodel/adapters/typebox.d.ts +35 -0
- package/dist/viewmodel/adapters/typebox.d.ts.map +1 -0
- package/dist/viewmodel/adapters/typebox.js +7 -0
- package/dist/viewmodel/adapters/typebox.js.map +1 -0
- package/dist/viewmodel/adapters/typia.d.ts +40 -0
- package/dist/viewmodel/adapters/typia.d.ts.map +1 -0
- package/dist/viewmodel/adapters/typia.js +7 -0
- package/dist/viewmodel/adapters/typia.js.map +1 -0
- package/dist/viewmodel/adapters/zod.d.ts +30 -0
- package/dist/viewmodel/adapters/zod.d.ts.map +1 -0
- package/dist/viewmodel/adapters/zod.js +7 -0
- package/dist/viewmodel/adapters/zod.js.map +1 -0
- package/dist/viewmodel/builder.d.ts +40 -0
- package/dist/viewmodel/builder.d.ts.map +1 -0
- package/dist/viewmodel/examples/all-adapters.d.ts +26 -0
- package/dist/viewmodel/examples/all-adapters.d.ts.map +1 -0
- package/dist/viewmodel/factory.d.ts +38 -0
- package/dist/viewmodel/factory.d.ts.map +1 -0
- package/dist/viewmodel/index.d.ts +10 -0
- package/dist/viewmodel/index.d.ts.map +1 -0
- package/dist/viewmodel/index.js +313 -0
- package/dist/viewmodel/index.js.map +1 -0
- package/dist/viewmodel/schema-adapter.d.ts +16 -0
- package/dist/viewmodel/schema-adapter.d.ts.map +1 -0
- package/dist/viewmodel/types.d.ts +97 -0
- package/dist/viewmodel/types.d.ts.map +1 -0
- package/package.json +29 -3
- package/src/applog/applog-utils.test.ts +43 -1
- package/src/applog/applog-utils.ts +38 -21
- package/src/applog/datom-types.ts +2 -2
- package/src/applog/object-values.test.ts +9 -9
- package/src/ipfs/car.ts +14 -2
- package/src/ipns/gateway-resolver.ts +63 -0
- package/src/ipns/ipns-record.ts +158 -29
- package/src/ipns/ipns-w3name.ts +207 -0
- package/src/ipns/ipns-watcher.ts +608 -0
- package/src/ipns.ts +3 -0
- package/src/pubsub/snap-push.ts +6 -5
- package/src/query/epoch-snapshot.test.ts +594 -0
- package/src/query/epoch-snapshot.ts +392 -0
- package/src/query.ts +1 -0
- package/src/viewmodel/adapters/arktype.ts +44 -0
- package/src/viewmodel/adapters/typebox.ts +59 -0
- package/src/viewmodel/adapters/typia.ts +50 -0
- package/src/viewmodel/adapters/zod.ts +55 -0
- package/src/viewmodel/builder.ts +71 -0
- package/src/viewmodel/examples/all-adapters.ts +206 -0
- package/src/viewmodel/factory.ts +330 -0
- package/src/viewmodel/index.ts +22 -0
- package/src/viewmodel/schema-adapter.ts +27 -0
- package/src/viewmodel/types.ts +152 -0
- package/dist/chunk-22WDFLXO.js +0 -138
- package/dist/chunk-22WDFLXO.js.map +0 -1
- package/dist/chunk-6ALNRM3J.js.map +0 -1
- package/dist/chunk-SHUHRHOT.js.map +0 -1
- /package/dist/{chunk-HUIQ54TT.js.map → chunk-6CSJTSQP.js.map} +0 -0
- /package/dist/{chunk-BLF5MAWU.js.map → chunk-BIYQEX3N.js.map} +0 -0
- /package/dist/{chunk-OC6Z6CQW.js.map → chunk-H4YVJKB7.js.map} +0 -0
package/dist/ipns.js
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
|
+
// src/ipns/gateway-resolver.ts
|
|
2
|
+
import { CID } from "multiformats/cid";
|
|
3
|
+
import { Logger } from "besonders-logger";
|
|
4
|
+
var { WARN, LOG, DEBUG } = Logger.setup(Logger.INFO);
|
|
5
|
+
async function resolveIPNSViaGateway(ipns, gateways) {
|
|
6
|
+
if (!ipns.startsWith("k51")) return null;
|
|
7
|
+
if (!gateways?.length) return null;
|
|
8
|
+
for (const rawGateway of gateways) {
|
|
9
|
+
const gateway = rawGateway.replace(/\/+$/, "");
|
|
10
|
+
const url = `${gateway}/ipns/${ipns}/`;
|
|
11
|
+
try {
|
|
12
|
+
DEBUG(`[resolveIPNSViaGateway] HEAD ${url}`);
|
|
13
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
DEBUG(`[resolveIPNSViaGateway] ${gateway} returned ${response.status}`);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const roots = response.headers.get("x-ipfs-roots") ?? response.headers.get("X-Ipfs-Roots");
|
|
19
|
+
if (roots) {
|
|
20
|
+
const first = roots.split(/[\s,]+/)[0]?.trim();
|
|
21
|
+
if (first) {
|
|
22
|
+
const cid = CID.parse(first);
|
|
23
|
+
DEBUG(`[resolveIPNSViaGateway] resolved via ${gateway} x-ipfs-roots:`, cid.toString());
|
|
24
|
+
return cid;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const etag = response.headers.get("etag");
|
|
28
|
+
if (etag) {
|
|
29
|
+
const m = etag.match(/^"?([a-z0-9]+)/i);
|
|
30
|
+
if (m) {
|
|
31
|
+
const cid = CID.parse(m[1]);
|
|
32
|
+
DEBUG(`[resolveIPNSViaGateway] resolved via ${gateway} etag:`, cid.toString());
|
|
33
|
+
return cid;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
WARN(`[resolveIPNSViaGateway] ${gateway} returned 200 but no usable CID header`);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
WARN(`[resolveIPNSViaGateway] ${gateway} failed:`, err);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
1
44
|
// src/ipns/ipns-record.ts
|
|
2
45
|
import { createIPNSRecord, marshalIPNSRecord, unmarshalIPNSRecord } from "ipns";
|
|
3
46
|
import { privateKeyFromRaw } from "@libp2p/crypto/keys";
|
|
@@ -36,29 +79,563 @@ async function resolveIPNSSequence(nameServiceUrl, ipnsName) {
|
|
|
36
79
|
}
|
|
37
80
|
return 0n;
|
|
38
81
|
}
|
|
39
|
-
|
|
82
|
+
var SEQ_CONFLICT_RE = /existing IPNS record has sequence (\d+) >= new record sequence (\d+)/;
|
|
83
|
+
async function publishIPNSRecord(privateKey, cid, targets, options) {
|
|
40
84
|
const ipnsName = ipnsNameFromPrivateKey(privateKey);
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
85
|
+
const maxBumps = options?.maxSequenceBumps ?? 3;
|
|
86
|
+
const publishTargets = targets.filter((t) => typeof t.publish === "function");
|
|
87
|
+
if (publishTargets.length === 0) {
|
|
88
|
+
throw new Error("No publish-capable targets supplied to publishIPNSRecord");
|
|
89
|
+
}
|
|
90
|
+
let sequence;
|
|
91
|
+
let bumpFloor;
|
|
92
|
+
for (let attempt = 0; ; attempt++) {
|
|
93
|
+
if (sequence === void 0) {
|
|
94
|
+
sequence = await pickNextSequence(ipnsName, targets);
|
|
95
|
+
}
|
|
96
|
+
if (bumpFloor !== void 0) {
|
|
97
|
+
if (bumpFloor > sequence) sequence = bumpFloor;
|
|
98
|
+
bumpFloor = void 0;
|
|
99
|
+
}
|
|
100
|
+
const signed = await createSignedIPNSRecord(privateKey, cid, sequence);
|
|
101
|
+
const results = await Promise.allSettled(
|
|
102
|
+
publishTargets.map((t) => t.publish(ipnsName, signed.recordBytes))
|
|
103
|
+
);
|
|
104
|
+
const failures = results.map((r, i) => ({ r, name: publishTargets[i].name })).filter(({ r }) => r.status === "rejected");
|
|
105
|
+
if (failures.length === 0) {
|
|
106
|
+
return signed;
|
|
107
|
+
}
|
|
108
|
+
const conflictReasons = failures.filter(
|
|
109
|
+
({ r }) => SEQ_CONFLICT_RE.test(typeof r.reason === "string" ? r.reason : String(r.reason))
|
|
110
|
+
);
|
|
111
|
+
if (conflictReasons.length > 0 && attempt < maxBumps) {
|
|
112
|
+
let bump = sequence;
|
|
113
|
+
for (const { r } of conflictReasons) {
|
|
114
|
+
const msg = typeof r.reason === "string" ? r.reason : String(r.reason);
|
|
115
|
+
const m = msg.match(SEQ_CONFLICT_RE);
|
|
116
|
+
if (m) {
|
|
117
|
+
const existing = BigInt(m[1]);
|
|
118
|
+
if (existing >= bump) bump = existing + 1n;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (bump > sequence) {
|
|
122
|
+
console.warn(
|
|
123
|
+
`[publishIPNSRecord] Sequence conflict: server seq ${bump - 1n} >= current ${sequence}, bumping to ${bump} (attempt ${attempt + 1}/${maxBumps})`
|
|
124
|
+
);
|
|
125
|
+
sequence = bump;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (failures.length < publishTargets.length) {
|
|
130
|
+
for (const { r, name } of failures) {
|
|
131
|
+
console.warn(`[publishIPNSRecord] target '${name}' failed:`, r.reason);
|
|
132
|
+
}
|
|
133
|
+
return signed;
|
|
134
|
+
}
|
|
135
|
+
if (conflictReasons.length > 0) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`IPNS sequence conflict not resolved after ${Math.min(attempt + 1, maxBumps)} bumps. All publish targets failed: ${failures.map(({ r, name }) => `${name}: ${r.reason}`).join("; ")}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
throw new Error(
|
|
141
|
+
`All IPNS publish targets failed: ${failures.map(({ r, name }) => `${name}: ${r.reason}`).join("; ")}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function pickNextSequence(ipnsName, targets) {
|
|
146
|
+
const capable = targets.filter((t) => typeof t.resolveSequence === "function");
|
|
147
|
+
if (capable.length === 0) {
|
|
148
|
+
return 0n;
|
|
149
|
+
}
|
|
150
|
+
for (const target of capable) {
|
|
151
|
+
try {
|
|
152
|
+
const current = await target.resolveSequence(ipnsName);
|
|
153
|
+
return current == null ? 0n : current + 1n;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.warn(`[publishIPNSRecord] target '${target.name}' sequence resolve failed:`, err);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
console.warn(`[publishIPNSRecord] no target could resolve sequence for ${ipnsName} \u2014 starting at 0n`);
|
|
159
|
+
return 0n;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/ipns/ipns-w3name.ts
|
|
163
|
+
import { Logger as Logger2 } from "besonders-logger";
|
|
164
|
+
import { base64pad as base64pad2 } from "multiformats/bases/base64";
|
|
165
|
+
import * as W3Name from "w3name";
|
|
166
|
+
var { WARN: WARN2, LOG: LOG2, DEBUG: DEBUG2, ERROR } = Logger2.setup(Logger2.INFO);
|
|
167
|
+
async function tryResolveIPNS(ipns) {
|
|
168
|
+
const url = `https://name.web3.storage/name/${ipns.toString()}`;
|
|
169
|
+
let response;
|
|
170
|
+
try {
|
|
171
|
+
response = await fetch(url, { signal: AbortSignal.timeout(3e4) });
|
|
172
|
+
} catch (err) {
|
|
173
|
+
throw ERROR("[w3name] Network error resolving IPNS:", err);
|
|
174
|
+
}
|
|
175
|
+
if (response.status === 404) {
|
|
176
|
+
DEBUG2("[w3name] IPNS record not found (never published):", ipns.toString());
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
throw ERROR(`[w3name] HTTP ${response.status} resolving IPNS:`, response.statusText);
|
|
181
|
+
}
|
|
182
|
+
const existing = await W3Name.resolve(ipns);
|
|
183
|
+
return existing;
|
|
184
|
+
}
|
|
185
|
+
async function publishIPNS(ipnsPrivateKey, cid) {
|
|
186
|
+
const TIMEOUT_MS = 3e4;
|
|
187
|
+
const timeout = new Promise(
|
|
188
|
+
(_, reject) => setTimeout(() => reject(new Error(`publishIPNS timed out after ${TIMEOUT_MS}ms`)), TIMEOUT_MS)
|
|
46
189
|
);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
190
|
+
return Promise.race([_publishIPNSImpl(ipnsPrivateKey, cid), timeout]);
|
|
191
|
+
}
|
|
192
|
+
async function _publishIPNSImpl(ipnsPrivateKey, cid) {
|
|
193
|
+
const value = `/ipfs/${cid}`;
|
|
194
|
+
const ipns = await W3Name.from(ipnsPrivateKey);
|
|
195
|
+
let revision;
|
|
196
|
+
const existing = await tryResolveIPNS(ipns);
|
|
197
|
+
if (existing) {
|
|
198
|
+
revision = await W3Name.increment(existing, value);
|
|
199
|
+
DEBUG2("[w3name] incrementing revision for", ipns.toString());
|
|
200
|
+
} else {
|
|
201
|
+
revision = await W3Name.v0(ipns, value);
|
|
202
|
+
DEBUG2("[w3name] creating initial revision for", ipns.toString());
|
|
203
|
+
}
|
|
204
|
+
await W3Name.publish(revision, ipns.key);
|
|
205
|
+
DEBUG2("[w3name] published", cid.toString(), "to", ipns.toString());
|
|
206
|
+
return revision;
|
|
207
|
+
}
|
|
208
|
+
async function resolveZtaxSequence(baseUrl, ipnsName) {
|
|
209
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/${ipnsName}`;
|
|
210
|
+
let response;
|
|
211
|
+
try {
|
|
212
|
+
response = await fetch(url);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
throw new Error(`Network error resolving sequence from ${baseUrl}: ${err}`);
|
|
215
|
+
}
|
|
216
|
+
if (response.status === 404) return null;
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
throw new Error(`HTTP ${response.status} resolving sequence from ${baseUrl}: ${response.statusText}`);
|
|
219
|
+
}
|
|
220
|
+
const body = await response.json();
|
|
221
|
+
if (typeof body.seq === "number") {
|
|
222
|
+
return BigInt(body.seq);
|
|
223
|
+
}
|
|
224
|
+
if (typeof body.record === "string") {
|
|
225
|
+
try {
|
|
226
|
+
const bytes = base64pad2.baseDecode(body.record);
|
|
227
|
+
const entry = unmarshalIPNSRecord(bytes);
|
|
228
|
+
return entry.sequence;
|
|
229
|
+
} catch {
|
|
230
|
+
WARN2("[resolveZtaxSequence] failed to unmarshal record for", ipnsName);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
function ztaxTarget(baseUrl = "https://ipfs.zt.ax") {
|
|
237
|
+
const cleanBase = baseUrl.replace(/\/+$/, "");
|
|
238
|
+
return {
|
|
239
|
+
name: "zt.ax",
|
|
240
|
+
async publish(ipnsName, recordBytes) {
|
|
241
|
+
const url = `${cleanBase}/${ipnsName}`;
|
|
242
|
+
DEBUG2(`[ztaxTarget] POST ${url}`);
|
|
243
|
+
const res = await fetch(url, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
body: base64pad2.baseEncode(recordBytes)
|
|
246
|
+
});
|
|
247
|
+
if (!res.ok) {
|
|
248
|
+
let detail = "";
|
|
249
|
+
try {
|
|
250
|
+
const body = await res.json();
|
|
251
|
+
detail = body.message ? ` \u2014 ${body.message}` : "";
|
|
252
|
+
} catch {
|
|
253
|
+
detail = res.statusText ? ` \u2014 ${res.statusText}` : "";
|
|
254
|
+
}
|
|
255
|
+
throw new Error(`zt.ax HTTP ${res.status}${detail}`);
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
async resolveSequence(ipnsName) {
|
|
259
|
+
return resolveZtaxSequence(cleanBase, ipnsName);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function w3nameTarget(serviceUrl = "https://name.web3.storage") {
|
|
264
|
+
return {
|
|
265
|
+
name: "w3name",
|
|
266
|
+
// No publish() — w3name is read-only; publishing goes through ztaxTarget
|
|
267
|
+
async resolveSequence(ipnsName) {
|
|
268
|
+
return resolveIPNSSequence(serviceUrl, ipnsName);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async function generateIpnsKey() {
|
|
273
|
+
return W3Name.create();
|
|
274
|
+
}
|
|
275
|
+
async function getW3NamePublic(pk) {
|
|
276
|
+
const ipns = await W3Name.from(pk);
|
|
277
|
+
return ipns.toString();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/ipns/ipns-watcher.ts
|
|
281
|
+
import { Logger as Logger3 } from "besonders-logger";
|
|
282
|
+
import { CID as CID2 } from "multiformats/cid";
|
|
283
|
+
import ReconnectingWebSocket from "partysocket/ws";
|
|
284
|
+
var { WARN: WARN3, LOG: LOG3, DEBUG: DEBUG3, ERROR: ERROR2 } = Logger3.setup(Logger3.INFO);
|
|
285
|
+
function buildWsUrl(nameBaseUrl, name) {
|
|
286
|
+
const base = nameBaseUrl.replace(/\/+$/, "").replace(/^http/, "ws");
|
|
287
|
+
return `${base}/${name}/watch`;
|
|
288
|
+
}
|
|
289
|
+
function buildHttpUrl(nameBaseUrl, name) {
|
|
290
|
+
const base = nameBaseUrl.replace(/\/+$/, "");
|
|
291
|
+
return `${base}/${name}`;
|
|
292
|
+
}
|
|
293
|
+
function requireNameBaseUrl(nameBaseUrl, fn) {
|
|
294
|
+
if (!nameBaseUrl) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`[${fn}] nameBaseUrl is required. The legacy default https://name.web3.storage is shut down. Pass the base URL of a naming service that supports WebSocket subscriptions to /name/<ipns>/watch and HTTP GET on /name/<ipns>.`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return nameBaseUrl;
|
|
300
|
+
}
|
|
301
|
+
function parseCidFromIpnsValue(value) {
|
|
302
|
+
try {
|
|
303
|
+
const cidStr = value.startsWith("/ipfs/") ? value.slice(6) : value;
|
|
304
|
+
return CID2.parse(cidStr);
|
|
305
|
+
} catch {
|
|
306
|
+
DEBUG3("[parseCidFromIpnsValue] failed to parse:", value);
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function watchNameRaw(nameBaseUrl, name, options) {
|
|
311
|
+
const resolvedBase = requireNameBaseUrl(nameBaseUrl, "watchNameRaw");
|
|
312
|
+
const url = buildWsUrl(resolvedBase, name);
|
|
313
|
+
DEBUG3("[watchNameRaw] connecting to", url);
|
|
314
|
+
const ws = new WebSocket(url);
|
|
315
|
+
ws.onopen = () => {
|
|
316
|
+
LOG3("[watchNameRaw] connected to", name);
|
|
317
|
+
options.onOpen?.();
|
|
318
|
+
};
|
|
319
|
+
ws.onmessage = (event) => {
|
|
320
|
+
try {
|
|
321
|
+
const record = JSON.parse(event.data);
|
|
322
|
+
DEBUG3("[watchNameRaw] received update for", name, record);
|
|
323
|
+
options.onUpdate(record);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
WARN3("[watchNameRaw] failed to parse message:", event.data, err);
|
|
326
|
+
options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
ws.onerror = (event) => {
|
|
330
|
+
const errorMsg = event instanceof ErrorEvent ? event.message : "WebSocket error";
|
|
331
|
+
WARN3("[watchNameRaw] error for", name, ":", errorMsg);
|
|
332
|
+
options.onError?.(new Error(errorMsg));
|
|
333
|
+
};
|
|
334
|
+
ws.onclose = (event) => {
|
|
335
|
+
DEBUG3("[watchNameRaw] closed for", name, "code:", event.code);
|
|
336
|
+
options.onClose?.(event);
|
|
337
|
+
};
|
|
338
|
+
return {
|
|
339
|
+
close: () => {
|
|
340
|
+
DEBUG3("[watchNameRaw] closing connection for", name);
|
|
341
|
+
ws.close();
|
|
342
|
+
},
|
|
343
|
+
ws
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
var DEFAULT_LIVENESS_INTERVAL = 36e5;
|
|
347
|
+
var IpnsWatcher = class {
|
|
348
|
+
name;
|
|
349
|
+
nameBaseUrl;
|
|
350
|
+
ws;
|
|
351
|
+
lastKnownValue = null;
|
|
352
|
+
options;
|
|
353
|
+
isFirstConnect = true;
|
|
354
|
+
livenessTimer = null;
|
|
355
|
+
connectedAt = null;
|
|
356
|
+
lastMessageAt = null;
|
|
357
|
+
constructor(nameBaseUrl, name, options) {
|
|
358
|
+
this.nameBaseUrl = requireNameBaseUrl(nameBaseUrl, "IpnsWatcher");
|
|
359
|
+
this.name = name;
|
|
360
|
+
this.options = options;
|
|
361
|
+
const url = buildWsUrl(this.nameBaseUrl, name);
|
|
362
|
+
DEBUG3("[IpnsWatcher] creating for", name);
|
|
363
|
+
this.ws = new ReconnectingWebSocket(url, [], {
|
|
364
|
+
maxReconnectionDelay: 9e5,
|
|
365
|
+
// 15min
|
|
366
|
+
minReconnectionDelay: 5e3,
|
|
367
|
+
reconnectionDelayGrowFactor: 2,
|
|
368
|
+
maxRetries: Infinity,
|
|
369
|
+
...options.wsOptions
|
|
370
|
+
});
|
|
371
|
+
this.ws.onopen = () => {
|
|
372
|
+
LOG3("[IpnsWatcher] connected to", name);
|
|
373
|
+
this.connectedAt = /* @__PURE__ */ new Date();
|
|
374
|
+
options.onConnected?.();
|
|
375
|
+
if (this.isFirstConnect && (options.fetchInitialState ?? false)) {
|
|
376
|
+
this.checkForMissedUpdates();
|
|
377
|
+
} else if (!this.isFirstConnect && (options.catchUpOnReconnect ?? true)) {
|
|
378
|
+
this.checkForMissedUpdates();
|
|
379
|
+
}
|
|
380
|
+
this.isFirstConnect = false;
|
|
381
|
+
if (options.livenessCheck !== false) {
|
|
382
|
+
this.startLivenessCheck();
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
this.ws.onmessage = (event) => {
|
|
386
|
+
this.lastMessageAt = /* @__PURE__ */ new Date();
|
|
387
|
+
try {
|
|
388
|
+
const record = JSON.parse(event.data);
|
|
389
|
+
DEBUG3("[IpnsWatcher] received update for", name, record);
|
|
390
|
+
const lastValue = this.lastKnownValue;
|
|
391
|
+
const isNew = record.value !== lastValue;
|
|
392
|
+
const cid = parseCidFromIpnsValue(record.value);
|
|
393
|
+
if (!isNew && !options.includeUnchanged) {
|
|
394
|
+
DEBUG3("[IpnsWatcher] skipping unchanged value for", name);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
this.lastKnownValue = record.value;
|
|
398
|
+
const update = {
|
|
399
|
+
value: record.value,
|
|
400
|
+
cid,
|
|
401
|
+
lastValue,
|
|
402
|
+
isNew,
|
|
403
|
+
record
|
|
404
|
+
};
|
|
405
|
+
void options.onUpdate(update);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
WARN3("[IpnsWatcher] failed to parse message:", event.data, err);
|
|
408
|
+
options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
this.ws.onerror = (event) => {
|
|
412
|
+
const errorMsg = event instanceof ErrorEvent ? event.message : "WebSocket error";
|
|
413
|
+
if (errorMsg === "Unexpected EOF") {
|
|
414
|
+
LOG3("[IpnsWatcher] error for", name, ":", errorMsg, "(auto-reconnect enabled)");
|
|
415
|
+
} else {
|
|
416
|
+
WARN3("[IpnsWatcher] error for", name, ":", errorMsg);
|
|
417
|
+
}
|
|
418
|
+
if (errorMsg !== "Unexpected EOF") {
|
|
419
|
+
options.onError?.(new Error(errorMsg));
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
this.ws.onclose = () => {
|
|
423
|
+
DEBUG3("[IpnsWatcher] disconnected from", name);
|
|
424
|
+
this.stopLivenessCheck();
|
|
425
|
+
this.connectedAt = null;
|
|
426
|
+
this.lastMessageAt = null;
|
|
427
|
+
options.onDisconnected?.();
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Resolve current IPNS value via HTTP API to catch missed updates
|
|
432
|
+
*/
|
|
433
|
+
async checkForMissedUpdates() {
|
|
434
|
+
try {
|
|
435
|
+
DEBUG3("[IpnsWatcher] checking for missed updates for", this.name);
|
|
436
|
+
const response = await fetch(buildHttpUrl(this.nameBaseUrl, this.name));
|
|
437
|
+
if (!response.ok) {
|
|
438
|
+
if (response.status === 404) {
|
|
439
|
+
DEBUG3("[IpnsWatcher] IPNS not yet published:", this.name);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
443
|
+
}
|
|
444
|
+
const record = await response.json();
|
|
445
|
+
const lastValue = this.lastKnownValue;
|
|
446
|
+
const isNew = record.value !== lastValue;
|
|
447
|
+
const cid = parseCidFromIpnsValue(record.value);
|
|
448
|
+
if (!isNew && !this.options.includeUnchanged) {
|
|
449
|
+
DEBUG3("[IpnsWatcher] no new updates for", this.name);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const logMsg = lastValue === null ? "[IpnsWatcher] fetched initial state for" : "[IpnsWatcher] caught missed update for";
|
|
453
|
+
LOG3(logMsg, this.name, {
|
|
454
|
+
previous: lastValue,
|
|
455
|
+
current: record.value
|
|
456
|
+
});
|
|
457
|
+
this.lastKnownValue = record.value;
|
|
458
|
+
const update = {
|
|
459
|
+
value: record.value,
|
|
460
|
+
cid,
|
|
461
|
+
lastValue,
|
|
462
|
+
isNew,
|
|
463
|
+
record
|
|
464
|
+
};
|
|
465
|
+
void this.options.onUpdate(update);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
WARN3("[IpnsWatcher] failed to check for missed updates:", this.name, err);
|
|
468
|
+
this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Start periodic liveness checks to detect zombie connections.
|
|
473
|
+
*/
|
|
474
|
+
startLivenessCheck() {
|
|
475
|
+
this.stopLivenessCheck();
|
|
476
|
+
const interval = this.options.livenessCheckInterval ?? DEFAULT_LIVENESS_INTERVAL;
|
|
477
|
+
DEBUG3("[IpnsWatcher] starting liveness check for", this.name, "interval:", interval);
|
|
478
|
+
this.livenessTimer = setInterval(() => {
|
|
479
|
+
void this.performLivenessCheck();
|
|
480
|
+
}, interval);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Stop periodic liveness checks.
|
|
484
|
+
*/
|
|
485
|
+
stopLivenessCheck() {
|
|
486
|
+
if (this.livenessTimer !== null) {
|
|
487
|
+
DEBUG3("[IpnsWatcher] stopping liveness check for", this.name);
|
|
488
|
+
clearInterval(this.livenessTimer);
|
|
489
|
+
this.livenessTimer = null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Perform a single liveness check via HTTP.
|
|
494
|
+
* If the HTTP value differs from lastKnownValue, the connection is stale.
|
|
495
|
+
*/
|
|
496
|
+
async performLivenessCheck() {
|
|
497
|
+
try {
|
|
498
|
+
DEBUG3("[IpnsWatcher] performing liveness check for", this.name);
|
|
499
|
+
const response = await fetch(buildHttpUrl(this.nameBaseUrl, this.name));
|
|
500
|
+
if (!response.ok) {
|
|
501
|
+
if (response.status === 404) {
|
|
502
|
+
if (this.lastKnownValue === null) {
|
|
503
|
+
DEBUG3("[IpnsWatcher] liveness check OK (both null) for", this.name);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
WARN3("[IpnsWatcher] liveness check inconsistent (we have value, HTTP 404) for", this.name);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
510
|
+
}
|
|
511
|
+
const record = await response.json();
|
|
512
|
+
if (record.value === this.lastKnownValue) {
|
|
513
|
+
DEBUG3("[IpnsWatcher] liveness check OK for", this.name);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const now = /* @__PURE__ */ new Date();
|
|
517
|
+
const silenceDuration = this.lastMessageAt ? now.getTime() - this.lastMessageAt.getTime() : this.connectedAt ? now.getTime() - this.connectedAt.getTime() : 0;
|
|
518
|
+
const staleInfo = {
|
|
519
|
+
connectedAt: this.connectedAt ?? now,
|
|
520
|
+
lastMessageAt: this.lastMessageAt,
|
|
521
|
+
silenceDuration,
|
|
522
|
+
staleValue: this.lastKnownValue,
|
|
523
|
+
currentValue: record.value
|
|
524
|
+
};
|
|
525
|
+
WARN3("[IpnsWatcher] stale connection detected for", this.name, {
|
|
526
|
+
connectedAt: staleInfo.connectedAt.toISOString(),
|
|
527
|
+
lastMessageAt: staleInfo.lastMessageAt?.toISOString() ?? "never",
|
|
528
|
+
silenceDuration: `${Math.round(silenceDuration / 1e3)}s`,
|
|
529
|
+
staleValue: staleInfo.staleValue,
|
|
530
|
+
currentValue: staleInfo.currentValue
|
|
531
|
+
});
|
|
532
|
+
this.options.onStaleConnection?.(staleInfo);
|
|
533
|
+
const lastValue = this.lastKnownValue;
|
|
534
|
+
const cid = parseCidFromIpnsValue(record.value);
|
|
535
|
+
this.lastKnownValue = record.value;
|
|
536
|
+
const update = {
|
|
537
|
+
value: record.value,
|
|
538
|
+
cid,
|
|
539
|
+
lastValue,
|
|
540
|
+
isNew: true,
|
|
541
|
+
record
|
|
542
|
+
};
|
|
543
|
+
void this.options.onUpdate(update);
|
|
544
|
+
LOG3("[IpnsWatcher] forcing reconnect due to stale connection for", this.name);
|
|
545
|
+
this.ws.reconnect();
|
|
546
|
+
} catch (err) {
|
|
547
|
+
WARN3("[IpnsWatcher] liveness check failed for", this.name, err);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Manually start/reconnect the WebSocket.
|
|
552
|
+
* Only needed if you used `wsOptions: { startClosed: true }`.
|
|
553
|
+
*/
|
|
554
|
+
start() {
|
|
555
|
+
LOG3("[IpnsWatcher] starting watcher for", this.name);
|
|
556
|
+
this.ws.reconnect();
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Alias for close() - for backward compatibility
|
|
560
|
+
*/
|
|
561
|
+
stop() {
|
|
562
|
+
this.close();
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Close the WebSocket connection and stop watching
|
|
566
|
+
*/
|
|
567
|
+
close() {
|
|
568
|
+
LOG3("[IpnsWatcher] closing watcher for", this.name);
|
|
569
|
+
this.stopLivenessCheck();
|
|
570
|
+
this.ws.close();
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Get the last known IPNS value
|
|
574
|
+
*/
|
|
575
|
+
get lastValue() {
|
|
576
|
+
return this.lastKnownValue;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Get the WebSocket ready state
|
|
580
|
+
*/
|
|
581
|
+
get readyState() {
|
|
582
|
+
return this.ws.readyState;
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
function watchName(nameBaseUrl, name, options) {
|
|
586
|
+
return new IpnsWatcher(nameBaseUrl, name, options);
|
|
587
|
+
}
|
|
588
|
+
async function* watchNameIterator(nameBaseUrl, name, signal) {
|
|
589
|
+
const queue = [];
|
|
590
|
+
let resolve2 = null;
|
|
591
|
+
let error = null;
|
|
592
|
+
const watcher = new IpnsWatcher(nameBaseUrl, name, {
|
|
593
|
+
onUpdate: (update) => {
|
|
594
|
+
queue.push(update);
|
|
595
|
+
resolve2?.();
|
|
596
|
+
},
|
|
597
|
+
onError: (err) => {
|
|
598
|
+
error = err instanceof Error ? err : new Error("WebSocket error");
|
|
599
|
+
resolve2?.();
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
signal?.addEventListener("abort", () => {
|
|
603
|
+
watcher.close();
|
|
604
|
+
});
|
|
605
|
+
try {
|
|
606
|
+
while (!signal?.aborted) {
|
|
607
|
+
if (queue.length > 0) {
|
|
608
|
+
yield queue.shift();
|
|
609
|
+
} else if (error) {
|
|
610
|
+
WARN3("[watchNameIterator] error occurred, continuing:", error);
|
|
611
|
+
error = null;
|
|
612
|
+
} else {
|
|
613
|
+
await new Promise((r) => {
|
|
614
|
+
resolve2 = r;
|
|
615
|
+
});
|
|
616
|
+
resolve2 = null;
|
|
617
|
+
}
|
|
51
618
|
}
|
|
52
|
-
}
|
|
53
|
-
|
|
619
|
+
} finally {
|
|
620
|
+
watcher.close();
|
|
54
621
|
}
|
|
55
|
-
return signed;
|
|
56
622
|
}
|
|
57
623
|
export {
|
|
624
|
+
IpnsWatcher,
|
|
58
625
|
createSignedIPNSRecord,
|
|
626
|
+
generateIpnsKey,
|
|
627
|
+
getW3NamePublic,
|
|
59
628
|
ipnsNameFromPrivateKey,
|
|
629
|
+
publishIPNS,
|
|
60
630
|
publishIPNSRecord,
|
|
61
631
|
resolveIPNSSequence,
|
|
62
|
-
|
|
632
|
+
resolveIPNSViaGateway,
|
|
633
|
+
resolveZtaxSequence,
|
|
634
|
+
unmarshalIPNSRecord,
|
|
635
|
+
w3nameTarget,
|
|
636
|
+
watchName,
|
|
637
|
+
watchNameIterator,
|
|
638
|
+
watchNameRaw,
|
|
639
|
+
ztaxTarget
|
|
63
640
|
};
|
|
64
641
|
//# sourceMappingURL=ipns.js.map
|