samva 0.10.0 → 0.12.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 +59 -9
- package/dist/client/sdk.gen.d.ts +1126 -219
- package/dist/client/types.gen.d.ts +11655 -6311
- package/dist/{client-mI4S99Ts.js → client-Cj1bqIEe.js} +5169 -1639
- package/dist/{client-attribution-Bl4nTEsE.js → client-attribution-BTWYSXxo.js} +2 -2
- package/dist/effect/analytics.d.ts +18 -18
- package/dist/effect/analytics.js +1 -1
- package/dist/effect/api-keys.d.ts +99 -43
- package/dist/effect/api-keys.js +2 -4
- package/dist/effect/attachments.d.ts +5 -6
- package/dist/effect/attachments.js +1 -1
- package/dist/effect/brands.d.ts +4 -4
- package/dist/effect/brands.js +2 -2
- package/dist/effect/campaigns.d.ts +30 -60
- package/dist/effect/campaigns.js +1 -1
- package/dist/effect/client.js +1 -1
- package/dist/effect/contact-groups.d.ts +29 -46
- package/dist/effect/contact-groups.js +1 -1
- package/dist/effect/contacts.d.ts +324 -726
- package/dist/effect/contacts.js +10 -2
- package/dist/effect/conversations.d.ts +575 -1130
- package/dist/effect/conversations.js +14 -2
- package/dist/effect/custom-fields.d.ts +3 -53
- package/dist/effect/custom-fields.js +1 -1
- package/dist/effect/email.d.ts +349 -298
- package/dist/effect/email.js +9 -3
- package/dist/effect/extension-installations.d.ts +218 -0
- package/dist/effect/extension-installations.js +25 -0
- package/dist/effect/extensions.d.ts +192 -0
- package/dist/effect/extensions.js +19 -0
- package/dist/effect/feedback.d.ts +12 -0
- package/dist/effect/feedback.js +13 -0
- package/dist/effect/generated.d.ts +33011 -15519
- package/dist/effect/index.d.ts +6 -1
- package/dist/effect/index.js +7 -2
- package/dist/effect/internal/client.d.ts +3 -0
- package/dist/effect/internal/mailbox-events.d.ts +11 -0
- package/dist/effect/internal/operation.d.ts +20 -374
- package/dist/effect/mailboxes.d.ts +1676 -0
- package/dist/effect/mailboxes.js +3 -0
- package/dist/effect/media.d.ts +5 -5
- package/dist/effect/media.js +1 -1
- package/dist/effect/messages.d.ts +171 -938
- package/dist/effect/messages.js +6 -2
- package/dist/effect/namespaces.d.ts +82 -0
- package/dist/effect/namespaces.js +19 -0
- package/dist/effect/operations.d.ts +68 -12
- package/dist/effect/operations.js +1 -1
- package/dist/effect/organizations.d.ts +5 -4
- package/dist/effect/organizations.js +1 -1
- package/dist/effect/pagination.d.ts +16 -21
- package/dist/effect/scheduled-messages.d.ts +13 -23
- package/dist/effect/scheduled-messages.js +1 -1
- package/dist/effect/templates.d.ts +204 -281
- package/dist/effect/templates.js +2 -2
- package/dist/effect/unsubscribe-groups.d.ts +21 -75
- package/dist/effect/unsubscribe-groups.js +1 -1
- package/dist/effect/webhooks.d.ts +117 -39
- package/dist/effect/webhooks.js +2 -2
- package/dist/index.js +3 -2
- package/dist/mailbox-events-CsbF35vH.js +629 -0
- package/dist/mailbox-events.d.ts +178 -0
- package/dist/mailboxes-BtcktcNB.js +147 -0
- package/dist/{operation-DQWJO4jY.js → operation-BEghYyLu.js} +411 -67
- package/dist/promises/errors.gen.d.ts +5 -0
- package/dist/promises/index.d.ts +23 -1
- package/dist/promises/index.js +3 -2
- package/dist/promises/types.gen.d.ts +1 -1
- package/dist/{promises-C7D4oaHn.js → promises-BzrwhHEW.js} +3273 -547
- package/dist/service-map.d.ts +468 -36
- package/package.json +22 -2
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
//#region src/mailbox-events.ts
|
|
2
|
+
/**
|
|
3
|
+
* The mailbox event stream: the `mailbox.*` events a webhook endpoint receives, delivered over a
|
|
4
|
+
* WebSocket to a program that has no public URL for webhooks to reach.
|
|
5
|
+
*
|
|
6
|
+
* {@link MailboxEventStream} is an async iterable. The loop body handling an event is the
|
|
7
|
+
* acknowledgement: asking for the next event marks the previous one handled, and the stream
|
|
8
|
+
* resumes from the last handled event whenever it reconnects. Transient closes reconnect by
|
|
9
|
+
* themselves; a close the caller has to act on ends the loop with a {@link MailboxEventStreamError}.
|
|
10
|
+
*
|
|
11
|
+
* This module has no runtime dependencies so the Promise and Effect clients share it.
|
|
12
|
+
*/
|
|
13
|
+
/** The WebSocket path on the events host. */
|
|
14
|
+
const MAILBOX_EVENT_STREAM_PATH = "/v1/mailboxes/events";
|
|
15
|
+
/** Close codes the events host ends an accepted connection with. */
|
|
16
|
+
const MailboxEventStreamCloseCode = {
|
|
17
|
+
/** The client sent a frame that is not an acknowledgement of a cursor it was sent. */
|
|
18
|
+
invalidFrame: 4400,
|
|
19
|
+
/** The credential stopped authenticating: revoked, rotated, or expired. */
|
|
20
|
+
unauthorized: 4401,
|
|
21
|
+
/** The credential authenticates but may no longer read mailboxes. */
|
|
22
|
+
forbidden: 4403,
|
|
23
|
+
/** Mailboxes are unavailable to the organization. */
|
|
24
|
+
gone: 4404,
|
|
25
|
+
/** Too many events went unacknowledged. Reconnecting from the last handled cursor recovers. */
|
|
26
|
+
slowConsumer: 4408,
|
|
27
|
+
/** Events after the cursor are no longer retained. Read current state and start without one. */
|
|
28
|
+
cursorExpired: 4410,
|
|
29
|
+
/** Access could not be confirmed for a while. Reconnecting from the last cursor recovers. */
|
|
30
|
+
accessUnavailable: 4503
|
|
31
|
+
};
|
|
32
|
+
/** The refusal tags the events host answers with, keyed by the status that carries them. */
|
|
33
|
+
const REFUSAL_TAG_BY_STATUS = {
|
|
34
|
+
401: "UnauthorizedError",
|
|
35
|
+
403: "ForbiddenError",
|
|
36
|
+
404: "ResourceNotFoundError",
|
|
37
|
+
405: "MethodNotAllowedError",
|
|
38
|
+
410: "CursorExpiredError",
|
|
39
|
+
422: "ValidationError",
|
|
40
|
+
426: "UpgradeRequiredError",
|
|
41
|
+
429: "RateLimitedError",
|
|
42
|
+
503: "ServiceUnavailableError"
|
|
43
|
+
};
|
|
44
|
+
const REFUSAL_TAGS = new Set(Object.values(REFUSAL_TAG_BY_STATUS));
|
|
45
|
+
/**
|
|
46
|
+
* A stream that ended. `_tag` names the cause and `message` says what to do next, as the host
|
|
47
|
+
* wrote it when the host refused. Branch on `_tag`; in the Effect client the stream fails with
|
|
48
|
+
* {@link MailboxEventStreamFailure}, which `Effect.catchTag` narrows by `_tag`.
|
|
49
|
+
*
|
|
50
|
+
* The stream retries by itself only where retrying can help: a transient close, an unreachable
|
|
51
|
+
* host, a `408`, a `429`, or a `5xx`. Every other refusal ends it, so a caller never loops on a
|
|
52
|
+
* request the host will keep refusing.
|
|
53
|
+
*/
|
|
54
|
+
var MailboxEventStreamError = class extends Error {
|
|
55
|
+
name = "MailboxEventStreamError";
|
|
56
|
+
_tag;
|
|
57
|
+
/** The HTTP status, when the host refused the connection before accepting it. */
|
|
58
|
+
status;
|
|
59
|
+
/** The WebSocket close code, when the host closed an accepted connection. */
|
|
60
|
+
closeCode;
|
|
61
|
+
/** The last handled cursor; reconnect with it unless `_tag` is `CursorExpiredError`. */
|
|
62
|
+
cursor;
|
|
63
|
+
/** A `ValidationError`'s field messages, such as `fields.cursor` or `fields.mailboxId`. */
|
|
64
|
+
fields;
|
|
65
|
+
/** How long a `RateLimitedError` or `ServiceUnavailableError` asked the caller to wait. */
|
|
66
|
+
retryAfterSeconds;
|
|
67
|
+
/** The tag the host sent, when it is not one this client maps. */
|
|
68
|
+
serverTag;
|
|
69
|
+
constructor(input) {
|
|
70
|
+
super(input.message);
|
|
71
|
+
this._tag = input._tag;
|
|
72
|
+
this.status = input.status;
|
|
73
|
+
this.closeCode = input.closeCode;
|
|
74
|
+
this.cursor = input.cursor;
|
|
75
|
+
this.fields = input.fields;
|
|
76
|
+
this.retryAfterSeconds = input.retryAfterSeconds;
|
|
77
|
+
this.serverTag = input.serverTag;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const OPEN = 1;
|
|
81
|
+
/** Acknowledge after this many handled events, well inside the server's window of 256. */
|
|
82
|
+
const ACK_EVERY = 32;
|
|
83
|
+
const RETRY_BASE_MILLIS = 500;
|
|
84
|
+
const RETRY_MAX_MILLIS = 3e4;
|
|
85
|
+
/** The longest wait a host's `retryAfterSeconds` is honored for. */
|
|
86
|
+
const RETRY_AFTER_MAX_MILLIS = 3e5;
|
|
87
|
+
/** The largest refusal body read; the host's error bodies are a few hundred bytes. */
|
|
88
|
+
const REFUSAL_BODY_LIMIT = 65536;
|
|
89
|
+
/** The longest the diagnosing request and its body may take before the refusal is treated as retryable. */
|
|
90
|
+
const DIAGNOSE_TIMEOUT_MILLIS = 1e4;
|
|
91
|
+
/** The frame that asks the events host whether the connection is alive; pinned to the host's by a test. */
|
|
92
|
+
const MAILBOX_EVENT_STREAM_PING_FRAME = "{\"kind\":\"ping\"}";
|
|
93
|
+
/** How often the stream sends a ping; pinned to the host's constant by a test. */
|
|
94
|
+
const MAILBOX_EVENT_STREAM_PING_MILLIS = 3e4;
|
|
95
|
+
/**
|
|
96
|
+
* How long a connection may stay silent before it is treated as dropped: three ping intervals, so
|
|
97
|
+
* one late or lost pong does not reconnect. The host sends nothing on a quiet mailbox, so the
|
|
98
|
+
* pong is the only proof of life; a socket that died without a close event is otherwise
|
|
99
|
+
* indistinguishable from a quiet mailbox.
|
|
100
|
+
*/
|
|
101
|
+
const MAILBOX_EVENT_STREAM_IDLE_MILLIS = 3 * MAILBOX_EVENT_STREAM_PING_MILLIS;
|
|
102
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
103
|
+
const frameText = (raw) => {
|
|
104
|
+
if (typeof raw === "string") return raw;
|
|
105
|
+
if (raw instanceof ArrayBuffer || ArrayBuffer.isView(raw)) return new TextDecoder().decode(raw);
|
|
106
|
+
};
|
|
107
|
+
const decodeFrame = (raw) => {
|
|
108
|
+
const text = frameText(raw);
|
|
109
|
+
if (text === void 0) return void 0;
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(text);
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (isRecord(parsed) && parsed.kind === "pong") return { kind: "pong" };
|
|
117
|
+
if (!isRecord(parsed) || typeof parsed.cursor !== "string") return void 0;
|
|
118
|
+
if (parsed.kind === "live" || parsed.kind === "progress") return {
|
|
119
|
+
kind: parsed.kind,
|
|
120
|
+
cursor: parsed.cursor
|
|
121
|
+
};
|
|
122
|
+
if (parsed.kind !== "event" || typeof parsed.id !== "string" || !isRecord(parsed.event)) return;
|
|
123
|
+
const { type, timestamp, data } = parsed.event;
|
|
124
|
+
if (typeof type !== "string" || typeof timestamp !== "string") return void 0;
|
|
125
|
+
return {
|
|
126
|
+
kind: "event",
|
|
127
|
+
cursor: parsed.cursor,
|
|
128
|
+
id: parsed.id,
|
|
129
|
+
event: {
|
|
130
|
+
type,
|
|
131
|
+
timestamp,
|
|
132
|
+
data
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
/** The events URL for an API origin: `https://api.samva.dev` → `wss://events.samva.dev/v1/…`. */
|
|
137
|
+
const mailboxEventsUrl = (connection, options) => {
|
|
138
|
+
const invalid = (message) => new MailboxEventStreamError({
|
|
139
|
+
_tag: "StreamClientError",
|
|
140
|
+
message
|
|
141
|
+
});
|
|
142
|
+
const url = URL.parse(options.eventsUrl ?? connection.baseUrl);
|
|
143
|
+
if (url === null) throw invalid("The events origin is not a valid URL.");
|
|
144
|
+
if (url.username !== "" || url.password !== "") throw invalid("The events origin must not contain credentials.");
|
|
145
|
+
if (options.eventsUrl === void 0) {
|
|
146
|
+
const labels = url.hostname.split(".");
|
|
147
|
+
const api = labels.indexOf("api");
|
|
148
|
+
if (api === -1) throw invalid("The API origin has no `api` host label; pass `eventsUrl`.");
|
|
149
|
+
labels[api] = "events";
|
|
150
|
+
url.hostname = labels.join(".");
|
|
151
|
+
}
|
|
152
|
+
if (url.protocol === "https:") url.protocol = "wss:";
|
|
153
|
+
else if (url.protocol === "http:") url.protocol = "ws:";
|
|
154
|
+
if (url.protocol !== "wss:" && url.protocol !== "ws:") throw invalid("The events origin must be an http, https, ws, or wss URL.");
|
|
155
|
+
url.pathname = MAILBOX_EVENT_STREAM_PATH;
|
|
156
|
+
url.search = "";
|
|
157
|
+
url.hash = "";
|
|
158
|
+
if (options.cursor !== void 0) url.searchParams.set("cursor", options.cursor);
|
|
159
|
+
for (const id of options.mailboxIds ?? []) url.searchParams.append("mailboxId", id);
|
|
160
|
+
return url;
|
|
161
|
+
};
|
|
162
|
+
const DESCRIPTION = {
|
|
163
|
+
UnauthorizedError: "The credential is not valid. Sign in again or use an active API key.",
|
|
164
|
+
ForbiddenError: "The credential may not read mailbox events.",
|
|
165
|
+
ResourceNotFoundError: "The mailbox event stream or a requested mailbox was not found; mailboxes may not be enabled for the organization.",
|
|
166
|
+
CursorExpiredError: "Events after the cursor are no longer retained. Read current state from the API and start without a cursor.",
|
|
167
|
+
ValidationError: "The stream request was not valid.",
|
|
168
|
+
UpgradeRequiredError: "The events host expects a WebSocket upgrade.",
|
|
169
|
+
RateLimitedError: "Too many streams are open. Close one or wait before connecting.",
|
|
170
|
+
ServiceUnavailableError: "The events host could not confirm access. Retry shortly.",
|
|
171
|
+
MethodNotAllowedError: "The events host accepts GET with a WebSocket upgrade.",
|
|
172
|
+
StreamDisconnectedError: "The mailbox event stream disconnected.",
|
|
173
|
+
StreamClientError: "The stream request could not be made.",
|
|
174
|
+
StreamProtocolError: "The events host sent a frame this client does not understand.",
|
|
175
|
+
StreamRefusedError: "The events host refused the connection."
|
|
176
|
+
};
|
|
177
|
+
const outcomeOf = (_tag, retry, detail = {}) => {
|
|
178
|
+
const { message, ...rest } = detail;
|
|
179
|
+
return {
|
|
180
|
+
retry,
|
|
181
|
+
retryAfterMillis: detail.retryAfterSeconds === void 0 ? void 0 : Math.min(RETRY_AFTER_MAX_MILLIS, detail.retryAfterSeconds * 1e3),
|
|
182
|
+
error: new MailboxEventStreamError({
|
|
183
|
+
_tag,
|
|
184
|
+
message: message ?? DESCRIPTION[_tag],
|
|
185
|
+
...rest
|
|
186
|
+
})
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
/** What a close means for the stream: the error it ends with, and whether to retry first. */
|
|
190
|
+
const closeOutcome = (closeCode) => {
|
|
191
|
+
switch (closeCode) {
|
|
192
|
+
case MailboxEventStreamCloseCode.invalidFrame: return outcomeOf("StreamClientError", false, {
|
|
193
|
+
closeCode,
|
|
194
|
+
message: "The events host rejected a frame this client sent."
|
|
195
|
+
});
|
|
196
|
+
case MailboxEventStreamCloseCode.unauthorized: return outcomeOf("UnauthorizedError", false, { closeCode });
|
|
197
|
+
case MailboxEventStreamCloseCode.forbidden: return outcomeOf("ForbiddenError", false, { closeCode });
|
|
198
|
+
case MailboxEventStreamCloseCode.gone: return outcomeOf("ResourceNotFoundError", false, { closeCode });
|
|
199
|
+
case MailboxEventStreamCloseCode.cursorExpired: return outcomeOf("CursorExpiredError", false, { closeCode });
|
|
200
|
+
case MailboxEventStreamCloseCode.accessUnavailable: return outcomeOf("ServiceUnavailableError", true, { closeCode });
|
|
201
|
+
default: return outcomeOf("StreamDisconnectedError", true, { closeCode });
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const fieldsOf = (value) => {
|
|
205
|
+
if (!isRecord(value)) return void 0;
|
|
206
|
+
const fields = {};
|
|
207
|
+
for (const [path, messages] of Object.entries(value)) if (Array.isArray(messages)) fields[path] = messages.filter((message) => typeof message === "string");
|
|
208
|
+
return fields;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* What a refusal means for the stream. Every 4xx except `408` and `429` ends it: the host will
|
|
212
|
+
* keep refusing the same request. `5xx`, `408`, and `429` retry, after the delay the host asked
|
|
213
|
+
* for when it asked for one.
|
|
214
|
+
*/
|
|
215
|
+
const refusalOutcome = (refusal) => {
|
|
216
|
+
const body = isRecord(refusal.body) ? refusal.body : {};
|
|
217
|
+
const serverTag = typeof body._tag === "string" ? body._tag : void 0;
|
|
218
|
+
const known = serverTag !== void 0 && REFUSAL_TAGS.has(serverTag);
|
|
219
|
+
const _tag = known ? serverTag : REFUSAL_TAG_BY_STATUS[refusal.status] ?? "StreamRefusedError";
|
|
220
|
+
const retryAfterSeconds = typeof body.retryAfterSeconds === "number" && body.retryAfterSeconds > 0 ? body.retryAfterSeconds : refusal.retryAfterSeconds;
|
|
221
|
+
const retry = refusal.status >= 500 || refusal.status === 408 || refusal.status === 429;
|
|
222
|
+
return outcomeOf(_tag, retry, {
|
|
223
|
+
status: refusal.status,
|
|
224
|
+
message: typeof body.message === "string" ? body.message : void 0,
|
|
225
|
+
fields: fieldsOf(body.fields),
|
|
226
|
+
retryAfterSeconds,
|
|
227
|
+
serverTag: known ? void 0 : serverTag
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* The runtime's WebSocket. The standard constructor's type omits the headers option Node and Bun
|
|
232
|
+
* accept, so it is read as the narrower constructor the stream calls.
|
|
233
|
+
*/
|
|
234
|
+
const globalWebSocket = () => {
|
|
235
|
+
const candidate = Reflect.get(globalThis, "WebSocket");
|
|
236
|
+
return typeof candidate === "function" ? candidate : void 0;
|
|
237
|
+
};
|
|
238
|
+
const sleep = (millis, signal) => new Promise((resolve) => {
|
|
239
|
+
if (signal.aborted) return resolve();
|
|
240
|
+
const timer = setTimeout(done, millis);
|
|
241
|
+
function done() {
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
signal.removeEventListener("abort", done);
|
|
244
|
+
resolve();
|
|
245
|
+
}
|
|
246
|
+
signal.addEventListener("abort", done, { once: true });
|
|
247
|
+
});
|
|
248
|
+
/**
|
|
249
|
+
* A resumable stream of mailbox events. Iterate it with `for await`; break out of the loop or call
|
|
250
|
+
* {@link close} to end it.
|
|
251
|
+
*/
|
|
252
|
+
var MailboxEventStream = class {
|
|
253
|
+
#connection;
|
|
254
|
+
#options;
|
|
255
|
+
#stop = new AbortController();
|
|
256
|
+
#queue = [];
|
|
257
|
+
#waiting;
|
|
258
|
+
/** A reconnect waiting for the caller to drain what the last connection buffered. */
|
|
259
|
+
#draining;
|
|
260
|
+
#socket;
|
|
261
|
+
#error;
|
|
262
|
+
#ended = false;
|
|
263
|
+
/** The caller ended the stream: nothing more is handed out, buffered or not. */
|
|
264
|
+
#closed = false;
|
|
265
|
+
#started = false;
|
|
266
|
+
/** The caller's signal and the listener on it, detached once the stream ends. */
|
|
267
|
+
#callerSignal;
|
|
268
|
+
#onCallerAbort = () => this.close();
|
|
269
|
+
/** The last event handed to the caller, handled once the caller asks for the next one. */
|
|
270
|
+
#inHand;
|
|
271
|
+
#cursor;
|
|
272
|
+
#unacked = 0;
|
|
273
|
+
constructor(connection, options = {}) {
|
|
274
|
+
this.#connection = connection;
|
|
275
|
+
this.#options = options;
|
|
276
|
+
this.#cursor = options.cursor;
|
|
277
|
+
mailboxEventsUrl(connection, options);
|
|
278
|
+
if (options.signal?.aborted) this.close();
|
|
279
|
+
else {
|
|
280
|
+
this.#callerSignal = options.signal;
|
|
281
|
+
options.signal?.addEventListener("abort", this.#onCallerAbort, { once: true });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The position to resume from: the last handled event's cursor, or a later position the
|
|
286
|
+
* server confirmed with nothing in between for this stream. Undefined until there is one.
|
|
287
|
+
*/
|
|
288
|
+
get cursor() {
|
|
289
|
+
return this.#cursor;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Mark the event in hand handled now, before asking for the next one: for a caller that stops
|
|
293
|
+
* after handling it, so {@link close} acknowledges it and {@link cursor} moves past it.
|
|
294
|
+
*/
|
|
295
|
+
markHandled() {
|
|
296
|
+
this.#settleInHand();
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* End the stream. Handled events are acknowledged; the event in hand is not, unless
|
|
300
|
+
* {@link markHandled} was called, so a resume repeats it. A pending iteration finishes.
|
|
301
|
+
*/
|
|
302
|
+
close() {
|
|
303
|
+
if (this.#ended) return;
|
|
304
|
+
this.#ack();
|
|
305
|
+
this.#ended = true;
|
|
306
|
+
this.#closed = true;
|
|
307
|
+
this.#callerSignal?.removeEventListener("abort", this.#onCallerAbort);
|
|
308
|
+
this.#socket?.close(1e3);
|
|
309
|
+
this.#stop.abort();
|
|
310
|
+
this.#wake();
|
|
311
|
+
}
|
|
312
|
+
[Symbol.asyncIterator]() {
|
|
313
|
+
return {
|
|
314
|
+
next: () => this.#next(),
|
|
315
|
+
return: async () => {
|
|
316
|
+
this.close();
|
|
317
|
+
return {
|
|
318
|
+
done: true,
|
|
319
|
+
value: void 0
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async #next() {
|
|
325
|
+
this.#settleInHand();
|
|
326
|
+
if (!this.#started) {
|
|
327
|
+
this.#started = true;
|
|
328
|
+
this.#run().catch((cause) => this.#fail(new MailboxEventStreamError({
|
|
329
|
+
_tag: "StreamDisconnectedError",
|
|
330
|
+
message: `The mailbox event stream failed: ${String(cause)}`,
|
|
331
|
+
cursor: this.#cursor
|
|
332
|
+
})));
|
|
333
|
+
}
|
|
334
|
+
for (;;) {
|
|
335
|
+
if (this.#closed) return {
|
|
336
|
+
done: true,
|
|
337
|
+
value: void 0
|
|
338
|
+
};
|
|
339
|
+
const event = this.#queue.shift();
|
|
340
|
+
if (event !== void 0) {
|
|
341
|
+
this.#inHand = event;
|
|
342
|
+
return {
|
|
343
|
+
done: false,
|
|
344
|
+
value: event
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const draining = this.#draining;
|
|
348
|
+
this.#draining = void 0;
|
|
349
|
+
draining?.resolve();
|
|
350
|
+
this.#ack();
|
|
351
|
+
if (this.#error !== void 0) throw this.#errorAtCursor(this.#error);
|
|
352
|
+
if (this.#ended) return {
|
|
353
|
+
done: true,
|
|
354
|
+
value: void 0
|
|
355
|
+
};
|
|
356
|
+
await new Promise((resolve) => {
|
|
357
|
+
this.#waiting = { resolve };
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* The failure as of the last handled event. Events buffered before the failure are still handed
|
|
363
|
+
* out, so the cursor recorded when the connection ended can trail the one to resume from.
|
|
364
|
+
*/
|
|
365
|
+
#errorAtCursor(error) {
|
|
366
|
+
if (error.cursor === this.#cursor) return error;
|
|
367
|
+
return this.#withCursor(error);
|
|
368
|
+
}
|
|
369
|
+
#settleInHand() {
|
|
370
|
+
if (this.#inHand === void 0) return;
|
|
371
|
+
this.#cursor = this.#inHand.cursor;
|
|
372
|
+
this.#inHand = void 0;
|
|
373
|
+
this.#unacked += 1;
|
|
374
|
+
if (this.#unacked >= ACK_EVERY) this.#ack();
|
|
375
|
+
}
|
|
376
|
+
#ack() {
|
|
377
|
+
const socket = this.#socket;
|
|
378
|
+
if (this.#unacked === 0 || this.#cursor === void 0 || socket?.readyState !== OPEN) return;
|
|
379
|
+
socket.send(JSON.stringify({
|
|
380
|
+
kind: "ack",
|
|
381
|
+
cursor: this.#cursor
|
|
382
|
+
}));
|
|
383
|
+
this.#unacked = 0;
|
|
384
|
+
}
|
|
385
|
+
/** Where a new connection starts: after every event this stream already holds. */
|
|
386
|
+
#resumeCursor() {
|
|
387
|
+
return this.#queue.at(-1)?.cursor ?? this.#inHand?.cursor ?? this.#cursor;
|
|
388
|
+
}
|
|
389
|
+
#wake() {
|
|
390
|
+
const waiting = this.#waiting;
|
|
391
|
+
this.#waiting = void 0;
|
|
392
|
+
waiting?.resolve();
|
|
393
|
+
}
|
|
394
|
+
#fail(error) {
|
|
395
|
+
if (this.#ended) return;
|
|
396
|
+
this.#error = error;
|
|
397
|
+
this.#ended = true;
|
|
398
|
+
this.#callerSignal?.removeEventListener("abort", this.#onCallerAbort);
|
|
399
|
+
this.#socket?.close(1e3);
|
|
400
|
+
this.#stop.abort();
|
|
401
|
+
this.#wake();
|
|
402
|
+
}
|
|
403
|
+
async #run() {
|
|
404
|
+
let attempt = 0;
|
|
405
|
+
while (!this.#ended) {
|
|
406
|
+
if (this.#queue.length > 0) {
|
|
407
|
+
await new Promise((resolve) => {
|
|
408
|
+
const done = () => {
|
|
409
|
+
this.#stop.signal.removeEventListener("abort", done);
|
|
410
|
+
resolve();
|
|
411
|
+
};
|
|
412
|
+
this.#draining = { resolve: done };
|
|
413
|
+
this.#stop.signal.addEventListener("abort", done, { once: true });
|
|
414
|
+
});
|
|
415
|
+
if (this.#ended) return;
|
|
416
|
+
}
|
|
417
|
+
const outcome = await this.#connect(() => {
|
|
418
|
+
attempt = 0;
|
|
419
|
+
});
|
|
420
|
+
if (this.#ended || outcome.kind === "stopped") return;
|
|
421
|
+
let result;
|
|
422
|
+
if (outcome.kind === "closed") result = closeOutcome(outcome.code);
|
|
423
|
+
else {
|
|
424
|
+
const refusal = await this.#diagnose();
|
|
425
|
+
result = refusal === void 0 ? outcomeOf("StreamDisconnectedError", true) : refusalOutcome(refusal);
|
|
426
|
+
}
|
|
427
|
+
if (!result.retry || this.#options.reconnect === false) {
|
|
428
|
+
this.#fail(this.#withCursor(result.error));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const backoff = Math.min(RETRY_MAX_MILLIS, RETRY_BASE_MILLIS * 2 ** attempt);
|
|
432
|
+
attempt += 1;
|
|
433
|
+
await sleep(result.retryAfterMillis ?? backoff / 2 + Math.random() * (backoff / 2), this.#stop.signal);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
#withCursor(error) {
|
|
437
|
+
return new MailboxEventStreamError({
|
|
438
|
+
...error,
|
|
439
|
+
message: error.message,
|
|
440
|
+
cursor: this.#cursor
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/** One connection, from upgrade to close. `onLive` runs once replay has caught up. */
|
|
444
|
+
#connect(onLive) {
|
|
445
|
+
return new Promise((resolve) => {
|
|
446
|
+
const WebSocketImplementation = this.#options.WebSocket ?? globalWebSocket();
|
|
447
|
+
if (WebSocketImplementation === void 0) {
|
|
448
|
+
this.#fail(new MailboxEventStreamError({
|
|
449
|
+
_tag: "StreamClientError",
|
|
450
|
+
message: "This runtime has no WebSocket; pass one as the `WebSocket` option."
|
|
451
|
+
}));
|
|
452
|
+
return resolve({ kind: "stopped" });
|
|
453
|
+
}
|
|
454
|
+
const url = mailboxEventsUrl(this.#connection, {
|
|
455
|
+
...this.#options,
|
|
456
|
+
cursor: this.#resumeCursor()
|
|
457
|
+
});
|
|
458
|
+
let opened = false;
|
|
459
|
+
let settled = false;
|
|
460
|
+
let idleTimer;
|
|
461
|
+
let pingTimer;
|
|
462
|
+
const armIdle = () => {
|
|
463
|
+
clearTimeout(idleTimer);
|
|
464
|
+
idleTimer = setTimeout(() => {
|
|
465
|
+
settle(opened ? {
|
|
466
|
+
kind: "closed",
|
|
467
|
+
code: 1006
|
|
468
|
+
} : { kind: "refused" });
|
|
469
|
+
socket.close(1e3);
|
|
470
|
+
}, MAILBOX_EVENT_STREAM_IDLE_MILLIS);
|
|
471
|
+
};
|
|
472
|
+
const settle = (result) => {
|
|
473
|
+
if (settled) return;
|
|
474
|
+
settled = true;
|
|
475
|
+
clearTimeout(idleTimer);
|
|
476
|
+
clearInterval(pingTimer);
|
|
477
|
+
this.#stop.signal.removeEventListener("abort", onStop);
|
|
478
|
+
if (this.#socket === socket) this.#socket = void 0;
|
|
479
|
+
resolve(result);
|
|
480
|
+
};
|
|
481
|
+
const onStop = () => settle({ kind: "stopped" });
|
|
482
|
+
let socket;
|
|
483
|
+
try {
|
|
484
|
+
socket = new WebSocketImplementation(url.toString(), { headers: this.#connection.headers });
|
|
485
|
+
} catch (cause) {
|
|
486
|
+
this.#fail(new MailboxEventStreamError({
|
|
487
|
+
_tag: "StreamClientError",
|
|
488
|
+
message: `This runtime's WebSocket cannot send request headers; pass one that can as the \`WebSocket\` option. ${String(cause)}`,
|
|
489
|
+
cursor: this.#cursor
|
|
490
|
+
}));
|
|
491
|
+
return resolve({ kind: "stopped" });
|
|
492
|
+
}
|
|
493
|
+
this.#socket = socket;
|
|
494
|
+
this.#stop.signal.addEventListener("abort", onStop, { once: true });
|
|
495
|
+
armIdle();
|
|
496
|
+
socket.addEventListener("open", () => {
|
|
497
|
+
opened = true;
|
|
498
|
+
pingTimer = setInterval(() => {
|
|
499
|
+
if (socket.readyState === OPEN) socket.send(MAILBOX_EVENT_STREAM_PING_FRAME);
|
|
500
|
+
}, MAILBOX_EVENT_STREAM_PING_MILLIS);
|
|
501
|
+
});
|
|
502
|
+
socket.addEventListener("message", (message) => {
|
|
503
|
+
if (this.#ended) return;
|
|
504
|
+
if (!settled) armIdle();
|
|
505
|
+
const frame = decodeFrame(message.data);
|
|
506
|
+
if (frame === void 0) {
|
|
507
|
+
this.#fail(new MailboxEventStreamError({
|
|
508
|
+
_tag: "StreamProtocolError",
|
|
509
|
+
message: DESCRIPTION.StreamProtocolError,
|
|
510
|
+
cursor: this.#cursor
|
|
511
|
+
}));
|
|
512
|
+
socket.close(1e3);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (frame.kind === "pong") return;
|
|
516
|
+
if (frame.kind === "event") {
|
|
517
|
+
this.#queue.push({
|
|
518
|
+
cursor: frame.cursor,
|
|
519
|
+
id: frame.id,
|
|
520
|
+
type: frame.event.type,
|
|
521
|
+
timestamp: frame.event.timestamp,
|
|
522
|
+
data: isRecord(frame.event.data) ? frame.event.data : {}
|
|
523
|
+
});
|
|
524
|
+
this.#wake();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (frame.kind === "live") {
|
|
528
|
+
onLive();
|
|
529
|
+
this.#options.onLive?.();
|
|
530
|
+
}
|
|
531
|
+
if (this.#queue.length === 0 && this.#inHand === void 0) {
|
|
532
|
+
if (this.#cursor !== frame.cursor) this.#unacked += 1;
|
|
533
|
+
this.#cursor = frame.cursor;
|
|
534
|
+
this.#ack();
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
socket.addEventListener("error", () => {});
|
|
538
|
+
socket.addEventListener("close", (close) => {
|
|
539
|
+
settle(opened ? {
|
|
540
|
+
kind: "closed",
|
|
541
|
+
code: close.code
|
|
542
|
+
} : { kind: "refused" });
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Why an upgrade was refused. The events host answers a plain GET with the same refusal an
|
|
548
|
+
* upgrade gets, so the status and the error body are readable even where the WebSocket API
|
|
549
|
+
* hides them. Undefined when the host would have accepted (426) or could not be reached: both
|
|
550
|
+
* are worth a retry.
|
|
551
|
+
*/
|
|
552
|
+
async #diagnose() {
|
|
553
|
+
const url = mailboxEventsUrl(this.#connection, {
|
|
554
|
+
...this.#options,
|
|
555
|
+
cursor: this.#resumeCursor()
|
|
556
|
+
});
|
|
557
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
558
|
+
const deadline = new AbortController();
|
|
559
|
+
const onStop = () => deadline.abort();
|
|
560
|
+
this.#stop.signal.addEventListener("abort", onStop, { once: true });
|
|
561
|
+
let timer;
|
|
562
|
+
const expired = new Promise((resolve) => {
|
|
563
|
+
timer = setTimeout(() => {
|
|
564
|
+
deadline.abort();
|
|
565
|
+
resolve(void 0);
|
|
566
|
+
}, DIAGNOSE_TIMEOUT_MILLIS);
|
|
567
|
+
});
|
|
568
|
+
const diagnosis = (async () => {
|
|
569
|
+
try {
|
|
570
|
+
const response = await (this.#connection.fetch ?? fetch)(url, {
|
|
571
|
+
headers: this.#connection.headers,
|
|
572
|
+
signal: deadline.signal
|
|
573
|
+
});
|
|
574
|
+
if (response.status === 426) {
|
|
575
|
+
await response.body?.cancel();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
579
|
+
return {
|
|
580
|
+
status: response.status,
|
|
581
|
+
body: await readRefusalBody(response),
|
|
582
|
+
retryAfterSeconds: Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0
|
|
583
|
+
};
|
|
584
|
+
} catch {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
})();
|
|
588
|
+
try {
|
|
589
|
+
return await Promise.race([diagnosis, expired]);
|
|
590
|
+
} finally {
|
|
591
|
+
clearTimeout(timer);
|
|
592
|
+
this.#stop.signal.removeEventListener("abort", onStop);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
/** The refusal's JSON body, or undefined when it is not JSON; reads at most {@link REFUSAL_BODY_LIMIT}. */
|
|
597
|
+
const readRefusalBody = async (response) => {
|
|
598
|
+
const reader = response.body?.getReader();
|
|
599
|
+
if (reader === void 0) return void 0;
|
|
600
|
+
const chunks = [];
|
|
601
|
+
let size = 0;
|
|
602
|
+
try {
|
|
603
|
+
while (size < REFUSAL_BODY_LIMIT) {
|
|
604
|
+
const { done, value } = await reader.read();
|
|
605
|
+
if (done) break;
|
|
606
|
+
chunks.push(value);
|
|
607
|
+
size += value.byteLength;
|
|
608
|
+
}
|
|
609
|
+
} finally {
|
|
610
|
+
await reader.cancel().catch(() => void 0);
|
|
611
|
+
}
|
|
612
|
+
const bytes = new Uint8Array(Math.min(size, REFUSAL_BODY_LIMIT));
|
|
613
|
+
let offset = 0;
|
|
614
|
+
for (const chunk of chunks) {
|
|
615
|
+
const room = bytes.length - offset;
|
|
616
|
+
if (room <= 0) break;
|
|
617
|
+
bytes.set(chunk.subarray(0, room), offset);
|
|
618
|
+
offset += Math.min(chunk.byteLength, room);
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
622
|
+
} catch {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
//#endregion
|
|
627
|
+
export { MailboxEventStreamError as n, MailboxEventStream as t };
|
|
628
|
+
|
|
629
|
+
//# sourceMappingURL=mailbox-events-CsbF35vH.js.map
|