@attocash/n8n-nodes-atto 0.3.2 → 0.4.0

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/README.md CHANGED
@@ -83,6 +83,8 @@ Trigger events:
83
83
  - **Transaction**: watches by hash, address stream, or all supported transactions.
84
84
  - **Account Entry**: watches by hash, address stream, or all supported account entries.
85
85
 
86
+ Triggers stay connected to the selected Atto NDJSON endpoint. If a stream closes, the node reconnects with exponential backoff from 1 to 30 seconds; a received event resets the delay.
87
+
86
88
  ## Example Workflows
87
89
 
88
90
  Importable examples live in [`examples`](./examples):
@@ -31,6 +31,38 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31
31
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32
32
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
33
 
34
+ ## js-joda
35
+
36
+ `@js-joda/core` is licensed under the BSD 3-Clause License.
37
+
38
+ Copyright (c) 2016, Philipp Thürwächter & Pattrick Hüper
39
+
40
+ All rights reserved.
41
+
42
+ Redistribution and use in source and binary forms, with or without
43
+ modification, are permitted provided that the following conditions are met:
44
+
45
+ 1. Redistributions of source code must retain the above copyright notice,
46
+ this list of conditions and the following disclaimer.
47
+ 2. Redistributions in binary form must reproduce the above copyright notice,
48
+ this list of conditions and the following disclaimer in the documentation
49
+ and/or other materials provided with the distribution.
50
+ 3. Neither the name of js-joda nor the names of its contributors may be used
51
+ to endorse or promote products derived from this software without specific
52
+ prior written permission.
53
+
54
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
55
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
56
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
57
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
58
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
59
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
60
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
61
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
62
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
63
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
64
+ POSSIBILITY OF SUCH DAMAGE.
65
+
34
66
  ## Noble Hashes
35
67
 
36
68
  `@noble/hashes` is licensed under the MIT License.
