@wovin/daemon-utils 0.0.18 → 0.0.20

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.
@@ -0,0 +1,174 @@
1
+ import {
2
+ g
3
+ } from "./chunk-MZUPFLFH.min.js";
4
+
5
+ // src/watcher.ts
6
+ var { WARN, LOG, DEBUG, ERROR } = g.setup(g.INFO);
7
+ var NAME_WS_URL = "wss://name.web3.storage/name";
8
+ var NAME_HTTP_URL = "https://name.web3.storage/name";
9
+ function watchNameRaw(name, options) {
10
+ const url = `${NAME_WS_URL}/${name}/watch`;
11
+ DEBUG("[w3name-watch] connecting to", url);
12
+ const ws = new WebSocket(url);
13
+ ws.onopen = () => {
14
+ LOG("[w3name-watch] connected to", name);
15
+ options.onOpen?.();
16
+ };
17
+ ws.onmessage = (event) => {
18
+ try {
19
+ const record = JSON.parse(event.data);
20
+ DEBUG("[w3name-watch] received update for", name, record);
21
+ options.onUpdate(record);
22
+ } catch (err) {
23
+ WARN("[w3name-watch] failed to parse message:", event.data, err);
24
+ options.onError?.(err instanceof Error ? err : new Error(String(err)));
25
+ }
26
+ };
27
+ ws.onerror = (event) => {
28
+ WARN("[w3name-watch] error for", name, event);
29
+ options.onError?.(event);
30
+ };
31
+ ws.onclose = (event) => {
32
+ DEBUG("[w3name-watch] closed for", name, "code:", event.code, "reason:", event.reason);
33
+ options.onClose?.(event);
34
+ };
35
+ return {
36
+ close: () => {
37
+ DEBUG("[w3name-watch] closing connection for", name);
38
+ ws.close();
39
+ },
40
+ ws
41
+ };
42
+ }
43
+ var IpnsWatcher = class {
44
+ name;
45
+ options;
46
+ subscription = null;
47
+ backoffDelay;
48
+ lastKnownValue = null;
49
+ stopped = false;
50
+ reconnectTimeout = null;
51
+ constructor(name, options) {
52
+ this.name = name;
53
+ this.options = {
54
+ onUpdate: options.onUpdate,
55
+ onError: options.onError ?? (() => {
56
+ }),
57
+ onConnected: options.onConnected ?? (() => {
58
+ }),
59
+ onDisconnected: options.onDisconnected ?? (() => {
60
+ }),
61
+ initialBackoff: options.initialBackoff ?? 5e3,
62
+ maxBackoff: options.maxBackoff ?? 9e5
63
+ };
64
+ this.backoffDelay = this.options.initialBackoff;
65
+ }
66
+ start() {
67
+ if (this.stopped) {
68
+ WARN(`[IpnsWatcher] Cannot restart stopped watcher for ${this.name}`);
69
+ return;
70
+ }
71
+ this.connect();
72
+ }
73
+ stop() {
74
+ this.stopped = true;
75
+ if (this.reconnectTimeout) {
76
+ clearTimeout(this.reconnectTimeout);
77
+ this.reconnectTimeout = null;
78
+ }
79
+ if (this.subscription) {
80
+ this.subscription.close();
81
+ this.subscription = null;
82
+ }
83
+ LOG(`[IpnsWatcher] Stopped watching ${this.name}`);
84
+ }
85
+ connect() {
86
+ if (this.stopped) return;
87
+ this.subscription = watchNameRaw(this.name, {
88
+ onUpdate: async (record) => {
89
+ this.lastKnownValue = record.value;
90
+ this.backoffDelay = this.options.initialBackoff;
91
+ await this.options.onUpdate(record);
92
+ },
93
+ onError: (error) => {
94
+ this.options.onError(error);
95
+ },
96
+ onOpen: () => {
97
+ this.options.onConnected();
98
+ },
99
+ onClose: () => {
100
+ this.options.onDisconnected();
101
+ this.scheduleReconnect();
102
+ }
103
+ });
104
+ }
105
+ scheduleReconnect() {
106
+ if (this.stopped) return;
107
+ WARN(
108
+ `[IpnsWatcher] Scheduling reconnect for ${this.name} in ${this.backoffDelay}ms`
109
+ );
110
+ this.reconnectTimeout = setTimeout(() => {
111
+ if (this.stopped) return;
112
+ this.checkForMissedUpdates().then(() => {
113
+ this.connect();
114
+ this.backoffDelay = Math.min(this.backoffDelay * 2, this.options.maxBackoff);
115
+ }).catch((err) => {
116
+ WARN(`[IpnsWatcher] Error during catch-up for ${this.name}:`, err);
117
+ this.options.onError(err instanceof Error ? err : new Error(String(err)));
118
+ this.connect();
119
+ this.backoffDelay = Math.min(this.backoffDelay * 2, this.options.maxBackoff);
120
+ });
121
+ }, this.backoffDelay);
122
+ }
123
+ /**
124
+ * Resolve current IPNS value via HTTP API
125
+ */
126
+ async resolveCurrentValue() {
127
+ try {
128
+ const response = await fetch(`${NAME_HTTP_URL}/${this.name}`);
129
+ if (!response.ok) {
130
+ if (response.status === 404) {
131
+ DEBUG(`[IpnsWatcher] IPNS ${this.name} not yet published`);
132
+ return null;
133
+ }
134
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
135
+ }
136
+ const record = await response.json();
137
+ return record;
138
+ } catch (err) {
139
+ WARN(`[IpnsWatcher] Failed to resolve IPNS ${this.name}:`, err);
140
+ return null;
141
+ }
142
+ }
143
+ async checkForMissedUpdates() {
144
+ try {
145
+ const currentRecord = await this.resolveCurrentValue();
146
+ if (currentRecord && currentRecord.value !== this.lastKnownValue) {
147
+ DEBUG(
148
+ `[IpnsWatcher] Detected missed update for ${this.name}`,
149
+ "previous:",
150
+ this.lastKnownValue,
151
+ "current:",
152
+ currentRecord.value
153
+ );
154
+ this.lastKnownValue = currentRecord.value;
155
+ await this.options.onUpdate(currentRecord);
156
+ }
157
+ } catch (err) {
158
+ WARN(`[IpnsWatcher] Failed to check for missed updates for ${this.name}:`, err);
159
+ throw err;
160
+ }
161
+ }
162
+ };
163
+ function watchName(name, options) {
164
+ const watcher = new IpnsWatcher(name, options);
165
+ watcher.start();
166
+ return watcher;
167
+ }
168
+
169
+ export {
170
+ watchNameRaw,
171
+ IpnsWatcher,
172
+ watchName
173
+ };
174
+ //# sourceMappingURL=chunk-GB53WLYA.min.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/watcher.ts"],"sourcesContent":["/**\n * IPNS Thread Watcher\n * Watches an IPNS thread for new audio files via WebSocket\n */\n\nimport { Logger } from 'besonders-logger'\n\nconst { WARN, LOG, DEBUG, ERROR } = Logger.setup(Logger.INFO)\n\nconst NAME_WS_URL = 'wss://name.web3.storage/name'\nconst NAME_HTTP_URL = 'https://name.web3.storage/name'\n\nexport interface W3NameRecord {\n\tvalue: string // e.g. \"/ipfs/bafybeig...\"\n\tseq?: number\n\tvalidity?: string\n}\n\nexport interface WatchSubscription {\n\tclose: () => void\n\tws: WebSocket\n}\n\nexport interface WatchOptions {\n\tonUpdate: (record: W3NameRecord) => void\n\tonError?: (error: Event | Error) => void\n\tonOpen?: () => void\n\tonClose?: (event: CloseEvent) => void\n}\n\nexport interface IpnsWatcherOptions {\n\tonUpdate: (record: W3NameRecord) => void | Promise<void>\n\tonError?: (error: Error | Event) => void\n\tonConnected?: () => void\n\tonDisconnected?: () => void\n\t/** Initial backoff delay in ms (default: 5000) */\n\tinitialBackoff?: number\n\t/** Max backoff delay in ms (default: 900000 = 15min) */\n\tmaxBackoff?: number\n}\n\n/**\n * Low-level WebSocket watcher for IPNS (no reconnect logic)\n * @internal Use IpnsWatcher for robust watching with auto-reconnect\n */\nexport function watchNameRaw(name: string, options: WatchOptions): WatchSubscription {\n\tconst url = `${NAME_WS_URL}/${name}/watch`\n\tDEBUG('[w3name-watch] connecting to', url)\n\n\tconst ws = new WebSocket(url)\n\n\tws.onopen = () => {\n\t\tLOG('[w3name-watch] connected to', name)\n\t\toptions.onOpen?.()\n\t}\n\n\tws.onmessage = (event) => {\n\t\ttry {\n\t\t\tconst record: W3NameRecord = JSON.parse(event.data)\n\t\t\tDEBUG('[w3name-watch] received update for', name, record)\n\t\t\toptions.onUpdate(record)\n\t\t} catch (err) {\n\t\t\tWARN('[w3name-watch] failed to parse message:', event.data, err)\n\t\t\toptions.onError?.(err instanceof Error ? err : new Error(String(err)))\n\t\t}\n\t}\n\n\tws.onerror = (event) => {\n\t\tWARN('[w3name-watch] error for', name, event)\n\t\toptions.onError?.(event)\n\t}\n\n\tws.onclose = (event) => {\n\t\tDEBUG('[w3name-watch] closed for', name, 'code:', event.code, 'reason:', event.reason)\n\t\toptions.onClose?.(event)\n\t}\n\n\treturn {\n\t\tclose: () => {\n\t\t\tDEBUG('[w3name-watch] closing connection for', name)\n\t\t\tws.close()\n\t\t},\n\t\tws,\n\t}\n}\n\n/**\n * Robust IPNS watcher with auto-reconnect and catch-up logic\n */\nexport class IpnsWatcher {\n\tprivate name: string\n\tprivate options: Required<Omit<IpnsWatcherOptions, 'onError' | 'onConnected' | 'onDisconnected'>> & {\n\t\tonError: (error: Error | Event) => void\n\t\tonConnected: () => void\n\t\tonDisconnected: () => void\n\t}\n\tprivate subscription: WatchSubscription | null = null\n\tprivate backoffDelay: number\n\tprivate lastKnownValue: string | null = null\n\tprivate stopped: boolean = false\n\tprivate reconnectTimeout: NodeJS.Timeout | null = null\n\n\tconstructor(name: string, options: IpnsWatcherOptions) {\n\t\tthis.name = name\n\t\tthis.options = {\n\t\t\tonUpdate: options.onUpdate,\n\t\t\tonError: options.onError ?? (() => {}),\n\t\t\tonConnected: options.onConnected ?? (() => {}),\n\t\t\tonDisconnected: options.onDisconnected ?? (() => {}),\n\t\t\tinitialBackoff: options.initialBackoff ?? 5000,\n\t\t\tmaxBackoff: options.maxBackoff ?? 900000,\n\t\t}\n\t\tthis.backoffDelay = this.options.initialBackoff\n\t}\n\n\tstart(): void {\n\t\tif (this.stopped) {\n\t\t\tWARN(`[IpnsWatcher] Cannot restart stopped watcher for ${this.name}`)\n\t\t\treturn\n\t\t}\n\t\tthis.connect()\n\t}\n\n\tstop(): void {\n\t\tthis.stopped = true\n\t\tif (this.reconnectTimeout) {\n\t\t\tclearTimeout(this.reconnectTimeout)\n\t\t\tthis.reconnectTimeout = null\n\t\t}\n\t\tif (this.subscription) {\n\t\t\tthis.subscription.close()\n\t\t\tthis.subscription = null\n\t\t}\n\t\tLOG(`[IpnsWatcher] Stopped watching ${this.name}`)\n\t}\n\n\tprivate connect(): void {\n\t\tif (this.stopped) return\n\n\t\tthis.subscription = watchNameRaw(this.name, {\n\t\t\tonUpdate: async (record) => {\n\t\t\t\tthis.lastKnownValue = record.value\n\t\t\t\tthis.backoffDelay = this.options.initialBackoff\n\t\t\t\tawait this.options.onUpdate(record)\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\tthis.options.onError(error)\n\t\t\t},\n\t\t\tonOpen: () => {\n\t\t\t\tthis.options.onConnected()\n\t\t\t},\n\t\t\tonClose: () => {\n\t\t\t\tthis.options.onDisconnected()\n\t\t\t\tthis.scheduleReconnect()\n\t\t\t},\n\t\t})\n\t}\n\n\tprivate scheduleReconnect(): void {\n\t\tif (this.stopped) return\n\n\t\tWARN(\n\t\t\t`[IpnsWatcher] Scheduling reconnect for ${this.name} in ${this.backoffDelay}ms`,\n\t\t)\n\n\t\tthis.reconnectTimeout = setTimeout(() => {\n\t\t\tif (this.stopped) return\n\n\t\t\tthis.checkForMissedUpdates()\n\t\t\t\t.then(() => {\n\t\t\t\t\tthis.connect()\n\t\t\t\t\t// Exponential backoff: double delay up to max\n\t\t\t\t\tthis.backoffDelay = Math.min(this.backoffDelay * 2, this.options.maxBackoff)\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tWARN(`[IpnsWatcher] Error during catch-up for ${this.name}:`, err)\n\t\t\t\t\tthis.options.onError(err instanceof Error ? err : new Error(String(err)))\n\t\t\t\t\t// Still reconnect, but don't update lastKnownValue\n\t\t\t\t\tthis.connect()\n\t\t\t\t\tthis.backoffDelay = Math.min(this.backoffDelay * 2, this.options.maxBackoff)\n\t\t\t\t})\n\t\t}, this.backoffDelay)\n\t}\n\n\t/**\n\t * Resolve current IPNS value via HTTP API\n\t */\n\tprivate async resolveCurrentValue(): Promise<W3NameRecord | null> {\n\t\ttry {\n\t\t\tconst response = await fetch(`${NAME_HTTP_URL}/${this.name}`)\n\t\t\tif (!response.ok) {\n\t\t\t\tif (response.status === 404) {\n\t\t\t\t\tDEBUG(`[IpnsWatcher] IPNS ${this.name} not yet published`)\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tthrow new Error(`HTTP ${response.status}: ${response.statusText}`)\n\t\t\t}\n\t\t\tconst record: W3NameRecord = await response.json()\n\t\t\treturn record\n\t\t} catch (err) {\n\t\t\tWARN(`[IpnsWatcher] Failed to resolve IPNS ${this.name}:`, err)\n\t\t\treturn null\n\t\t}\n\t}\n\n\tprivate async checkForMissedUpdates(): Promise<void> {\n\t\ttry {\n\t\t\tconst currentRecord = await this.resolveCurrentValue()\n\t\t\tif (currentRecord && currentRecord.value !== this.lastKnownValue) {\n\t\t\t\tDEBUG(\n\t\t\t\t\t`[IpnsWatcher] Detected missed update for ${this.name}`,\n\t\t\t\t\t'previous:',\n\t\t\t\t\tthis.lastKnownValue,\n\t\t\t\t\t'current:',\n\t\t\t\t\tcurrentRecord.value,\n\t\t\t\t)\n\t\t\t\tthis.lastKnownValue = currentRecord.value\n\t\t\t\tawait this.options.onUpdate(currentRecord)\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tWARN(`[IpnsWatcher] Failed to check for missed updates for ${this.name}:`, err)\n\t\t\tthrow err\n\t\t}\n\t}\n}\n\n/**\n * Subscribe to IPNS name updates with auto-reconnect and catch-up logic\n */\nexport function watchName(name: string, options: IpnsWatcherOptions): IpnsWatcher {\n\tconst watcher = new IpnsWatcher(name, options)\n\twatcher.start()\n\treturn watcher\n}\n"],"mappings":"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","names":[]}
@@ -13,8 +13,9 @@ async function loadSecret(envVar, secretFile) {
13
13
  async function loadSecretOrThrow(envVar, secretFile, displayName) {
14
14
  const secret = await loadSecret(envVar, secretFile);
15
15
  if (!secret) {
16
+ const name = displayName || envVar.split("_").map((word) => word.charAt(0) + word.slice(1).toLowerCase()).join(" ");
16
17
  throw new Error(
17
- `${displayName} not found. Set ${envVar} environment variable or create ${secretFile} file`
18
+ `${name} not found. Set ${envVar} environment variable or create ${secretFile} file`
18
19
  );
19
20
  }
20
21
  return secret;
@@ -24,4 +25,4 @@ export {
24
25
  loadSecret,
25
26
  loadSecretOrThrow
26
27
  };
27
- //# sourceMappingURL=chunk-XAFSYOU2.min.js.map
28
+ //# sourceMappingURL=chunk-IUNRH65Q.min.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/secrets.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\n\n/**\n * Utility functions for loading API keys from environment variables or secret files\n */\n\n/**\n * Load an API key from environment variable or secret file\n * @param envVar - Environment variable name (e.g., 'REGOLO_API_KEY')\n * @param secretFile - Path to secret file (e.g., 'secret/regolo')\n * @returns The API key, or undefined if not found\n */\nexport async function loadSecret(envVar: string, secretFile: string): Promise<string | undefined> {\n\t// Try environment variable first\n\tconst envValue = process.env[envVar]\n\tif (envValue) return envValue\n\n\t// Fall back to secret file\n\ttry {\n\t\tconst fileContent = await readFile(secretFile, 'utf-8')\n\t\treturn fileContent.trim() || undefined\n\t} catch {\n\t\t// File doesn't exist or can't be read\n\t\treturn undefined\n\t}\n}\n\n/**\n * Load an API key from environment variable or secret file, with error handling\n * @param envVar - Environment variable name\n * @param secretFile - Path to secret file\n * @param displayName - Human-readable name for error messages. Defaults to capitalized envVar (e.g., 'MISTRAL_API_KEY' -> 'Mistral Api Key')\n * @returns The API key\n * @throws If the key is not found\n */\nexport async function loadSecretOrThrow(\n\tenvVar: string,\n\tsecretFile: string,\n\tdisplayName?: string,\n): Promise<string> {\n\tconst secret = await loadSecret(envVar, secretFile)\n\tif (!secret) {\n\t\tconst name = displayName || envVar.split('_').map(word => word.charAt(0) + word.slice(1).toLowerCase()).join(' ')\n\t\tthrow new Error(\n\t\t\t`${name} not found. Set ${envVar} environment variable or create ${secretFile} file`,\n\t\t)\n\t}\n\treturn secret\n}\n"],"mappings":";AAAA,SAAS,gBAAgB;AAYzB,eAAsB,WAAW,QAAgB,YAAiD;AAEjG,QAAM,WAAW,QAAQ,IAAI,MAAM;AACnC,MAAI,SAAU,QAAO;AAGrB,MAAI;AACH,UAAM,cAAc,MAAM,SAAS,YAAY,OAAO;AACtD,WAAO,YAAY,KAAK,KAAK;AAAA,EAC9B,QAAQ;AAEP,WAAO;AAAA,EACR;AACD;AAUA,eAAsB,kBACrB,QACA,YACA,aACkB;AAClB,QAAM,SAAS,MAAM,WAAW,QAAQ,UAAU;AAClD,MAAI,CAAC,QAAQ;AACZ,UAAM,OAAO,eAAe,OAAO,MAAM,GAAG,EAAE,IAAI,UAAQ,KAAK,OAAO,CAAC,IAAI,KAAK,MAAM,CAAC,EAAE,YAAY,CAAC,EAAE,KAAK,GAAG;AAChH,UAAM,IAAI;AAAA,MACT,GAAG,IAAI,mBAAmB,MAAM,mCAAmC,UAAU;AAAA,IAC9E;AAAA,EACD;AACA,SAAO;AACR;","names":[]}
package/dist/index.min.js CHANGED
@@ -13,13 +13,16 @@ import {
13
13
  import {
14
14
  loadSecret,
15
15
  loadSecretOrThrow
16
- } from "./chunk-XAFSYOU2.min.js";
16
+ } from "./chunk-IUNRH65Q.min.js";
17
17
  import {
18
- watchName
19
- } from "./chunk-IAQU2NGT.min.js";
18
+ IpnsWatcher,
19
+ watchName,
20
+ watchNameRaw
21
+ } from "./chunk-GB53WLYA.min.js";
20
22
  import "./chunk-MZUPFLFH.min.js";
21
23
  import "./chunk-PHITDXZT.min.js";
22
24
  export {
25
+ IpnsWatcher,
23
26
  ManagedThread,
24
27
  generateIpnsKey,
25
28
  getW3NamePublic,
@@ -28,6 +31,7 @@ export {
28
31
  loadSecretOrThrow,
29
32
  publishIPNS,
30
33
  publishIpnsThread,
31
- watchName
34
+ watchName,
35
+ watchNameRaw
32
36
  };
33
37
  //# sourceMappingURL=index.min.js.map
package/dist/secrets.d.ts CHANGED
@@ -12,9 +12,9 @@ export declare function loadSecret(envVar: string, secretFile: string): Promise<
12
12
  * Load an API key from environment variable or secret file, with error handling
13
13
  * @param envVar - Environment variable name
14
14
  * @param secretFile - Path to secret file
15
- * @param displayName - Human-readable name for error messages (e.g., 'REGOLO API key')
15
+ * @param displayName - Human-readable name for error messages. Defaults to capitalized envVar (e.g., 'MISTRAL_API_KEY' -> 'Mistral Api Key')
16
16
  * @returns The API key
17
17
  * @throws If the key is not found
18
18
  */
19
- export declare function loadSecretOrThrow(envVar: string, secretFile: string, displayName: string): Promise<string>;
19
+ export declare function loadSecretOrThrow(envVar: string, secretFile: string, displayName?: string): Promise<string>;
20
20
  //# sourceMappingURL=secrets.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAEA;;GAEG;AAEH;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAahG;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACtC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAQjB"}
1
+ {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAEA;;GAEG;AAEH;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAahG;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACtC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,CASjB"}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  loadSecret,
3
3
  loadSecretOrThrow
4
- } from "./chunk-XAFSYOU2.min.js";
4
+ } from "./chunk-IUNRH65Q.min.js";
5
5
  import "./chunk-PHITDXZT.min.js";
6
6
  export {
7
7
  loadSecret,
package/dist/watcher.d.ts CHANGED
@@ -17,8 +17,45 @@ export interface WatchOptions {
17
17
  onOpen?: () => void;
18
18
  onClose?: (event: CloseEvent) => void;
19
19
  }
20
+ export interface IpnsWatcherOptions {
21
+ onUpdate: (record: W3NameRecord) => void | Promise<void>;
22
+ onError?: (error: Error | Event) => void;
23
+ onConnected?: () => void;
24
+ onDisconnected?: () => void;
25
+ /** Initial backoff delay in ms (default: 5000) */
26
+ initialBackoff?: number;
27
+ /** Max backoff delay in ms (default: 900000 = 15min) */
28
+ maxBackoff?: number;
29
+ }
30
+ /**
31
+ * Low-level WebSocket watcher for IPNS (no reconnect logic)
32
+ * @internal Use IpnsWatcher for robust watching with auto-reconnect
33
+ */
34
+ export declare function watchNameRaw(name: string, options: WatchOptions): WatchSubscription;
35
+ /**
36
+ * Robust IPNS watcher with auto-reconnect and catch-up logic
37
+ */
38
+ export declare class IpnsWatcher {
39
+ private name;
40
+ private options;
41
+ private subscription;
42
+ private backoffDelay;
43
+ private lastKnownValue;
44
+ private stopped;
45
+ private reconnectTimeout;
46
+ constructor(name: string, options: IpnsWatcherOptions);
47
+ start(): void;
48
+ stop(): void;
49
+ private connect;
50
+ private scheduleReconnect;
51
+ /**
52
+ * Resolve current IPNS value via HTTP API
53
+ */
54
+ private resolveCurrentValue;
55
+ private checkForMissedUpdates;
56
+ }
20
57
  /**
21
- * Subscribe to IPNS name updates via WebSocket
58
+ * Subscribe to IPNS name updates with auto-reconnect and catch-up logic
22
59
  */
23
- export declare function watchName(name: string, options: WatchOptions): WatchSubscription;
60
+ export declare function watchName(name: string, options: IpnsWatcherOptions): IpnsWatcher;
24
61
  //# sourceMappingURL=watcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,EAAE,EAAE,SAAS,CAAA;CACb;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;IACxC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,KAAK,IAAI,CAAA;IACxC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;CACrC;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,iBAAiB,CAuChF"}
1
+ {"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,EAAE,EAAE,SAAS,CAAA;CACb;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;IACxC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,KAAK,IAAI,CAAA;IACxC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;CACrC;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,KAAK,IAAI,CAAA;IACxC,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,kDAAkD;IAClD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,iBAAiB,CAuCnF;AAED;;GAEG;AACH,qBAAa,WAAW;IACvB,OAAO,CAAC,IAAI,CAAQ;IACpB,OAAO,CAAC,OAAO,CAId;IACD,OAAO,CAAC,YAAY,CAAiC;IACrD,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,cAAc,CAAsB;IAC5C,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,gBAAgB,CAA8B;gBAE1C,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB;IAarD,KAAK,IAAI,IAAI;IAQb,IAAI,IAAI,IAAI;IAaZ,OAAO,CAAC,OAAO;IAsBf,OAAO,CAAC,iBAAiB;IA0BzB;;OAEG;YACW,mBAAmB;YAkBnB,qBAAqB;CAmBnC;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAIhF"}
@@ -1,9 +1,13 @@
1
1
  import {
2
- watchName
3
- } from "./chunk-IAQU2NGT.min.js";
2
+ IpnsWatcher,
3
+ watchName,
4
+ watchNameRaw
5
+ } from "./chunk-GB53WLYA.min.js";
4
6
  import "./chunk-MZUPFLFH.min.js";
5
7
  import "./chunk-PHITDXZT.min.js";
6
8
  export {
7
- watchName
9
+ IpnsWatcher,
10
+ watchName,
11
+ watchNameRaw
8
12
  };
9
13
  //# sourceMappingURL=watcher.min.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wovin/daemon-utils",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "type": "module",
5
5
  "main": "./dist/index.min.js",
6
6
  "module": "./dist/index.min.js",
@@ -18,8 +18,8 @@
18
18
  "access": "public"
19
19
  },
