@arkade-os/sdk 0.4.20 → 0.4.21

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.
@@ -254,13 +254,18 @@ class ContractWatcher {
254
254
  }
255
255
  /**
256
256
  * Connect to the subscription.
257
+ *
258
+ * @param skipUpdate - Skip the leading `updateSubscription` call when
259
+ * the caller has already established `subscriptionId`.
257
260
  */
258
- async connect() {
261
+ async connect(skipUpdate = false) {
259
262
  if (!this.isWatching)
260
263
  return;
261
264
  this.connectionState = "connecting";
262
265
  try {
263
- await this.updateSubscription();
266
+ if (!skipUpdate) {
267
+ await this.updateSubscription();
268
+ }
264
269
  // Poll immediately after connection to sync state
265
270
  await this.pollAllContracts();
266
271
  this.connectionState = "connected";
@@ -391,11 +396,30 @@ class ContractWatcher {
391
396
  }
392
397
  }
393
398
  async tryUpdateSubscription() {
399
+ const hadSubscription = this.subscriptionId !== undefined;
394
400
  try {
395
401
  await this.updateSubscription();
396
402
  }
397
403
  catch (error) {
398
404
  // nothing, the connection will be retried later
405
+ return;
406
+ }
407
+ // Cold start: `startWatching` may have run with zero scripts,
408
+ // leaving `listenLoop` parked behind the reconnect timer. Kick
409
+ // `connect` now so streaming resumes without waiting on the
410
+ // backoff. `skipUpdate` avoids re-issuing `subscribeForScripts`.
411
+ const justGotSubscription = !hadSubscription && this.subscriptionId !== undefined;
412
+ const listenerParked = this.connectionState === "disconnected" ||
413
+ this.connectionState === "reconnecting";
414
+ if (this.isWatching && justGotSubscription && listenerParked) {
415
+ if (this.reconnectTimeoutId) {
416
+ clearTimeout(this.reconnectTimeoutId);
417
+ this.reconnectTimeoutId = undefined;
418
+ }
419
+ this.reconnectAttempts = 0;
420
+ this.connect(true).catch((error) => {
421
+ console.warn("ContractWatcher cold-start connect failed:", error);
422
+ });
399
423
  }
400
424
  }
401
425
  /**
@@ -236,18 +236,19 @@ class RestArkProvider {
236
236
  // leak the underlying SSE connection. `return()` is overridden below
237
237
  // so that closing the generator also closes the connection even when
238
238
  // the body is currently suspended at an await point.
239
- let eventSource = null;
239
+ let iterator = null;
240
+ const closeIterator = () => iterator?.close();
240
241
  // eslint-disable-next-line @typescript-eslint/no-this-alias
241
242
  const self = this;
242
243
  const gen = (async function* () {
243
- const abortHandler = () => eventSource?.close();
244
+ const abortHandler = closeIterator;
244
245
  signal?.addEventListener("abort", abortHandler);
245
246
  try {
246
247
  while (!signal?.aborted) {
247
- eventSource = new EventSource(url + queryParams);
248
- const iterator = (0, utils_1.eventSourceIterator)(eventSource);
248
+ const currentIterator = (0, utils_1.eventSourceIterator)(new EventSource(url + queryParams));
249
+ iterator = currentIterator;
249
250
  try {
250
- for await (const event of iterator) {
251
+ for await (const event of currentIterator) {
251
252
  if (signal?.aborted)
252
253
  break;
253
254
  try {
@@ -281,71 +282,87 @@ class RestArkProvider {
281
282
  throw error;
282
283
  }
283
284
  finally {
284
- eventSource.close();
285
+ currentIterator.close();
286
+ iterator = null;
285
287
  }
286
288
  }
287
289
  }
288
290
  finally {
289
291
  signal?.removeEventListener("abort", abortHandler);
290
- eventSource?.close();
292
+ closeIterator();
291
293
  }
292
294
  })();
293
295
  const origReturn = gen.return.bind(gen);
294
296
  gen.return = (value) => {
295
- eventSource?.close();
297
+ closeIterator();
296
298
  return origReturn(value);
297
299
  };
298
300
  return gen;
299
301
  }
300
- async *getTransactionsStream(signal) {
302
+ getTransactionsStream(signal) {
301
303
  const url = `${this.serverUrl}/v1/txs`;
302
- while (!signal?.aborted) {
304
+ let iterator = null;
305
+ const closeIterator = () => iterator?.close();
306
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
307
+ const self = this;
308
+ const gen = (async function* () {
309
+ const abortHandler = closeIterator;
310
+ signal?.addEventListener("abort", abortHandler);
303
311
  try {
304
- const eventSource = new EventSource(url);
305
- // Set up abort handling
306
- const abortHandler = () => {
307
- eventSource.close();
308
- };
309
- signal?.addEventListener("abort", abortHandler);
310
- try {
311
- for await (const event of (0, utils_1.eventSourceIterator)(eventSource)) {
312
- if (signal?.aborted)
313
- break;
314
- try {
315
- const data = JSON.parse(event.data);
316
- const txNotification = this.parseTransactionNotification(data);
317
- if (txNotification) {
318
- yield txNotification;
312
+ while (!signal?.aborted) {
313
+ try {
314
+ const currentIterator = (0, utils_1.eventSourceIterator)(new EventSource(url));
315
+ iterator = currentIterator;
316
+ for await (const event of currentIterator) {
317
+ if (signal?.aborted)
318
+ break;
319
+ try {
320
+ const data = JSON.parse(event.data);
321
+ const txNotification = self.parseTransactionNotification(data);
322
+ if (txNotification) {
323
+ yield txNotification;
324
+ }
325
+ }
326
+ catch (err) {
327
+ console.error("Failed to parse transaction notification:", err);
328
+ throw err;
319
329
  }
320
330
  }
321
- catch (err) {
322
- console.error("Failed to parse transaction notification:", err);
323
- throw err;
331
+ }
332
+ catch (error) {
333
+ if (signal?.aborted ||
334
+ (error instanceof Error &&
335
+ error.name === "AbortError")) {
336
+ break;
324
337
  }
338
+ // ignore timeout errors, they're expected when the server is not sending anything for 5 min
339
+ if (isFetchTimeoutError(error)) {
340
+ console.debug("Timeout error ignored");
341
+ continue;
342
+ }
343
+ if ((0, utils_1.isEventSourceError)(error)) {
344
+ throw error;
345
+ }
346
+ console.error("Transaction stream error:", error);
347
+ throw error;
348
+ }
349
+ finally {
350
+ closeIterator();
351
+ iterator = null;
325
352
  }
326
- }
327
- finally {
328
- signal?.removeEventListener("abort", abortHandler);
329
- eventSource.close();
330
353
  }
331
354
  }
332
- catch (error) {
333
- if (signal?.aborted ||
334
- (error instanceof Error && error.name === "AbortError")) {
335
- break;
336
- }
337
- // ignore timeout errors, they're expected when the server is not sending anything for 5 min
338
- if (isFetchTimeoutError(error)) {
339
- console.debug("Timeout error ignored");
340
- continue;
341
- }
342
- if ((0, utils_1.isEventSourceError)(error)) {
343
- throw error;
344
- }
345
- console.error("Transaction stream error:", error);
346
- throw error;
355
+ finally {
356
+ signal?.removeEventListener("abort", abortHandler);
357
+ closeIterator();
347
358
  }
348
- }
359
+ })();
360
+ const origReturn = gen.return.bind(gen);
361
+ gen.return = (value) => {
362
+ closeIterator();
363
+ return origReturn(value);
364
+ };
365
+ return gen;
349
366
  }
350
367
  async getPendingTxs(intent) {
351
368
  const url = `${this.serverUrl}/v1/tx/pending`;
@@ -156,62 +156,75 @@ class RestIndexerProvider {
156
156
  }
157
157
  return data;
158
158
  }
159
- async *getSubscription(subscriptionId, abortSignal) {
159
+ getSubscription(subscriptionId, abortSignal) {
160
160
  const url = `${this.serverUrl}/v1/indexer/script/subscription/${subscriptionId}`;
161
- while (!abortSignal?.aborted) {
161
+ let iterator = null;
162
+ const closeIterator = () => iterator?.close();
163
+ const gen = (async function* () {
164
+ const abortHandler = closeIterator;
165
+ abortSignal?.addEventListener("abort", abortHandler);
162
166
  try {
163
- const eventSource = new EventSource(url);
164
- // Set up abort handling
165
- const abortHandler = () => {
166
- eventSource.close();
167
- };
168
- abortSignal?.addEventListener("abort", abortHandler);
169
- try {
170
- for await (const event of (0, utils_1.eventSourceIterator)(eventSource)) {
171
- if (abortSignal?.aborted)
172
- break;
173
- try {
174
- const data = JSON.parse(event.data);
175
- if (data.event) {
176
- yield {
177
- txid: data.event.txid,
178
- scripts: data.event.scripts || [],
179
- newVtxos: (data.event.newVtxos || []).map(convertVtxo),
180
- spentVtxos: (data.event.spentVtxos || []).map(convertVtxo),
181
- sweptVtxos: (data.event.sweptVtxos || []).map(convertVtxo),
182
- tx: data.event.tx,
183
- checkpointTxs: data.event.checkpointTxs,
184
- };
167
+ while (!abortSignal?.aborted) {
168
+ try {
169
+ const currentIterator = (0, utils_1.eventSourceIterator)(new EventSource(url));
170
+ iterator = currentIterator;
171
+ for await (const event of currentIterator) {
172
+ if (abortSignal?.aborted)
173
+ break;
174
+ try {
175
+ const data = JSON.parse(event.data);
176
+ if (data.event) {
177
+ yield {
178
+ txid: data.event.txid,
179
+ scripts: data.event.scripts || [],
180
+ newVtxos: (data.event.newVtxos || []).map(convertVtxo),
181
+ spentVtxos: (data.event.spentVtxos || []).map(convertVtxo),
182
+ sweptVtxos: (data.event.sweptVtxos || []).map(convertVtxo),
183
+ tx: data.event.tx,
184
+ checkpointTxs: data.event.checkpointTxs,
185
+ };
186
+ }
187
+ }
188
+ catch (err) {
189
+ console.error("Failed to parse subscription event:", err);
190
+ throw err;
185
191
  }
186
192
  }
187
- catch (err) {
188
- console.error("Failed to parse subscription event:", err);
189
- throw err;
193
+ }
194
+ catch (error) {
195
+ if (abortSignal?.aborted ||
196
+ (error instanceof Error &&
197
+ error.name === "AbortError")) {
198
+ break;
190
199
  }
200
+ // ignore timeout errors, they're expected when the server is not sending anything for 5 min
201
+ if ((0, ark_1.isFetchTimeoutError)(error)) {
202
+ console.debug("Timeout error ignored");
203
+ continue;
204
+ }
205
+ if ((0, utils_1.isEventSourceError)(error)) {
206
+ throw error;
207
+ }
208
+ console.error("Subscription error:", error);
209
+ throw error;
210
+ }
211
+ finally {
212
+ closeIterator();
213
+ iterator = null;
191
214
  }
192
- }
193
- finally {
194
- abortSignal?.removeEventListener("abort", abortHandler);
195
- eventSource.close();
196
215
  }
197
216
  }
198
- catch (error) {
199
- if (abortSignal?.aborted ||
200
- (error instanceof Error && error.name === "AbortError")) {
201
- break;
202
- }
203
- // ignore timeout errors, they're expected when the server is not sending anything for 5 min
204
- if ((0, ark_1.isFetchTimeoutError)(error)) {
205
- console.debug("Timeout error ignored");
206
- continue;
207
- }
208
- if ((0, utils_1.isEventSourceError)(error)) {
209
- throw error;
210
- }
211
- console.error("Subscription error:", error);
212
- throw error;
217
+ finally {
218
+ abortSignal?.removeEventListener("abort", abortHandler);
219
+ closeIterator();
213
220
  }
214
- }
221
+ })();
222
+ const origReturn = gen.return.bind(gen);
223
+ gen.return = (value) => {
224
+ closeIterator();
225
+ return origReturn(value);
226
+ };
227
+ return gen;
215
228
  }
216
229
  async getVirtualTxs(txids, opts) {
217
230
  let url = `${this.serverUrl}/v1/indexer/virtualTx/${txids.join(",")}`;
@@ -2,32 +2,70 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.eventSourceIterator = eventSourceIterator;
4
4
  exports.isEventSourceError = isEventSourceError;
5
+ function createAbortError() {
6
+ const error = new Error("EventSource closed");
7
+ error.name = "AbortError";
8
+ return error;
9
+ }
5
10
  /**
6
- * Creates an async iterator over EventSource messages that attaches listeners
7
- * eagerly (at call time) rather than lazily (at first .next() call).
8
- * This ensures events are buffered immediately, preventing race conditions
9
- * where events arrive before iteration begins.
11
+ * Creates a close-aware EventSource async iterator.
12
+ *
13
+ * Listeners attach eagerly so events are buffered before the first next() call.
14
+ * close() closes the EventSource, removes listeners, and wakes any pending
15
+ * next() even when the browser does not emit an error from EventSource.close().
10
16
  */
11
17
  function eventSourceIterator(eventSource) {
12
18
  const messageQueue = [];
13
19
  const errorQueue = [];
14
20
  let messageResolve = null;
15
21
  let errorResolve = null;
22
+ let closed = false;
23
+ let cleanedUp = false;
24
+ const cleanup = () => {
25
+ if (cleanedUp)
26
+ return;
27
+ cleanedUp = true;
28
+ eventSource.removeEventListener("message", messageHandler);
29
+ eventSource.removeEventListener("error", errorHandler);
30
+ };
31
+ const close = () => {
32
+ if (closed)
33
+ return;
34
+ closed = true;
35
+ messageQueue.length = 0;
36
+ errorQueue.length = 0;
37
+ eventSource.close();
38
+ cleanup();
39
+ if (errorResolve) {
40
+ const reject = errorResolve;
41
+ messageResolve = null;
42
+ errorResolve = null;
43
+ reject(createAbortError());
44
+ }
45
+ };
16
46
  const messageHandler = (event) => {
47
+ if (closed)
48
+ return;
17
49
  if (messageResolve) {
18
- messageResolve(event);
50
+ const resolve = messageResolve;
19
51
  messageResolve = null;
52
+ errorResolve = null;
53
+ resolve(event);
20
54
  }
21
55
  else {
22
56
  messageQueue.push(event);
23
57
  }
24
58
  };
25
59
  const errorHandler = () => {
60
+ if (closed)
61
+ return;
26
62
  const error = new Error("EventSource error");
27
63
  error.name = "EventSourceError";
28
64
  if (errorResolve) {
29
- errorResolve(error);
65
+ const reject = errorResolve;
66
+ messageResolve = null;
30
67
  errorResolve = null;
68
+ reject(error);
31
69
  }
32
70
  else {
33
71
  errorQueue.push(error);
@@ -37,9 +75,9 @@ function eventSourceIterator(eventSource) {
37
75
  // even before the caller starts iterating
38
76
  eventSource.addEventListener("message", messageHandler);
39
77
  eventSource.addEventListener("error", errorHandler);
40
- return (async function* () {
78
+ const gen = (async function* () {
41
79
  try {
42
- while (true) {
80
+ while (!closed) {
43
81
  // if we have queued messages, yield the first one, remove it from the queue
44
82
  if (messageQueue.length > 0) {
45
83
  yield messageQueue.shift();
@@ -58,17 +96,25 @@ function eventSourceIterator(eventSource) {
58
96
  messageResolve = null;
59
97
  errorResolve = null;
60
98
  });
61
- if (result) {
99
+ if (!closed && result) {
62
100
  yield result;
63
101
  }
64
102
  }
65
103
  }
66
104
  finally {
67
- // clean up
68
- eventSource.removeEventListener("message", messageHandler);
69
- eventSource.removeEventListener("error", errorHandler);
105
+ closed = true;
106
+ cleanup();
107
+ eventSource.close();
70
108
  }
71
109
  })();
110
+ const origReturn = gen.return.bind(gen);
111
+ const managed = gen;
112
+ managed.close = close;
113
+ managed.return = (value) => {
114
+ close();
115
+ return origReturn(value);
116
+ };
117
+ return managed;
72
118
  }
73
119
  function isEventSourceError(error) {
74
120
  return error instanceof Error && error.name === "EventSourceError";