@@ -7,12 +7,12 @@ const SECRET_PARAMETER_NAMES = ['secretSource', 'walletSecretType', 'walletSecre
7
7
  const OPERATION_PARAMETER_NAMES = {
8
8
  deriveAddress: SECRET_PARAMETER_NAMES,
9
9
  deriveAccount: SECRET_PARAMETER_NAMES,
10
- getAccount: ['address', 'simplify'],
11
- getReceivables: ['addressSource', 'addresses', 'minAmount', 'minAmountUnit', 'maxItems', 'timeoutMs', 'simplify'],
12
- getTransactions: ['queryMode', 'addresses', 'hash', 'fromHeight', 'toHeight', 'maxItems', 'timeoutMs', 'simplify'],
10
+ getAccount: ['address'],
11
+ getReceivables: ['addressSource', 'addresses', 'minAmount', 'minAmountUnit', 'maxItems', 'timeoutMs'],
12
+ getTransactions: ['queryMode', 'addresses', 'hash', 'fromHeight', 'toHeight', 'maxItems', 'timeoutMs'],
13
13
  sendTransaction: [...SECRET_PARAMETER_NAMES, 'destinationAddress', 'amount', 'amountUnit', 'timeoutMs'],
14
14
  receivePending: [...SECRET_PARAMETER_NAMES, 'representativeAddress', 'timeoutMs'],
15
- getAccountEntries: ['queryMode', 'addresses', 'hash', 'fromHeight', 'toHeight', 'maxItems', 'timeoutMs', 'simplify'],
15
+ getAccountEntries: ['queryMode', 'addresses', 'hash', 'fromHeight', 'toHeight', 'maxItems', 'timeoutMs'],
16
16
  changeRepresentative: [...SECRET_PARAMETER_NAMES, 'representativeAddress'],
17
17
  };
18
18
  const DEFAULT_OPERATION_BY_RESOURCE = {
@@ -51,7 +51,6 @@ const DEFAULT_PARAMETER_VALUES = {
51
51
  representativeAddress: '',
52
52
  timeoutMs: 5000,
53
53
  maxItems: 25,
54
- simplify: true,
55
54
  };
56
55
  const AMOUNT_UNITS = [
57
56
  {
@@ -553,18 +552,6 @@ class Atto {
553
552
  },
554
553
  description: 'New representative address',
555
554
  },
556
- {
557
- displayName: 'Simplify',
558
- name: 'simplify',
559
- type: 'boolean',
560
- default: true,
561
- displayOptions: {
562
- show: {
563
- operation: ['getAccount', 'getReceivables', 'getTransactions', 'getAccountEntries'],
564
- },
565
- },
566
- description: 'Whether to return a compact set of commonly used fields instead of the complete Atto API response',
567
- },
568
555
  {
569
556
  displayName: 'Max Items',
570
557
  name: 'maxItems',
@@ -1,14 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.executeAttoOperation = executeAttoOperation;
4
- exports.pollAttoEvent = pollAttoEvent;
4
+ exports.attoStreamRequest = attoStreamRequest;
5
+ exports.attoHttpStreamRequest = attoHttpStreamRequest;
6
+ exports.startAttoEventStream = startAttoEventStream;
5
7
  const n8n_workflow_1 = require("n8n-workflow");
6
8
  const protocol_1 = require("./protocol");
7
9
  const DEFAULT_STREAM_TIMEOUT_MS = 5000;
8
10
  const DEFAULT_PUBLISH_TIMEOUT_MS = 60000;
9
- const POLL_MAX_ITEMS = 100;
10
- const POLL_TIMEOUT_MS = 2000;
11
- const MAX_SEEN_POLL_ITEMS = 500;
11
+ const INITIAL_RECONNECT_DELAY_MS = 1000;
12
+ const MAX_RECONNECT_DELAY_MS = 30000;
13
+ function scheduleTimeout(callback, delayMs) {
14
+ const controller = new AbortController();
15
+ const timeout = AbortSignal.timeout(delayMs);
16
+ const onTimeout = () => {
17
+ if (!controller.signal.aborted)
18
+ callback();
19
+ };
20
+ timeout.addEventListener('abort', onTimeout, { once: true });
21
+ controller.signal.addEventListener('abort', () => timeout.removeEventListener('abort', onTimeout), { once: true });
22
+ return controller;
23
+ }
24
+ const DEFAULT_STREAM_RUNTIME = {
25
+ open: openAttoEventStream,
26
+ setTimeout: scheduleTimeout,
27
+ clearTimeout: (timer) => timer.abort(),
28
+ };
12
29
  function credentials(value) {
13
30
  return (value ?? {});
14
31
  }
@@ -39,13 +56,14 @@ function positiveHeight(value, fieldName) {
39
56
  throw new Error(`${fieldName} must be a positive integer`);
40
57
  return valueText;
41
58
  }
42
- function simplifyOutput(parameters) {
43
- return parameters.simplify !== false;
44
- }
45
59
  function requireNodeUrl(value) {
46
60
  const url = text(value.nodeUrl);
47
61
  if (!url)
48
62
  throw new Error('Atto credentials must include a Node Base URL');
63
+ const parsed = URL.canParse(url) ? new URL(url) : undefined;
64
+ if (!parsed || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {
65
+ throw new Error('Atto credentials must include a valid HTTP Node Base URL');
66
+ }
49
67
  return url.replace(/\/+$/, '');
50
68
  }
51
69
  function requireWorkerUrl(value) {
@@ -189,6 +207,24 @@ async function requestStream(context, value, request) {
189
207
  throw apiError(context, error, `Atto API stream failed: ${request.method} /${request.path}`);
190
208
  }
191
209
  }
210
+ async function openAttoEventStream(context, value, request, signal) {
211
+ const response = await context.helpers.httpRequest({
212
+ url: `${requireNodeUrl(value)}/${request.path}`,
213
+ method: request.method,
214
+ headers: {
215
+ ...requestHeaders(value, 'application/x-ndjson'),
216
+ ...(request.body ? { 'Content-Type': 'application/json' } : {}),
217
+ },
218
+ encoding: 'stream',
219
+ json: false,
220
+ abortSignal: signal,
221
+ ...(request.body ? { body: (0, protocol_1.stringifyAttoJson)(request.body) } : {}),
222
+ });
223
+ if (!response || typeof response !== 'object' || !(Symbol.asyncIterator in response)) {
224
+ throw new Error('Atto stream response is not readable');
225
+ }
226
+ return response;
227
+ }
192
228
  function contextRequired(context) {
193
229
  if (!context)
194
230
  throw new Error('This Atto operation requires an n8n execution context');
@@ -410,7 +446,6 @@ async function receivableFromInput(context, value, parameters) {
410
446
  }
411
447
  async function executeAttoOperation(context, operation, parameters, credentialData) {
412
448
  const value = credentials(credentialData);
413
- const simplify = simplifyOutput(parameters);
414
449
  if (operation === 'deriveAddress' || operation === 'deriveAccount') {
415
450
  const derived = await (0, protocol_1.deriveAddressFromSecret)(parameters, value);
416
451
  return { address: derived.value, publicKey: derived.publicKey, keyIndex: derived.keyIndex, secretType: derived.secretType };
@@ -419,21 +454,21 @@ async function executeAttoOperation(context, operation, parameters, credentialDa
419
454
  if (operation === 'getAccount') {
420
455
  const address = (0, protocol_1.parseAddress)(parameters.address ?? parameters.lookupAddress);
421
456
  const account = await accountByAddress(execution, value, address);
422
- return account ? (0, protocol_1.accountOutput)(account, simplify) : { found: false, address: address.value };
457
+ return account ? (0, protocol_1.accountOutput)(account) : { found: false, address: address.value };
423
458
  }
424
459
  if (operation === 'getReceivables') {
425
- return (await requestStream(execution, value, await receivableStreamRequest(parameters, value))).map((item) => (0, protocol_1.receivableOutput)(item, simplify));
460
+ return (await requestStream(execution, value, await receivableStreamRequest(parameters, value))).map((item) => (0, protocol_1.receivableOutput)(item));
426
461
  }
427
462
  if (operation === 'getTransactions') {
428
463
  if (queryMode(parameters.queryMode) === 'hash') {
429
464
  const hash = (0, protocol_1.parseHash)(parameters.hash);
430
465
  const transaction = (await requestJson(execution, value, requireNodeUrl(value), 'GET', `transactions/${hash}`));
431
- return [(0, protocol_1.transactionOutput)(transaction, simplify)];
466
+ return [(0, protocol_1.transactionOutput)(transaction)];
432
467
  }
433
- return (await requestStream(execution, value, await transactionStreamRequest(parameters, value))).map((item) => (0, protocol_1.transactionOutput)(item, simplify));
468
+ return (await requestStream(execution, value, await transactionStreamRequest(parameters, value))).map((item) => (0, protocol_1.transactionOutput)(item));
434
469
  }
435
470
  if (operation === 'getAccountEntries') {
436
- return (await requestStream(execution, value, await accountEntryStreamRequest(parameters, value))).map((item) => (0, protocol_1.accountEntryOutput)(item, simplify));
471
+ return (await requestStream(execution, value, await accountEntryStreamRequest(parameters, value))).map((item) => (0, protocol_1.accountEntryOutput)(item));
437
472
  }
438
473
  const derived = await (0, protocol_1.deriveAddressFromSecret)(parameters, value);
439
474
  const account = await accountByAddress(execution, value, derived);
@@ -447,7 +482,7 @@ async function executeAttoOperation(context, operation, parameters, credentialDa
447
482
  const block = (0, protocol_1.createSendBlock)(account, destination, amount, timestamp);
448
483
  const transaction = await publishBlock(execution, value, block, derived.signer, timeoutMs);
449
484
  return {
450
- ...(0, protocol_1.transactionOutput)(transaction, true, 'published'),
485
+ ...(0, protocol_1.transactionOutput)(transaction, 'published'),
451
486
  fromAddress: derived.value,
452
487
  destinationAddress: destination.value,
453
488
  amount: (0, protocol_1.amountOutput)(amount),
@@ -464,7 +499,7 @@ async function executeAttoOperation(context, operation, parameters, credentialDa
464
499
  const block = (0, protocol_1.createReceiveBlock)(account, receivable, representative, timestamp);
465
500
  const transaction = await publishBlock(execution, value, block, derived.signer, timeoutMs);
466
501
  return {
467
- ...(0, protocol_1.transactionOutput)(transaction, true, 'received'),
502
+ ...(0, protocol_1.transactionOutput)(transaction, 'received'),
468
503
  address: derived.value,
469
504
  representativeAddress: representative.value,
470
505
  amount: (0, protocol_1.amountOutput)(receivable.amount),
@@ -477,66 +512,234 @@ async function executeAttoOperation(context, operation, parameters, credentialDa
477
512
  const block = (0, protocol_1.createChangeBlock)(account, representative, timestamp);
478
513
  const transaction = await publishBlock(execution, value, block, derived.signer, timeoutMs);
479
514
  return {
480
- ...(0, protocol_1.transactionOutput)(transaction, true, 'representative_changed'),
515
+ ...(0, protocol_1.transactionOutput)(transaction, 'representative_changed'),
481
516
  address: derived.value,
482
517
  representativeAddress: representative.value,
483
518
  };
484
519
  }
485
520
  throw new Error(`Unsupported Atto operation: ${operation}`);
486
521
  }
487
- async function pollAccounts(context, parameters, value) {
488
- const addresses = await addressesForSource(parameters, value, true);
489
- if (addresses?.length === 1) {
490
- const account = await accountByAddress(context, value, addresses[0]);
491
- return account ? [(0, protocol_1.accountOutput)(account, true)] : [];
492
- }
493
- return (await requestStream(context, value, {
494
- method: addresses ? 'POST' : 'GET',
495
- path: 'accounts/stream',
496
- ...(addresses ? { body: { addresses: addresses.map((address) => address.value) } } : {}),
497
- maxItems: POLL_MAX_ITEMS,
498
- timeoutMs: POLL_TIMEOUT_MS,
499
- })).map((item) => (0, protocol_1.accountOutput)(item, true));
500
- }
501
- function pollItemKey(event, item) {
502
- if (event === 'account')
503
- return `${String(item.address)}:${String(item.frontier)}:${String(item.balance?.raw ?? '')}`;
504
- return String(item.hash ?? JSON.stringify(item));
505
- }
506
- function newPollItems(context, event, items) {
507
- const staticData = context.getWorkflowStaticData('node');
508
- const stateKey = `seen_${event}`;
509
- const existing = Array.isArray(staticData[stateKey]) ? staticData[stateKey].map(String) : [];
510
- const seen = new Set(existing);
511
- const fresh = items.filter((item) => !seen.has(pollItemKey(event, item)));
512
- const updated = [...existing, ...items.map((item) => pollItemKey(event, item))].slice(-MAX_SEEN_POLL_ITEMS);
513
- staticData[stateKey] = updated;
514
- if (existing.length === 0 && context.getMode() !== 'manual')
515
- return [];
516
- return fresh;
517
- }
518
- async function pollAttoEvent(context, event, parameters, credentialData) {
522
+ function addressStreamRoute(event, addresses) {
523
+ if (!addresses)
524
+ return { event, route: 'all' };
525
+ return {
526
+ event,
527
+ route: addresses.length === 1 ? 'publicKey' : 'addresses',
528
+ addresses,
529
+ };
530
+ }
531
+ async function attoStreamRequest(event, parameters, credentialData) {
519
532
  const value = credentials(credentialData);
520
- let items;
521
533
  if (event === 'receivable') {
522
- items = (await requestStream(context, value, await receivableStreamRequest(parameters, value, POLL_MAX_ITEMS, POLL_TIMEOUT_MS))).map((item) => (0, protocol_1.receivableOutput)(item, true));
534
+ const request = addressStreamRoute(event, await addressesForSource(parameters, value));
535
+ return {
536
+ ...request,
537
+ minAmount: optionalText(parameters.minAmount)
538
+ ? (0, protocol_1.parseAmount)(parameters.minAmount, parameters.minAmountUnit ?? 'RAW', 'Minimum Amount').toString()
539
+ : '0',
540
+ };
523
541
  }
524
- else if (event === 'account') {
525
- items = await pollAccounts(context, parameters, value);
542
+ if (event === 'account') {
543
+ return addressStreamRoute(event, await addressesForSource(parameters, value, true));
526
544
  }
527
- else if (event === 'transaction') {
528
- const mode = queryMode(parameters.queryMode);
529
- if (mode === 'hash') {
530
- const transaction = (await requestJson(context, value, requireNodeUrl(value), 'GET', `transactions/${(0, protocol_1.parseHash)(parameters.hash)}`));
531
- items = [(0, protocol_1.transactionOutput)(transaction, true)];
545
+ const mode = queryMode(parameters.queryMode);
546
+ if (mode === 'hash') {
547
+ return {
548
+ event,
549
+ route: 'hash',
550
+ hash: (0, protocol_1.parseHash)(parameters.hash),
551
+ };
552
+ }
553
+ const request = addressStreamRoute(event, await addressesForQuery(parameters, value));
554
+ if (request.route === 'all')
555
+ return request;
556
+ return {
557
+ ...request,
558
+ fromHeight: positiveHeight(parameters.fromHeight, 'From Height') ?? '1',
559
+ toHeight: positiveHeight(parameters.toHeight, 'To Height'),
560
+ };
561
+ }
562
+ function requiredStreamAddresses(request) {
563
+ const addresses = request.addresses ?? [];
564
+ if (addresses.length === 0)
565
+ throw new Error('Atto stream route requires at least one address');
566
+ return addresses;
567
+ }
568
+ function streamHeightQuery(request) {
569
+ const fromHeight = request.fromHeight ?? '1';
570
+ return `fromHeight=${encodeURIComponent(fromHeight)}${request.toHeight ? `&toHeight=${encodeURIComponent(request.toHeight)}` : ''}`;
571
+ }
572
+ function streamHeightSearch(request) {
573
+ return {
574
+ search: requiredStreamAddresses(request).map((address) => ({
575
+ address: address.value,
576
+ fromHeight: request.fromHeight ?? '1',
577
+ ...(request.toHeight ? { toHeight: request.toHeight } : {}),
578
+ })),
579
+ };
580
+ }
581
+ function attoHttpStreamRequest(request) {
582
+ if (request.event === 'account') {
583
+ if (request.route === 'all')
584
+ return { method: 'GET', path: 'accounts/stream' };
585
+ const addresses = requiredStreamAddresses(request);
586
+ if (request.route === 'publicKey') {
587
+ return { method: 'GET', path: `accounts/${addresses[0].publicKey}/stream` };
532
588
  }
533
- else {
534
- items = (await requestStream(context, value, await transactionStreamRequest(parameters, value, POLL_MAX_ITEMS, POLL_TIMEOUT_MS))).map((item) => (0, protocol_1.transactionOutput)(item, true));
589
+ if (request.route === 'addresses') {
590
+ return {
591
+ method: 'POST',
592
+ path: 'accounts/stream',
593
+ body: { addresses: addresses.map((address) => address.value) },
594
+ };
535
595
  }
536
596
  }
537
- else {
538
- items = (await requestStream(context, value, await accountEntryStreamRequest(parameters, value, POLL_MAX_ITEMS, POLL_TIMEOUT_MS))).map((item) => (0, protocol_1.accountEntryOutput)(item, true));
597
+ if (request.event === 'receivable') {
598
+ const addresses = requiredStreamAddresses(request);
599
+ const path = `accounts/receivables/stream?minAmount=${encodeURIComponent(request.minAmount ?? '0')}`;
600
+ if (request.route === 'publicKey') {
601
+ return {
602
+ method: 'GET',
603
+ path: `accounts/${addresses[0].publicKey}/receivables/stream?minAmount=${encodeURIComponent(request.minAmount ?? '0')}`,
604
+ };
605
+ }
606
+ if (request.route === 'addresses') {
607
+ return {
608
+ method: 'POST',
609
+ path,
610
+ body: { addresses: addresses.map((address) => address.value) },
611
+ };
612
+ }
613
+ }
614
+ if (request.event === 'transaction') {
615
+ if (request.route === 'all')
616
+ return { method: 'GET', path: 'transactions/stream' };
617
+ if (request.route === 'hash' && request.hash) {
618
+ return { method: 'GET', path: `transactions/${request.hash}/stream` };
619
+ }
620
+ const addresses = requiredStreamAddresses(request);
621
+ if (request.route === 'publicKey') {
622
+ return {
623
+ method: 'GET',
624
+ path: `accounts/${addresses[0].publicKey}/transactions/stream?${streamHeightQuery(request)}`,
625
+ };
626
+ }
627
+ if (request.route === 'addresses') {
628
+ return {
629
+ method: 'POST',
630
+ path: 'accounts/transactions/stream',
631
+ body: streamHeightSearch(request),
632
+ };
633
+ }
634
+ }
635
+ if (request.event === 'accountEntry') {
636
+ if (request.route === 'all')
637
+ return { method: 'GET', path: 'accounts/entries/stream' };
638
+ if (request.route === 'hash' && request.hash) {
639
+ return { method: 'GET', path: `accounts/entries/${request.hash}/stream` };
640
+ }
641
+ const addresses = requiredStreamAddresses(request);
642
+ if (request.route === 'publicKey') {
643
+ return {
644
+ method: 'GET',
645
+ path: `accounts/${addresses[0].publicKey}/entries/stream?${streamHeightQuery(request)}`,
646
+ };
647
+ }
648
+ if (request.route === 'addresses') {
649
+ return {
650
+ method: 'POST',
651
+ path: 'accounts/entries/stream',
652
+ body: streamHeightSearch(request),
653
+ };
654
+ }
539
655
  }
540
- return newPollItems(context, event, items);
656
+ throw new Error(`Unsupported Atto ${request.event} stream route: ${request.route}`);
657
+ }
658
+ async function startAttoEventStream(context, event, parameters, credentialData, onEvent, runtime = DEFAULT_STREAM_RUNTIME) {
659
+ const value = credentials(credentialData);
660
+ requireNodeUrl(value);
661
+ requestHeaders(value, 'application/x-ndjson');
662
+ const request = attoHttpStreamRequest(await attoStreamRequest(event, parameters, credentialData));
663
+ let activeController;
664
+ let activeConnection;
665
+ let reconnectTimer;
666
+ let reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS;
667
+ let generation = 0;
668
+ let closed = false;
669
+ const scheduleReconnect = () => {
670
+ if (closed || activeController || reconnectTimer !== undefined)
671
+ return;
672
+ const delayMs = reconnectDelayMs;
673
+ reconnectDelayMs = Math.min(reconnectDelayMs * 2, MAX_RECONNECT_DELAY_MS);
674
+ reconnectTimer = runtime.setTimeout(() => {
675
+ reconnectTimer = undefined;
676
+ connect();
677
+ }, delayMs);
678
+ };
679
+ const connect = () => {
680
+ if (closed || activeController)
681
+ return;
682
+ const streamGeneration = ++generation;
683
+ const controller = new AbortController();
684
+ activeController = controller;
685
+ activeConnection = (async () => {
686
+ try {
687
+ const response = await runtime.open(context, value, request, controller.signal);
688
+ let pending = '';
689
+ for await (const chunk of response) {
690
+ if (closed || streamGeneration !== generation)
691
+ return;
692
+ pending += streamChunk(chunk);
693
+ const lines = pending.split('\n');
694
+ pending = lines.pop() ?? '';
695
+ for (const line of lines) {
696
+ if (!line.trim())
697
+ continue;
698
+ const parsed = (0, protocol_1.parseAttoJson)(line);
699
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
700
+ throw new Error(`Atto ${event} stream item must contain a JSON object`);
701
+ }
702
+ onEvent((0, protocol_1.streamEventOutput)(event, parsed));
703
+ reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS;
704
+ }
705
+ }
706
+ if (pending.trim() && !closed && streamGeneration === generation) {
707
+ const parsed = (0, protocol_1.parseAttoJson)(pending);
708
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
709
+ throw new Error(`Atto ${event} stream item must contain a JSON object`);
710
+ }
711
+ onEvent((0, protocol_1.streamEventOutput)(event, parsed));
712
+ reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS;
713
+ }
714
+ }
715
+ catch {
716
+ }
717
+ finally {
718
+ if (streamGeneration === generation) {
719
+ activeController = undefined;
720
+ activeConnection = undefined;
721
+ scheduleReconnect();
722
+ }
723
+ }
724
+ })();
725
+ };
726
+ connect();
727
+ return {
728
+ async close() {
729
+ if (closed)
730
+ return;
731
+ closed = true;
732
+ generation++;
733
+ if (reconnectTimer !== undefined) {
734
+ runtime.clearTimeout(reconnectTimer);
735
+ reconnectTimer = undefined;
736
+ }
737
+ const connection = activeConnection;
738
+ activeController?.abort();
739
+ activeController = undefined;
740
+ activeConnection = undefined;
741
+ await connection;
742
+ },
743
+ };
541
744
  }
542
745
  //# sourceMappingURL=operations.js.map