20
20
  "peerDependencies": {
21
- "@wovin/core": "^0.0.18",
22
- "@wovin/connect-ucan-store-proxy": "^0.0.18"
21
+ "@wovin/core": "0.0.20",
22
+ "@wovin/connect-ucan-store-proxy": "0.0.20"
23
23
  },
24
24
  "dependencies": {
25
25
  "besonders-logger": "1.0.2",
@@ -29,8 +29,11 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^22.15.21",
32
+ "@wovin/connect-ucan-store-proxy": "0.0.20",
33
+ "@wovin/core": "0.0.20",
32
34
  "concurrently": "^8.2.2",
33
35
  "tsup": "^8.5.0",
36
+ "tsx": "^4.19.2",
34
37
  "typescript": "^5.9.2",
35
38
  "tsupconfig": "^0.0.0"
36
39
  },
@@ -41,6 +44,7 @@
41
44
  "dev": "concurrently \"pnpm dev:code\" \"pnpm dev:types\"",
42
45
  "dev:code": "tsup --watch",
43
46
  "dev:types": "tsc --emitDeclarationOnly --declaration --watch",
47
+ "test": "tsx test.ts",
44
48
  "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
45
49
  "pub": "npm publish --tag latest --access=public"
46
50
  }
@@ -1,46 +0,0 @@
1
- import {
2
- g
3
- } from "./chunk-MZUPFLFH.min.js";
4
-
5
- // src/watcher.ts
6
- var { WARN, LOG, DEBUG, ERROR } = g.setup(g.INFO);
7
- var NAME_WS_URL = "wss://name.web3.storage/name";
8
- function watchName(name, options) {
9
- const url = `${NAME_WS_URL}/${name}/watch`;
10
- DEBUG("[w3name-watch] connecting to", url);
11
- const ws = new WebSocket(url);
12
- ws.onopen = () => {
13
- LOG("[w3name-watch] connected to", name);
14
- options.onOpen?.();
15
- };
16
- ws.onmessage = (event) => {
17
- try {
18
- const record = JSON.parse(event.data);
19
- DEBUG("[w3name-watch] received update for", name, record);
20
- options.onUpdate(record);
21
- } catch (err) {
22
- WARN("[w3name-watch] failed to parse message:", event.data, err);
23
- options.onError?.(err instanceof Error ? err : new Error(String(err)));
24
- }
25
- };
26
- ws.onerror = (event) => {
27
- ERROR("[w3name-watch] error for", name, event);
28
- options.onError?.(event);
29
- };
30
- ws.onclose = (event) => {
31
- DEBUG("[w3name-watch] closed for", name, "code:", event.code, "reason:", event.reason);
32
- options.onClose?.(event);
33
- };
34
- return {
35
- close: () => {
36
- DEBUG("[w3name-watch] closing connection for", name);
37
- ws.close();
38
- },
39
- ws
40
- };
41
- }
42
-
43
- export {
44
- watchName
45
- };
46
- //# sourceMappingURL=chunk-IAQU2NGT.min.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/watcher.ts"],"sourcesContent":["/**\n * IPNS Thread Watcher\n * Watches an IPNS thread for new audio files via WebSocket\n */\n\nimport { Logger } from 'besonders-logger'\n\nconst { WARN, LOG, DEBUG, ERROR } = Logger.setup(Logger.INFO)\n\nconst NAME_WS_URL = 'wss://name.web3.storage/name'\n\nexport interface W3NameRecord {\n\tvalue: string // e.g. \"/ipfs/bafybeig...\"\n\tseq?: number\n\tvalidity?: string\n}\n\nexport interface WatchSubscription {\n\tclose: () => void\n\tws: WebSocket\n}\n\nexport interface WatchOptions {\n\tonUpdate: (record: W3NameRecord) => void\n\tonError?: (error: Event | Error) => void\n\tonOpen?: () => void\n\tonClose?: (event: CloseEvent) => void\n}\n\n/**\n * Subscribe to IPNS name updates via WebSocket\n */\nexport function watchName(name: string, options: WatchOptions): WatchSubscription {\n\tconst url = `${NAME_WS_URL}/${name}/watch`\n\tDEBUG('[w3name-watch] connecting to', url)\n\n\tconst ws = new WebSocket(url)\n\n\tws.onopen = () => {\n\t\tLOG('[w3name-watch] connected to', name)\n\t\toptions.onOpen?.()\n\t}\n\n\tws.onmessage = (event) => {\n\t\ttry {\n\t\t\tconst record: W3NameRecord = JSON.parse(event.data)\n\t\t\tDEBUG('[w3name-watch] received update for', name, record)\n\t\t\toptions.onUpdate(record)\n\t\t} catch (err) {\n\t\t\tWARN('[w3name-watch] failed to parse message:', event.data, err)\n\t\t\toptions.onError?.(err instanceof Error ? err : new Error(String(err)))\n\t\t}\n\t}\n\n\tws.onerror = (event) => {\n\t\tERROR('[w3name-watch] error for', name, event)\n\t\toptions.onError?.(event)\n\t}\n\n\tws.onclose = (event) => {\n\t\tDEBUG('[w3name-watch] closed for', name, 'code:', event.code, 'reason:', event.reason)\n\t\toptions.onClose?.(event)\n\t}\n\n\treturn {\n\t\tclose: () => {\n\t\t\tDEBUG('[w3name-watch] closing connection for', name)\n\t\t\tws.close()\n\t\t},\n\t\tws,\n\t}\n}\n"],"mappings":";;;;;AAOA,IAAM,EAAE,MAAM,KAAK,OAAO,MAAM,IAAI,EAAO,MAAM,EAAO,IAAI;AAE5D,IAAM,cAAc;AAuBb,SAAS,UAAU,MAAc,SAA0C;AACjF,QAAM,MAAM,GAAG,WAAW,IAAI,IAAI;AAClC,QAAM,gCAAgC,GAAG;AAEzC,QAAM,KAAK,IAAI,UAAU,GAAG;AAE5B,KAAG,SAAS,MAAM;AACjB,QAAI,+BAA+B,IAAI;AACvC,YAAQ,SAAS;AAAA,EAClB;AAEA,KAAG,YAAY,CAAC,UAAU;AACzB,QAAI;AACH,YAAM,SAAuB,KAAK,MAAM,MAAM,IAAI;AAClD,YAAM,sCAAsC,MAAM,MAAM;AACxD,cAAQ,SAAS,MAAM;AAAA,IACxB,SAAS,KAAK;AACb,WAAK,2CAA2C,MAAM,MAAM,GAAG;AAC/D,cAAQ,UAAU,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IACtE;AAAA,EACD;AAEA,KAAG,UAAU,CAAC,UAAU;AACvB,UAAM,4BAA4B,MAAM,KAAK;AAC7C,YAAQ,UAAU,KAAK;AAAA,EACxB;AAEA,KAAG,UAAU,CAAC,UAAU;AACvB,UAAM,6BAA6B,MAAM,SAAS,MAAM,MAAM,WAAW,MAAM,MAAM;AACrF,YAAQ,UAAU,KAAK;AAAA,EACxB;AAEA,SAAO;AAAA,IACN,OAAO,MAAM;AACZ,YAAM,yCAAyC,IAAI;AACnD,SAAG,MAAM;AAAA,IACV;AAAA,IACA;AAAA,EACD;AACD;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/secrets.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\n\n/**\n * Utility functions for loading API keys from environment variables or secret files\n */\n\n/**\n * Load an API key from environment variable or secret file\n * @param envVar - Environment variable name (e.g., 'REGOLO_API_KEY')\n * @param secretFile - Path to secret file (e.g., 'secret/regolo')\n * @returns The API key, or undefined if not found\n */\nexport async function loadSecret(envVar: string, secretFile: string): Promise<string | undefined> {\n\t// Try environment variable first\n\tconst envValue = process.env[envVar]\n\tif (envValue) return envValue\n\n\t// Fall back to secret file\n\ttry {\n\t\tconst fileContent = await readFile(secretFile, 'utf-8')\n\t\treturn fileContent.trim() || undefined\n\t} catch {\n\t\t// File doesn't exist or can't be read\n\t\treturn undefined\n\t}\n}\n\n/**\n * Load an API key from environment variable or secret file, with error handling\n * @param envVar - Environment variable name\n * @param secretFile - Path to secret file\n * @param displayName - Human-readable name for error messages (e.g., 'REGOLO API key')\n * @returns The API key\n * @throws If the key is not found\n */\nexport async function loadSecretOrThrow(\n\tenvVar: string,\n\tsecretFile: string,\n\tdisplayName: string,\n): Promise<string> {\n\tconst secret = await loadSecret(envVar, secretFile)\n\tif (!secret) {\n\t\tthrow new Error(\n\t\t\t`${displayName} not found. Set ${envVar} environment variable or create ${secretFile} file`,\n\t\t)\n\t}\n\treturn secret\n}\n"],"mappings":";AAAA,SAAS,gBAAgB;AAYzB,eAAsB,WAAW,QAAgB,YAAiD;AAEjG,QAAM,WAAW,QAAQ,IAAI,MAAM;AACnC,MAAI,SAAU,QAAO;AAGrB,MAAI;AACH,UAAM,cAAc,MAAM,SAAS,YAAY,OAAO;AACtD,WAAO,YAAY,KAAK,KAAK;AAAA,EAC9B,QAAQ;AAEP,WAAO;AAAA,EACR;AACD;AAUA,eAAsB,kBACrB,QACA,YACA,aACkB;AAClB,QAAM,SAAS,MAAM,WAAW,QAAQ,UAAU;AAClD,MAAI,CAAC,QAAQ;AACZ,UAAM,IAAI;AAAA,MACT,GAAG,WAAW,mBAAmB,MAAM,mCAAmC,UAAU;AAAA,IACrF;AAAA,EACD;AACA,SAAO;AACR;","names":[]}