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