@stage-labs/metro 0.1.0-beta.249 → 0.1.0-beta.250
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +4 -3
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +2 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +7 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +22 -14
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +8 -53
- package/runtime/node_modules/@metro-labs/xmtp/src/network.ts +110 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/stream.ts +66 -0
- package/runtime/runtime.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.250",
|
|
4
4
|
"description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { updateChannelMeta } from './actions-meta.js';
|
|
17
17
|
import { deletedByRequests, isDeleteRequest, superAdminCheck } from './delete-requests.js';
|
|
18
18
|
import { isCallSignal } from './codecs.js';
|
|
19
|
+
import { syncConversation } from './network.js';
|
|
19
20
|
|
|
20
21
|
type Args = Record<string, unknown>;
|
|
21
22
|
type Handler = (id: string, args: Args) => Promise<void>;
|
|
@@ -61,7 +62,7 @@ async function read(id: string, args: Args): Promise<void> {
|
|
|
61
62
|
if (!conv)
|
|
62
63
|
throw new TrainError('NOT_FOUND', `conversation not found for ${line}`);
|
|
63
64
|
const lim = Math.min(Math.max(1, limit ?? 20), 200);
|
|
64
|
-
await
|
|
65
|
+
await syncConversation(acct.client, conv);
|
|
65
66
|
const all = await conv.messages();
|
|
66
67
|
const deleted = deletedByRequests(all, superAdminCheck(conv));
|
|
67
68
|
const slice = upTo(all.filter((m) => !isDeleteRequest(m) && !isCallSignal(m)), before).slice(-lim);
|
|
@@ -89,10 +90,10 @@ async function groupInfo(id: string, args: Args): Promise<void> {
|
|
|
89
90
|
|
|
90
91
|
async function listMembers(id: string, args: Args): Promise<void> {
|
|
91
92
|
const { line } = args as { line: string };
|
|
92
|
-
const {
|
|
93
|
+
const { conv } = await convOf(line);
|
|
93
94
|
if (!conv)
|
|
94
95
|
throw new TrainError('NOT_FOUND', `conversation not found for ${line}`);
|
|
95
|
-
respond(id, { result: await buildMemberList(
|
|
96
|
+
respond(id, { result: await buildMemberList(conv) });
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
export const convHandlers: Record<string, Handler> = {
|
|
@@ -3,6 +3,7 @@ import { respond } from '@metro-labs/core/stations/station-runtime';
|
|
|
3
3
|
import { ethIdentifiers, warmGroupName } from './conv-helpers.js';
|
|
4
4
|
import { mergeAppData, normalizeAssigned, readAppDataObject, type GroupLike } from './labels.js';
|
|
5
5
|
import { TrainError } from '@metro-labs/core/train-error';
|
|
6
|
+
import { syncConversation } from './network.js';
|
|
6
7
|
|
|
7
8
|
type Args = Record<string, unknown>;
|
|
8
9
|
|
|
@@ -73,7 +74,7 @@ async function applyChannelMeta(
|
|
|
73
74
|
`${verb} target is not a group (no updateAppData)`,
|
|
74
75
|
);
|
|
75
76
|
}
|
|
76
|
-
await conv
|
|
77
|
+
await syncConversation(acct.client, conv);
|
|
77
78
|
if (appData) mergeAppData(group.appData, appData);
|
|
78
79
|
else readAppDataObject(group.appData);
|
|
79
80
|
if (appData && Object.hasOwn(appData, 'assigned')) {
|
|
@@ -16,6 +16,7 @@ import { emitOutbound } from './emit.js';
|
|
|
16
16
|
import { CallSignalCodec, FrameCodec, PollCodec, buildPollContent, encodeDeleteMessage } from './codecs.js';
|
|
17
17
|
import { buildFrameContent } from './frames.js';
|
|
18
18
|
import { buildWalletContent } from './wallet.js';
|
|
19
|
+
import { network } from './network.js';
|
|
19
20
|
import { callLeftovers } from './call-leftovers.js';
|
|
20
21
|
import { convHandlers } from './actions-conv.js';
|
|
21
22
|
import { messagingAliases } from '@metro-labs/core/stations/messaging-normalize';
|
|
@@ -253,6 +254,11 @@ const handlers: Record<string, (id: string, args: Args) => Promise<void>> = {
|
|
|
253
254
|
export type { CallMsg };
|
|
254
255
|
|
|
255
256
|
export const handleCall = makeStation({
|
|
256
|
-
handlers,
|
|
257
|
+
handlers: Object.fromEntries(Object.entries(handlers).map(([action, handler]) => [
|
|
258
|
+
action,
|
|
259
|
+
action === 'accounts' || action === 'callLeftovers'
|
|
260
|
+
? handler
|
|
261
|
+
: (id: string, args: Args) => network.run(() => handler(id, args)),
|
|
262
|
+
])),
|
|
257
263
|
normalize: messagingAliases({ action: 'removed' }),
|
|
258
264
|
});
|
|
@@ -4,6 +4,7 @@ import { inboxEthCache, cacheInboxEth } from './wire.js';
|
|
|
4
4
|
import { TrainError } from '@metro-labs/core/train-error';
|
|
5
5
|
import type { MemberList, MetroMember } from '@metro-labs/core/stations/types';
|
|
6
6
|
import { readAppData, readAppDataObject, type GroupLike } from './labels.js';
|
|
7
|
+
import { network, syncConversation } from './network.js';
|
|
7
8
|
|
|
8
9
|
export { parseMemberArgs, resolveMembers } from './member-args.js';
|
|
9
10
|
|
|
@@ -67,7 +68,9 @@ export async function resolveAddresses(
|
|
|
67
68
|
cacheInboxEth(key, eth.identifier);
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
|
-
} catch {
|
|
71
|
+
} catch (error) {
|
|
72
|
+
network.note(error);
|
|
73
|
+
if (network.remaining() > 0) throw error;
|
|
71
74
|
return addresses;
|
|
72
75
|
}
|
|
73
76
|
return addresses;
|
|
@@ -78,11 +81,12 @@ export async function buildGroupInfo(
|
|
|
78
81
|
acct: Account,
|
|
79
82
|
conv: Conv,
|
|
80
83
|
): Promise<Record<string, unknown>> {
|
|
81
|
-
await conv
|
|
84
|
+
await syncConversation(acct.client, conv);
|
|
82
85
|
const rawAppData = (conv as unknown as GroupLike).appData ?? '';
|
|
83
86
|
const appData = readAppDataObject(rawAppData);
|
|
84
|
-
const
|
|
85
|
-
const
|
|
87
|
+
const members = await conv.members();
|
|
88
|
+
const inboxIds = members.map((m) => m.inboxId);
|
|
89
|
+
const addresses = memberAddresses(members);
|
|
86
90
|
const isDm =
|
|
87
91
|
typeof (conv as unknown as { peerInboxId?: unknown }).peerInboxId ===
|
|
88
92
|
'function';
|
|
@@ -114,6 +118,16 @@ export async function buildGroupInfo(
|
|
|
114
118
|
interface RawMember {
|
|
115
119
|
inboxId: string;
|
|
116
120
|
permissionLevel?: number;
|
|
121
|
+
accountIdentifiers?: EthId[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function memberAddresses(members: RawMember[]): Record<string, string> {
|
|
125
|
+
const addresses: Record<string, string> = {};
|
|
126
|
+
for (const member of members) {
|
|
127
|
+
const eth = member.accountIdentifiers?.find((id) => id.identifierKind === IdentifierKind.Ethereum);
|
|
128
|
+
if (eth) addresses[member.inboxId] = eth.identifier;
|
|
129
|
+
}
|
|
130
|
+
return addresses;
|
|
117
131
|
}
|
|
118
132
|
|
|
119
133
|
function toMetroMember(
|
|
@@ -128,15 +142,9 @@ function toMetroMember(
|
|
|
128
142
|
return member;
|
|
129
143
|
}
|
|
130
144
|
|
|
131
|
-
export async function buildMemberList(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
): Promise<MemberList> {
|
|
135
|
-
const raw = (await conv.members()) as RawMember[];
|
|
136
|
-
const addresses = await resolveAddresses(
|
|
137
|
-
acct,
|
|
138
|
-
raw.map((m) => m.inboxId),
|
|
139
|
-
);
|
|
145
|
+
export async function buildMemberList(conv: Conv): Promise<MemberList> {
|
|
146
|
+
const raw = await conv.members();
|
|
147
|
+
const addresses = memberAddresses(raw);
|
|
140
148
|
return {
|
|
141
149
|
members: raw.map((m) => toMetroMember(m, addresses)),
|
|
142
150
|
capability: { supported: true, complete: true, total: raw.length },
|
|
@@ -199,7 +207,7 @@ export async function applyMemberOp(
|
|
|
199
207
|
'INVALID_ARGS',
|
|
200
208
|
`${verb} target is not a group (no ${verb})`,
|
|
201
209
|
);
|
|
202
|
-
await group.sync?.()
|
|
210
|
+
await group.sync?.();
|
|
203
211
|
await applyByIdentifiers(group, byIdent, addrs, verb);
|
|
204
212
|
await applyByInboxId(group, byId, inboxes, verb);
|
|
205
213
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { errMsg } from '@metro-labs/core/log';
|
|
2
|
-
import {
|
|
2
|
+
import type { DecodedMessage } from '@xmtp/node-sdk';
|
|
3
3
|
import {
|
|
4
4
|
accounts,
|
|
5
5
|
bootAccount,
|
|
@@ -13,10 +13,10 @@ import { groupNameFor } from './conv-helpers.js';
|
|
|
13
13
|
import { senderFieldsNow } from './sender.js';
|
|
14
14
|
import { readCalls } from '@metro-labs/core/trains/protocol';
|
|
15
15
|
import { handleCall } from './actions.js';
|
|
16
|
+
import { streamMessages } from './stream.js';
|
|
16
17
|
|
|
17
18
|
readCalls('xmtp', handleCall);
|
|
18
19
|
|
|
19
|
-
const SYNC_MS = Number(process.env.XMTP_SYNC_MS ?? '60000');
|
|
20
20
|
const SILENT_TYPES = new Set([
|
|
21
21
|
'readReceipt',
|
|
22
22
|
'transactionReference',
|
|
@@ -26,35 +26,6 @@ const SILENT_TYPES = new Set([
|
|
|
26
26
|
'deleteRequest',
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
|
-
async function bootSync(acct: Account): Promise<void> {
|
|
30
|
-
const { id } = acct.cfg;
|
|
31
|
-
try {
|
|
32
|
-
await acct.client.conversations.syncAll([
|
|
33
|
-
ConsentState.Allowed,
|
|
34
|
-
ConsentState.Unknown,
|
|
35
|
-
]);
|
|
36
|
-
const initial = await acct.client.conversations.list();
|
|
37
|
-
process.stderr.write(
|
|
38
|
-
`xmtp[${id}]: synced ${initial.length} conversation(s) at boot\n`,
|
|
39
|
-
);
|
|
40
|
-
} catch (err) {
|
|
41
|
-
process.stderr.write(
|
|
42
|
-
`xmtp[${id}] boot sync error: ${errMsg(err)}\n`,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function startPeriodicSync(acct: Account): void {
|
|
48
|
-
const { id } = acct.cfg;
|
|
49
|
-
setInterval(() => {
|
|
50
|
-
acct.client.conversations
|
|
51
|
-
.syncAll([ConsentState.Allowed, ConsentState.Unknown])
|
|
52
|
-
.catch((err: unknown) => {
|
|
53
|
-
process.stderr.write(`xmtp[${id}] sync error: ${errMsg(err)}\n`);
|
|
54
|
-
});
|
|
55
|
-
}, SYNC_MS).unref();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
29
|
async function handleStreamMessage(
|
|
59
30
|
acct: Account,
|
|
60
31
|
msg: DecodedMessage,
|
|
@@ -74,28 +45,12 @@ async function handleStreamMessage(
|
|
|
74
45
|
}
|
|
75
46
|
|
|
76
47
|
async function runAccount(acct: Account): Promise<void> {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const stream = await acct.client.conversations.streamAllMessages({
|
|
84
|
-
consentStates: [ConsentState.Allowed, ConsentState.Unknown],
|
|
85
|
-
});
|
|
86
|
-
for await (const msg of stream) {
|
|
87
|
-
if (!msg) continue;
|
|
88
|
-
await handleStreamMessage(acct, msg).catch((err: unknown) => {
|
|
89
|
-
process.stderr.write(`xmtp[${id}] one message failed and was skipped: ${errMsg(err)}\n`);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
} catch (err) {
|
|
93
|
-
process.stderr.write(
|
|
94
|
-
`xmtp[${id}] stream error (retry 5s): ${errMsg(err)}\n`,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
98
|
-
}
|
|
48
|
+
await streamMessages(acct.client.conversations, async (msg) => {
|
|
49
|
+
if (!msg) return;
|
|
50
|
+
await handleStreamMessage(acct, msg).catch((err: unknown) => {
|
|
51
|
+
process.stderr.write(`xmtp[${acct.cfg.id}] one message failed and was skipped: ${errMsg(err)}\n`);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
99
54
|
}
|
|
100
55
|
|
|
101
56
|
const cfgs = loadAccounts();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { errMsg } from '@metro-labs/core/log';
|
|
2
|
+
import { TrainError } from '@metro-labs/core/train-error';
|
|
3
|
+
|
|
4
|
+
const RATE_DELAY_MS = 60_000;
|
|
5
|
+
const MAX_DELAY_MS = 300_000;
|
|
6
|
+
|
|
7
|
+
function recordOf(value: unknown): Record<string, unknown> {
|
|
8
|
+
return typeof value === 'object' && value !== null ? value as Record<string, unknown> : {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function retryAfter(error: unknown, now: number): number {
|
|
12
|
+
const fields = recordOf(error);
|
|
13
|
+
if (typeof fields.retryAfterMs === 'number' && Number.isFinite(fields.retryAfterMs))
|
|
14
|
+
return Math.max(0, fields.retryAfterMs);
|
|
15
|
+
const headers = fields.headers ?? recordOf(fields.response).headers ?? fields.metadata;
|
|
16
|
+
const get = recordOf(headers).get;
|
|
17
|
+
const raw: unknown = typeof get === 'function' ? get.call(headers, 'retry-after') : recordOf(headers)['retry-after'];
|
|
18
|
+
return retryDelay(Array.isArray(raw) ? raw[0] : raw, now);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function retryDelay(value: unknown, now: number): number {
|
|
22
|
+
if (typeof value !== 'string' && typeof value !== 'number') return 0;
|
|
23
|
+
const seconds = Number(value);
|
|
24
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
|
|
25
|
+
const date = Date.parse(String(value));
|
|
26
|
+
return Number.isFinite(date) ? Math.max(0, date - now) : 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function rateLimited(error: unknown): boolean {
|
|
30
|
+
const fields = recordOf(error);
|
|
31
|
+
return fields.code === 8 || fields.code === 429 || fields.status === 429 ||
|
|
32
|
+
/resource.?exhausted|resource has been exhausted|exceeds rate limit|\b429\b/i.test(errMsg(error));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class NetworkBackoff {
|
|
36
|
+
private until = 0;
|
|
37
|
+
private delay = RATE_DELAY_MS;
|
|
38
|
+
private reason = '';
|
|
39
|
+
|
|
40
|
+
constructor(private readonly now: () => number = Date.now) {}
|
|
41
|
+
|
|
42
|
+
remaining(): number {
|
|
43
|
+
return Math.max(0, this.until - this.now());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
note(error: unknown): void {
|
|
47
|
+
if (!rateLimited(error)) return;
|
|
48
|
+
const now = this.now();
|
|
49
|
+
if (now < this.until) {
|
|
50
|
+
this.until = Math.max(this.until, now + retryAfter(error, now));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (now - this.until >= MAX_DELAY_MS) this.delay = RATE_DELAY_MS;
|
|
54
|
+
this.until = now + Math.max(this.delay, retryAfter(error, now));
|
|
55
|
+
this.delay = Math.min(this.delay * 2, MAX_DELAY_MS);
|
|
56
|
+
this.reason = errMsg(error);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async run<T>(task: () => Promise<T>): Promise<T> {
|
|
60
|
+
const remaining = this.remaining();
|
|
61
|
+
if (remaining > 0)
|
|
62
|
+
throw new TrainError('xmtp_rate_limited', `XMTP is rate limited; retry in ${Math.ceil(remaining / 1000)}s. ${this.reason}`, {
|
|
63
|
+
retryable: true, retryAfterMs: remaining,
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
return await task();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.note(error);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const network = new NetworkBackoff();
|
|
75
|
+
|
|
76
|
+
export class SyncRequests {
|
|
77
|
+
private tail: Promise<unknown> = Promise.resolve();
|
|
78
|
+
private readonly pending = new WeakMap<object, Map<string, Promise<unknown>>>();
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
private readonly backoff: NetworkBackoff = network,
|
|
82
|
+
private readonly now: () => number = Date.now,
|
|
83
|
+
) {}
|
|
84
|
+
|
|
85
|
+
run(owner: object, id: string, task: () => Promise<unknown>): Promise<unknown> {
|
|
86
|
+
let requests = this.pending.get(owner);
|
|
87
|
+
if (!requests) {
|
|
88
|
+
requests = new Map();
|
|
89
|
+
this.pending.set(owner, requests);
|
|
90
|
+
}
|
|
91
|
+
const previous = requests.get(id);
|
|
92
|
+
if (previous) return previous;
|
|
93
|
+
const queuedAt = this.now();
|
|
94
|
+
const run = () => {
|
|
95
|
+
if (this.now() - queuedAt >= 30_000)
|
|
96
|
+
throw new TrainError('xmtp_sync_busy', 'XMTP sync waited 30s; retry the request.', { retryable: true });
|
|
97
|
+
return this.backoff.run(task);
|
|
98
|
+
};
|
|
99
|
+
const pending = this.tail.then(run, run).finally(() => { requests.delete(id); });
|
|
100
|
+
requests.set(id, pending);
|
|
101
|
+
this.tail = pending;
|
|
102
|
+
return pending;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const syncs = new SyncRequests();
|
|
107
|
+
|
|
108
|
+
export function syncConversation(owner: object, conv: { id: string; sync: () => Promise<unknown> }): Promise<unknown> {
|
|
109
|
+
return syncs.run(owner, conv.id, () => conv.sync());
|
|
110
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { errMsg } from '@metro-labs/core/log';
|
|
2
|
+
import { ConsentState } from '@xmtp/node-sdk';
|
|
3
|
+
import { network, type NetworkBackoff } from './network.js';
|
|
4
|
+
|
|
5
|
+
interface StreamOptions<T> {
|
|
6
|
+
consentStates: ConsentState[];
|
|
7
|
+
retryOnFail: false;
|
|
8
|
+
onError: (error: Error) => void;
|
|
9
|
+
onValue: (message: T) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface StreamSource<T> {
|
|
13
|
+
streamAllMessages: (options: StreamOptions<T>) => Promise<AsyncIterable<T>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface StreamRuntime {
|
|
17
|
+
backoff: NetworkBackoff;
|
|
18
|
+
now: () => number;
|
|
19
|
+
wait: (ms: number) => Promise<void>;
|
|
20
|
+
report: (message: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const defaultRuntime: StreamRuntime = {
|
|
24
|
+
backoff: network,
|
|
25
|
+
now: Date.now,
|
|
26
|
+
wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
27
|
+
report: (message) => { process.stderr.write(`xmtp: ${message}\n`); },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export async function streamMessages<T>(
|
|
31
|
+
source: StreamSource<T>,
|
|
32
|
+
handle: (message: T) => Promise<void>,
|
|
33
|
+
runtime: StreamRuntime = defaultRuntime,
|
|
34
|
+
): Promise<never> {
|
|
35
|
+
let delay = 5000;
|
|
36
|
+
for (;;) {
|
|
37
|
+
const started = runtime.now();
|
|
38
|
+
let delivery = Promise.resolve();
|
|
39
|
+
try {
|
|
40
|
+
const stream = await runtime.backoff.run(() => source.streamAllMessages({
|
|
41
|
+
consentStates: [ConsentState.Allowed, ConsentState.Unknown],
|
|
42
|
+
retryOnFail: false,
|
|
43
|
+
onValue: (message) => {
|
|
44
|
+
delivery = delivery.then(() => handle(message)).catch((error: unknown) => {
|
|
45
|
+
runtime.report(`message handler failed: ${errMsg(error)}`);
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
onError: (error) => {
|
|
49
|
+
runtime.backoff.note(error);
|
|
50
|
+
runtime.report(`stream error: ${errMsg(error)}`);
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
54
|
+
while (!(await iterator.next()).done) await delivery;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
runtime.backoff.note(error);
|
|
57
|
+
runtime.report(`stream error: ${errMsg(error)}`);
|
|
58
|
+
}
|
|
59
|
+
await delivery;
|
|
60
|
+
if (runtime.now() - started >= 60_000) delay = 5000;
|
|
61
|
+
const wait = Math.max(delay, runtime.backoff.remaining());
|
|
62
|
+
runtime.report(`stream ended; retry in ${Math.ceil(wait / 1000)}s`);
|
|
63
|
+
await runtime.wait(wait);
|
|
64
|
+
delay = Math.min(delay * 2, 300_000);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/runtime/runtime.json
CHANGED