@tinycloud/sdk-services 3.1.0-beta.11 → 3.1.0-beta.14

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/dist/index.cjs CHANGED
@@ -1863,6 +1863,18 @@ var MAX_KV_BATCH_READ_ITEMS = 100;
1863
1863
  function isJsonObject(value) {
1864
1864
  return typeof value === "object" && value !== null && !Array.isArray(value);
1865
1865
  }
1866
+ var pathByteEncoder = new TextEncoder();
1867
+ function comparePathsByBytes(a, b) {
1868
+ const left = pathByteEncoder.encode(a);
1869
+ const right = pathByteEncoder.encode(b);
1870
+ const length = Math.min(left.length, right.length);
1871
+ for (let index = 0; index < length; index++) {
1872
+ if (left[index] !== right[index]) {
1873
+ return left[index] - right[index];
1874
+ }
1875
+ }
1876
+ return left.length - right.length;
1877
+ }
1866
1878
  var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
1867
1879
  function revocationCodeOf(body) {
1868
1880
  const match = REVOKED_BODY.exec(body.trim());
@@ -2096,15 +2108,25 @@ var KVService = class extends BaseService {
2096
2108
  )?.[1];
2097
2109
  return contentType?.includes("application/json") ? JSON.parse(text) : text;
2098
2110
  }
2111
+ /**
2112
+ * Validate a batch read response and return its items in the caller's
2113
+ * request order. The node flattens invocation abilities from a `BTreeMap`,
2114
+ * so results arrive sorted by UTF-8 byte order of the path, not in request
2115
+ * order. Items that carry their path in `key` are matched by path; if no
2116
+ * item carries a key, items are assumed to be in the node's byte-sorted
2117
+ * order and the request is aligned the same way. Any mix of keyed and
2118
+ * keyless items, a missing requested path, an unrequested path, or a
2119
+ * malformed item fails closed (returns undefined).
2120
+ */
2099
2121
  normalizeBatchReadResponse(data, paths, requireData) {
2100
2122
  if (!data || typeof data !== "object") return void 0;
2101
2123
  const response = data;
2102
- if (!Array.isArray(response.results) || response.results.length !== paths.length) {
2124
+ const items = response.results;
2125
+ if (!Array.isArray(items) || items.length !== paths.length) {
2103
2126
  return void 0;
2104
2127
  }
2105
- for (let index = 0; index < response.results.length; index++) {
2106
- const item = response.results[index];
2107
- if (!item || typeof item !== "object" || item.key !== paths[index] || typeof item.ok !== "boolean") {
2128
+ for (const item of items) {
2129
+ if (!item || typeof item !== "object" || typeof item.ok !== "boolean") {
2108
2130
  return void 0;
2109
2131
  }
2110
2132
  if (item.ok && (!item.headers || typeof item.headers !== "object" || Object.values(item.headers).some((value) => typeof value !== "string") || requireData && typeof item.dataBase64 !== "string")) {
@@ -2114,7 +2136,24 @@ var KVService = class extends BaseService {
2114
2136
  return void 0;
2115
2137
  }
2116
2138
  }
2117
- return response;
2139
+ if (items.every((item) => typeof item.key === "string")) {
2140
+ const byPath = new Map(items.map((item) => [item.key, item]));
2141
+ if (byPath.size !== items.length) return void 0;
2142
+ const aligned2 = [];
2143
+ for (const path of paths) {
2144
+ const item = byPath.get(path);
2145
+ if (!item) return void 0;
2146
+ aligned2.push(item);
2147
+ }
2148
+ return aligned2;
2149
+ }
2150
+ if (items.some((item) => item.key !== void 0)) return void 0;
2151
+ const requestOrder = paths.map((_, index) => index).sort((a, b) => comparePathsByBytes(paths[a], paths[b]));
2152
+ const aligned = new Array(items.length);
2153
+ for (let position = 0; position < requestOrder.length; position++) {
2154
+ aligned[requestOrder[position]] = items[position];
2155
+ }
2156
+ return aligned;
2118
2157
  }
2119
2158
  async batchRead(keys, action, options) {
2120
2159
  if (!this.requireAuth()) return err(authRequiredError("kv"));
@@ -2207,19 +2246,19 @@ var KVService = class extends BaseService {
2207
2246
  { meta: { status: response.status, statusText: response.statusText } }
2208
2247
  ));
2209
2248
  }
2210
- const payload = this.normalizeBatchReadResponse(
2249
+ const items = this.normalizeBatchReadResponse(
2211
2250
  await response.json(),
2212
2251
  paths,
2213
2252
  action === KVAction.GET
2214
2253
  );
2215
- if (!payload) {
2254
+ if (!items) {
2216
2255
  return err(serviceError(
2217
2256
  ErrorCodes.NETWORK_ERROR,
2218
2257
  "KV batch read response did not match the requested keys",
2219
2258
  "kv"
2220
2259
  ));
2221
2260
  }
2222
- const results = payload.results.map((item, index) => {
2261
+ const results = items.map((item, index) => {
2223
2262
  if (!item.ok) {
2224
2263
  return {
2225
2264
  key: keys[index],
@@ -4146,7 +4185,7 @@ var AsyncQueue = class {
4146
4185
  }
4147
4186
  const waiter = this.waiters.shift();
4148
4187
  if (waiter) {
4149
- waiter({ value, done: false });
4188
+ waiter.resolve({ value, done: false });
4150
4189
  return;
4151
4190
  }
4152
4191
  this.values.push(value);
@@ -4158,7 +4197,22 @@ var AsyncQueue = class {
4158
4197
  this.closed = true;
4159
4198
  while (this.waiters.length > 0) {
4160
4199
  const waiter = this.waiters.shift();
4161
- waiter?.({ value: void 0, done: true });
4200
+ waiter?.resolve({ value: void 0, done: true });
4201
+ }
4202
+ }
4203
+ /**
4204
+ * Fail the queue: pending and future `next()` calls reject with `error`,
4205
+ * so a `for await` consumer sees the iterator throw.
4206
+ */
4207
+ fail(error) {
4208
+ if (this.closed) {
4209
+ return;
4210
+ }
4211
+ this.closed = true;
4212
+ this.failure = error;
4213
+ while (this.waiters.length > 0) {
4214
+ const waiter = this.waiters.shift();
4215
+ waiter?.reject(error);
4162
4216
  }
4163
4217
  }
4164
4218
  next() {
@@ -4166,20 +4220,28 @@ var AsyncQueue = class {
4166
4220
  const value = this.values.shift();
4167
4221
  return Promise.resolve({ value, done: false });
4168
4222
  }
4223
+ if (this.failure !== void 0) {
4224
+ return Promise.reject(this.failure);
4225
+ }
4169
4226
  if (this.closed) {
4170
4227
  return Promise.resolve({ value: void 0, done: true });
4171
4228
  }
4172
- return new Promise((resolve) => {
4173
- this.waiters.push(resolve);
4229
+ return new Promise((resolve, reject) => {
4230
+ this.waiters.push({ resolve, reject });
4174
4231
  });
4175
4232
  }
4176
4233
  };
4234
+ var HOOK_STREAM_RETRY_BASE_DELAY_MS = 250;
4235
+ var HOOK_STREAM_RETRY_MAX_DELAY_MS = 3e4;
4236
+ var HOOK_STREAM_HEALTHY_MS = 5e3;
4177
4237
  var HooksService = class extends BaseService {
4178
4238
  constructor(config = {}) {
4179
4239
  super();
4180
4240
  this._subscribers = /* @__PURE__ */ new Set();
4181
- this._refreshChain = Promise.resolve();
4182
- this._activeSignature = "";
4241
+ /** True while the one supervisor loop runs. Set only by wake(), cleared only at loop exit. */
4242
+ this._supervising = false;
4243
+ /** Bumped on every session change; part of the plan key. */
4244
+ this._sessionGeneration = 0;
4183
4245
  this._config = config;
4184
4246
  }
4185
4247
  get config() {
@@ -4195,35 +4257,32 @@ var HooksService = class extends BaseService {
4195
4257
  if (subscriptions.length === 0) {
4196
4258
  throw new Error("At least one hook subscription is required");
4197
4259
  }
4198
- const normalized = subscriptions.map(normalizeSubscription);
4260
+ const requested = subscriptions.map(normalizeSubscription);
4199
4261
  const subscriber = {
4200
- requested: normalized,
4262
+ requested,
4263
+ signatures: requested.map(subscriptionSignature),
4201
4264
  ttlSeconds: options.ttlSeconds,
4202
4265
  queue: new AsyncQueue()
4203
4266
  };
4204
- this._subscribers.add(subscriber);
4205
- const abortHandler = () => {
4267
+ const leave = () => {
4206
4268
  this._subscribers.delete(subscriber);
4207
4269
  subscriber.queue.close();
4208
- void this.scheduleSharedStreamRefresh();
4270
+ this.wake();
4209
4271
  };
4210
- if (options.signal) {
4211
- if (options.signal.aborted) {
4212
- abortHandler();
4213
- } else {
4214
- options.signal.addEventListener("abort", abortHandler, { once: true });
4215
- }
4272
+ const { signal } = options;
4273
+ if (signal?.aborted) {
4274
+ return;
4216
4275
  }
4217
- void this.scheduleSharedStreamRefresh();
4276
+ signal?.addEventListener("abort", leave, { once: true });
4277
+ this._subscribers.add(subscriber);
4278
+ this.wake();
4218
4279
  try {
4219
4280
  for await (const event of subscriber.queue) {
4220
4281
  yield event;
4221
4282
  }
4222
4283
  } finally {
4223
- if (options.signal) {
4224
- options.signal.removeEventListener("abort", abortHandler);
4225
- }
4226
- abortHandler();
4284
+ signal?.removeEventListener("abort", leave);
4285
+ leave();
4227
4286
  }
4228
4287
  }
4229
4288
  async register(webhook) {
@@ -4364,146 +4423,292 @@ var HooksService = class extends BaseService {
4364
4423
  return err(wrapError("hooks", error));
4365
4424
  }
4366
4425
  }
4367
- async scheduleSharedStreamRefresh() {
4368
- this._refreshChain = this._refreshChain.then(() => this.refreshSharedStream()).catch(() => void 0);
4369
- await this._refreshChain;
4370
- }
4371
- async refreshSharedStream() {
4372
- if (!this.requireAuth() || this._subscribers.size === 0) {
4373
- this.abortSharedStream();
4374
- this._activeSignature = "";
4375
- return;
4376
- }
4377
- const state = this.collectSharedStreamState();
4378
- if (state.signature !== this._activeSignature) {
4379
- this._activeSignature = state.signature;
4380
- this.abortSharedStream();
4426
+ /**
4427
+ * Called after anything that may change what the supervisor should do:
4428
+ * subscribe, unsubscribe, session change, sign-out. Interrupts the running
4429
+ * step if the plan moved away from it, and starts the supervisor if none
4430
+ * runs. Touches only SDK-owned state, so it never throws into callers such
4431
+ * as `ServiceContext.setSession`.
4432
+ */
4433
+ wake() {
4434
+ const phase = this._phase;
4435
+ if (phase && phase.key !== this.plan()?.key) {
4436
+ phase.interrupt.abort();
4381
4437
  }
4382
- if (!this._sharedStreamTask) {
4383
- this._sharedStreamTask = this.runSharedStream(state).catch((error) => {
4384
- if (!isAbortError(error)) {
4385
- throw error;
4386
- }
4387
- }).finally(() => {
4388
- this._sharedStreamTask = void 0;
4389
- this._sharedStreamAbort = void 0;
4390
- if (this._subscribers.size > 0) {
4391
- void this.scheduleSharedStreamRefresh();
4392
- }
4393
- });
4438
+ if (this._supervising || this._subscribers.size === 0) {
4439
+ return;
4394
4440
  }
4441
+ this._supervising = true;
4442
+ this.supervise().catch(() => this.collapse());
4395
4443
  }
4396
- collectSharedStreamState() {
4397
- const merged = /* @__PURE__ */ new Map();
4398
- const ttlCandidates = [];
4399
- for (const subscriber of this._subscribers) {
4400
- if (typeof subscriber.ttlSeconds === "number") {
4401
- ttlCandidates.push(subscriber.ttlSeconds);
4444
+ /**
4445
+ * The only owner of the shared stream. Each iteration is one attempt
4446
+ * (mint → open → drain), then a classification and, unless the attempt was
4447
+ * superseded or released its subscribers, a backoff. Every await is a
4448
+ * `phase` that wake() can interrupt; an interrupted phase yields
4449
+ * `undefined`, so a superseded mint can never act. Outside `attempt` and
4450
+ * `pause`, which each own one catch, the loop touches only SDK state.
4451
+ */
4452
+ async supervise() {
4453
+ let streak = 0;
4454
+ for (; ; ) {
4455
+ const plan = this.plan();
4456
+ if (!plan) {
4457
+ this._supervising = false;
4458
+ return;
4459
+ }
4460
+ const health = { openedAt: 0, delivered: false };
4461
+ const outcome = await this.phase(
4462
+ plan.key,
4463
+ (signal) => this.attempt(plan, signal, health)
4464
+ );
4465
+ if (health.delivered || health.openedAt > 0 && performance.now() - health.openedAt >= HOOK_STREAM_HEALTHY_MS) {
4466
+ streak = 0;
4467
+ }
4468
+ if (!outcome) {
4469
+ continue;
4470
+ }
4471
+ if (this.plan()?.key !== plan.key) {
4472
+ continue;
4473
+ }
4474
+ if (outcome.kind === "stopped") {
4475
+ this.release();
4476
+ continue;
4477
+ }
4478
+ if (outcome.kind === "refused" && outcome.terminal) {
4479
+ this.release(outcome.error);
4480
+ this.report(outcome.error);
4481
+ continue;
4402
4482
  }
4403
- for (const subscription of subscriber.requested) {
4404
- merged.set(subscriptionSignature(subscription), subscription);
4483
+ if (outcome.kind !== "ended") {
4484
+ this.report(outcome.error);
4405
4485
  }
4486
+ streak += 1;
4487
+ await this.phase(
4488
+ plan.key,
4489
+ (signal) => this.pause(streak, signal)
4490
+ );
4491
+ }
4492
+ }
4493
+ /**
4494
+ * Run one step under an interrupt that wake() fires when the plan key moves
4495
+ * on. An interrupted step's result is discarded. The interrupt is also
4496
+ * fired when the step returns, which releases whatever it left open: the
4497
+ * request signal and its listeners, an unread response body, a timer.
4498
+ */
4499
+ async phase(key, step) {
4500
+ const interrupt = new AbortController();
4501
+ this._phase = { key, interrupt };
4502
+ try {
4503
+ if (this.plan()?.key !== key) {
4504
+ interrupt.abort();
4505
+ }
4506
+ const result = await step(interrupt.signal);
4507
+ return interrupt.signal.aborted ? void 0 : result;
4508
+ } finally {
4509
+ this._phase = void 0;
4510
+ interrupt.abort();
4406
4511
  }
4407
- const subscriptions = [...merged.values()].sort(
4408
- (left, right) => subscriptionSignature(left).localeCompare(subscriptionSignature(right))
4409
- );
4410
- const ttlSeconds = ttlCandidates.length > 0 ? Math.min(...ttlCandidates) : void 0;
4411
- const signature = JSON.stringify({
4412
- subscriptions: subscriptions.map(subscriptionSignature),
4413
- ttlSeconds
4414
- });
4415
- return {
4416
- subscriptions,
4417
- ttlSeconds,
4418
- signature
4419
- };
4420
4512
  }
4421
- async runSharedStream(state) {
4422
- const abortController = new AbortController();
4423
- this._sharedStreamAbort = abortController;
4513
+ /**
4514
+ * One mint → open → drain. The single catch site for everything the
4515
+ * attempt touches that the SDK does not own: context accessors, invoke,
4516
+ * fetch, response bodies, the SSE parser. Never rejects.
4517
+ */
4518
+ async attempt(plan, signal, health) {
4519
+ let phase = "mint";
4520
+ let request;
4521
+ const owns = () => !signal.aborted && !request?.signal.aborted && this.plan()?.key === plan.key;
4424
4522
  try {
4425
- const host = this._config.host ?? this.context.hosts[0];
4426
- const ticketResponse = await this.mintHookTicket(
4427
- state.subscriptions,
4428
- state.ttlSeconds,
4429
- abortController.signal
4523
+ const context = this.context;
4524
+ const contextSignal = context.abortSignal;
4525
+ this._lastContextSignal = contextSignal;
4526
+ if (!context.isAuthenticated || contextSignal?.aborted) {
4527
+ return { kind: "stopped" };
4528
+ }
4529
+ request = this.createRequestSignal(signal, 0);
4530
+ const host = this.host;
4531
+ const minted = await awaitOwned(
4532
+ request.signal,
4533
+ () => context.fetch(`${host}/hooks/tickets`, {
4534
+ method: "POST",
4535
+ headers: {
4536
+ ...serviceHeadersToRecord(this.createInvokeHeaders(plan.subscriptions)),
4537
+ "content-type": "application/json"
4538
+ },
4539
+ body: JSON.stringify({
4540
+ subscriptions: plan.subscriptions,
4541
+ ttlSeconds: plan.ttlSeconds
4542
+ }),
4543
+ signal: request.signal
4544
+ })
4430
4545
  );
4431
- const streamResponse = await this.openHookStream(
4432
- host,
4433
- ticketResponse.ticket,
4434
- abortController.signal
4546
+ if (!owns()) return { kind: "stopped" };
4547
+ if (!minted.ok) {
4548
+ const status = minted.status;
4549
+ const error = await awaitOwned(
4550
+ request.signal,
4551
+ () => responseError("hooks", "failed to mint hook ticket", minted)
4552
+ );
4553
+ if (!owns()) return { kind: "stopped" };
4554
+ return { kind: "refused", terminal: isTerminalMintRefusal(status), error };
4555
+ }
4556
+ const body = await awaitOwned(request.signal, () => minted.json());
4557
+ if (!owns()) return { kind: "stopped" };
4558
+ const ticket = body !== null && typeof body === "object" && "ticket" in body ? body.ticket : void 0;
4559
+ if (typeof ticket !== "string" || ticket.length === 0) {
4560
+ throw new Error("Hook ticket response did not include a ticket");
4561
+ }
4562
+ phase = "open";
4563
+ if (!owns()) return { kind: "stopped" };
4564
+ const stream = await awaitOwned(
4565
+ request.signal,
4566
+ () => context.fetch(`${host}/hooks/events?ticket=${encodeURIComponent(ticket)}`, {
4567
+ method: "GET",
4568
+ headers: { accept: "text/event-stream" },
4569
+ signal: request.signal
4570
+ })
4435
4571
  );
4436
- for await (const message of parseSseStream(
4437
- streamResponse.body,
4438
- abortController.signal
4439
- )) {
4440
- if (!message.data) {
4441
- continue;
4442
- }
4443
- const event = parseHookEvent(message);
4444
- for (const subscriber of this._subscribers) {
4445
- if (matchesAnySubscription(event, subscriber.requested)) {
4446
- subscriber.queue.push(event);
4447
- }
4572
+ if (!owns()) return { kind: "stopped" };
4573
+ if (!stream.ok) {
4574
+ return { kind: "refused", terminal: false, error: hookStreamRefusal(stream.status) };
4575
+ }
4576
+ health.openedAt = performance.now();
4577
+ const iterator = parseSseStream(stream.body, request.signal)[Symbol.asyncIterator]();
4578
+ try {
4579
+ for (; ; ) {
4580
+ const next = await awaitOwned(request.signal, () => iterator.next());
4581
+ if (!owns()) return { kind: "stopped" };
4582
+ if (next.done) break;
4583
+ const message = next.value;
4584
+ if (!message.data) continue;
4585
+ const event = parseHookEvent(message);
4586
+ if (!owns()) return { kind: "stopped" };
4587
+ this.dispatch(event);
4588
+ health.delivered = true;
4448
4589
  }
4590
+ } finally {
4591
+ const closing = iterator.return?.();
4592
+ if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
4449
4593
  }
4594
+ return owns() ? { kind: "ended" } : { kind: "stopped" };
4595
+ } catch (thrown) {
4596
+ return request?.signal.aborted || thrown === SUPERSEDED ? { kind: "stopped" } : { kind: "failed", error: hookStreamFailure(phase, thrown) };
4597
+ }
4598
+ }
4599
+ /**
4600
+ * Back off before the next attempt. `config.streamRetry` overrides the
4601
+ * delay and the clock; whatever they do, the pause ends by the delay or the
4602
+ * interrupt, then yields one timer turn so an instant clock cannot starve
4603
+ * due timers or the process.
4604
+ */
4605
+ async pause(attempt, signal) {
4606
+ try {
4607
+ this._lastContextSignal = this.context.abortSignal;
4608
+ } catch {
4609
+ }
4610
+ const contextSignal = this._lastContextSignal;
4611
+ const waits = [];
4612
+ const aborts = [];
4613
+ try {
4614
+ const retry = this._config.streamRetry;
4615
+ const delayMs = retry?.delay?.(attempt) ?? hookStreamRetryDelay(attempt);
4616
+ waits.push((retry?.wait ?? sleep)(delayMs, attempt, signal));
4617
+ } catch {
4618
+ waits.push(Promise.resolve());
4619
+ }
4620
+ aborts.push(abortPromise(signal));
4621
+ if (contextSignal) aborts.push(abortPromise(contextSignal));
4622
+ try {
4623
+ await Promise.race([...waits, ...aborts.map(({ promise }) => promise)]);
4624
+ } catch {
4450
4625
  } finally {
4451
- if (this._sharedStreamAbort === abortController) {
4452
- this._sharedStreamAbort = void 0;
4626
+ for (const abort of aborts) abort.dispose();
4627
+ }
4628
+ await new Promise((resolve) => setTimeout(resolve, 0));
4629
+ }
4630
+ /** The stream the current subscribers need, or undefined when none remain. */
4631
+ plan() {
4632
+ if (this._subscribers.size === 0) {
4633
+ return void 0;
4634
+ }
4635
+ const merged = /* @__PURE__ */ new Map();
4636
+ let ttlSeconds;
4637
+ for (const subscriber of this._subscribers) {
4638
+ subscriber.requested.forEach(
4639
+ (subscription, index) => merged.set(subscriber.signatures[index], subscription)
4640
+ );
4641
+ if (typeof subscriber.ttlSeconds === "number") {
4642
+ ttlSeconds = Math.min(subscriber.ttlSeconds, ttlSeconds ?? Infinity);
4453
4643
  }
4454
4644
  }
4645
+ const signatures = [...merged.keys()].sort();
4646
+ return {
4647
+ key: JSON.stringify([this._sessionGeneration, signatures, ttlSeconds ?? null]),
4648
+ subscriptions: signatures.map((signature) => merged.get(signature)),
4649
+ ttlSeconds
4650
+ };
4455
4651
  }
4456
- abortSharedStream() {
4457
- this._sharedStreamAbort?.abort();
4652
+ dispatch(event) {
4653
+ for (const subscriber of this._subscribers) {
4654
+ if (matchesAnySubscription(event, subscriber.requested)) {
4655
+ subscriber.queue.push(event);
4656
+ }
4657
+ }
4458
4658
  }
4459
- createHookHeaders(action, path) {
4460
- return this.context.invoke(this.session, "hooks", path, action);
4659
+ /**
4660
+ * Detach every subscriber. With `error`, their iterators throw it after
4661
+ * draining buffered events; without, they complete.
4662
+ */
4663
+ release(error) {
4664
+ const subscribers = [...this._subscribers];
4665
+ this._subscribers.clear();
4666
+ for (const { queue } of subscribers) {
4667
+ if (error) {
4668
+ queue.fail(error);
4669
+ } else {
4670
+ queue.close();
4671
+ }
4672
+ }
4461
4673
  }
4462
- async mintHookTicket(subscriptions, ttlSeconds, signal) {
4463
- const host = this._config.host ?? this.context.hosts[0];
4464
- const headers = this.createInvokeHeaders(subscriptions);
4465
- const ticketResponse = await this.context.fetch(`${host}/hooks/tickets`, {
4466
- method: "POST",
4467
- headers: {
4468
- ...serviceHeadersToRecord(headers),
4469
- "content-type": "application/json"
4470
- },
4471
- body: JSON.stringify({
4472
- subscriptions,
4473
- ttlSeconds
4474
- }),
4475
- signal
4476
- });
4477
- if (!ticketResponse.ok) {
4478
- throw await responseError(
4479
- "hooks",
4480
- "failed to mint hook ticket",
4481
- ticketResponse
4482
- );
4674
+ report(error) {
4675
+ if (error.meta && typeof error.meta === "object") {
4676
+ Object.freeze(error.meta);
4483
4677
  }
4484
- const ticketJson = await ticketResponse.json();
4485
- if (!ticketJson?.ticket) {
4486
- throw new Error("Hook ticket response did not include a ticket");
4678
+ Object.freeze(error);
4679
+ try {
4680
+ this.emitError(error, "stream");
4681
+ } catch {
4487
4682
  }
4488
- return ticketJson;
4489
4683
  }
4490
- async openHookStream(host, ticket, signal) {
4491
- const streamResponse = await this.context.fetch(
4492
- `${host}/hooks/events?ticket=${encodeURIComponent(ticket)}`,
4493
- {
4494
- method: "GET",
4495
- headers: { accept: "text/event-stream" },
4496
- signal
4497
- }
4684
+ /**
4685
+ * Unreachable by construction. If the loop ever rejects anyway, fail its
4686
+ * subscribers visibly instead of stranding them or crashing the process.
4687
+ */
4688
+ collapse() {
4689
+ this._supervising = false;
4690
+ this._phase?.interrupt.abort();
4691
+ this._phase = void 0;
4692
+ const error = serviceError(
4693
+ ErrorCodes.NETWORK_ERROR,
4694
+ "hook stream supervisor stopped unexpectedly",
4695
+ "hooks"
4498
4696
  );
4499
- if (!streamResponse.ok) {
4500
- throw await responseError(
4501
- "hooks",
4502
- "failed to open hook stream",
4503
- streamResponse
4504
- );
4505
- }
4506
- return streamResponse;
4697
+ this.report(error);
4698
+ this.release(error);
4699
+ }
4700
+ onSessionChange(session) {
4701
+ super.onSessionChange(session);
4702
+ this._sessionGeneration += 1;
4703
+ this.wake();
4704
+ }
4705
+ onSignOut() {
4706
+ super.onSignOut();
4707
+ this.release();
4708
+ this.wake();
4709
+ }
4710
+ createHookHeaders(action, path) {
4711
+ return this.context.invoke(this.session, "hooks", path, action);
4507
4712
  }
4508
4713
  createInvokeHeaders(subscriptions) {
4509
4714
  const entries = subscriptions.map((subscription) => ({
@@ -4534,11 +4739,168 @@ function buildScopePath(service, pathPrefix) {
4534
4739
  const normalized = normalizePathPrefix(pathPrefix);
4535
4740
  return normalized ? `${service}/${normalized}` : service;
4536
4741
  }
4742
+ function hookStreamRetryDelay(attempt) {
4743
+ const cap = Math.min(
4744
+ HOOK_STREAM_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1),
4745
+ HOOK_STREAM_RETRY_MAX_DELAY_MS
4746
+ );
4747
+ return cap / 2 + Math.random() * (cap / 2);
4748
+ }
4749
+ function isTerminalMintRefusal(status) {
4750
+ return typeof status === "number" && status >= 400 && status < 500 && status !== 408 && status !== 429;
4751
+ }
4752
+ var SUPERSEDED = /* @__PURE__ */ Symbol("hooks attempt superseded");
4753
+ async function awaitOwned(signal, operation) {
4754
+ if (signal.aborted) {
4755
+ if (typeof operation !== "function") {
4756
+ void operation.then(cancelLateResponse, () => void 0);
4757
+ }
4758
+ throw SUPERSEDED;
4759
+ }
4760
+ const promise = typeof operation === "function" ? operation() : operation;
4761
+ return new Promise((resolve, reject) => {
4762
+ let settled = false;
4763
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
4764
+ const onAbort = () => {
4765
+ if (settled) return;
4766
+ settled = true;
4767
+ cleanup();
4768
+ reject(SUPERSEDED);
4769
+ };
4770
+ signal.addEventListener("abort", onAbort, { once: true });
4771
+ promise.then(
4772
+ (value) => {
4773
+ if (settled) {
4774
+ cancelLateResponse(value);
4775
+ return;
4776
+ }
4777
+ settled = true;
4778
+ cleanup();
4779
+ resolve(value);
4780
+ },
4781
+ (error) => {
4782
+ if (settled) return;
4783
+ settled = true;
4784
+ cleanup();
4785
+ reject(error);
4786
+ }
4787
+ );
4788
+ });
4789
+ }
4790
+ function cancelLateResponse(value) {
4791
+ if (!(value instanceof Response) || !value.body) return;
4792
+ try {
4793
+ const cancellation = value.body.cancel();
4794
+ void cancellation.then(() => void 0, () => void 0);
4795
+ } catch {
4796
+ }
4797
+ }
4798
+ function hookStreamRefusal(status) {
4799
+ const code = typeof status === "number" ? status : void 0;
4800
+ return serviceError(
4801
+ ErrorCodes.NETWORK_ERROR,
4802
+ `hook stream open refused: ${code ?? "unknown status"}`,
4803
+ "hooks",
4804
+ { meta: { phase: "open", status: code } }
4805
+ );
4806
+ }
4807
+ var STREAM_ERROR_NAMES = /* @__PURE__ */ new Set([
4808
+ "Error",
4809
+ "TypeError",
4810
+ "AbortError",
4811
+ "TimeoutError",
4812
+ "SyntaxError",
4813
+ "RangeError",
4814
+ "NetworkError"
4815
+ ]);
4816
+ function hookStreamFailure(phase, thrown) {
4817
+ const candidateName = readProperty(thrown, "name");
4818
+ const name = typeof candidateName === "string" && STREAM_ERROR_NAMES.has(candidateName) ? candidateName : "Error";
4819
+ const code = identifier(readProperty(thrown, "code")) ?? identifier(readProperty(readProperty(thrown, "cause"), "code"));
4820
+ return serviceError(
4821
+ ErrorCodes.NETWORK_ERROR,
4822
+ `hook stream ${phase} failed: ${name}${code ? ` (${code})` : ""}`,
4823
+ "hooks",
4824
+ { meta: { phase, errorName: name, ...code ? { errorCode: code } : {} } }
4825
+ );
4826
+ }
4827
+ var STREAM_ERROR_CODES = /* @__PURE__ */ new Set([
4828
+ "ECONNREFUSED",
4829
+ "ECONNRESET",
4830
+ "ECONNABORTED",
4831
+ "ETIMEDOUT",
4832
+ "EPIPE",
4833
+ "ENOTFOUND",
4834
+ "EAI_AGAIN",
4835
+ "EHOSTUNREACH",
4836
+ "ENETUNREACH",
4837
+ "ENETDOWN",
4838
+ "EADDRNOTAVAIL",
4839
+ "UND_ERR_SOCKET",
4840
+ "UND_ERR_CONNECT_TIMEOUT",
4841
+ "UND_ERR_HEADERS_TIMEOUT",
4842
+ "UND_ERR_BODY_TIMEOUT",
4843
+ "UND_ERR_CLOSED",
4844
+ "UND_ERR_ABORTED",
4845
+ "UND_ERR_RESPONSE_STATUS_CODE",
4846
+ "ERR_TLS_CERT_ALTNAME_INVALID",
4847
+ "CERT_HAS_EXPIRED",
4848
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
4849
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
4850
+ "SELF_SIGNED_CERT_IN_CHAIN",
4851
+ "ERR_SSL_WRONG_VERSION_NUMBER"
4852
+ ]);
4853
+ function identifier(value) {
4854
+ return typeof value === "string" && STREAM_ERROR_CODES.has(value) ? value : void 0;
4855
+ }
4856
+ function readProperty(value, key) {
4857
+ if (value === null || typeof value !== "object" && typeof value !== "function") {
4858
+ return void 0;
4859
+ }
4860
+ try {
4861
+ return Reflect.get(value, key);
4862
+ } catch {
4863
+ return void 0;
4864
+ }
4865
+ }
4866
+ function sleep(delayMs, _attempt, signal) {
4867
+ return new Promise((resolve) => {
4868
+ const finish = () => {
4869
+ clearTimeout(timer);
4870
+ signal.removeEventListener("abort", stop);
4871
+ resolve();
4872
+ };
4873
+ const timer = setTimeout(finish, delayMs);
4874
+ timer.unref?.();
4875
+ const stop = () => finish();
4876
+ if (signal.aborted) {
4877
+ finish();
4878
+ } else {
4879
+ signal.addEventListener("abort", stop, { once: true });
4880
+ }
4881
+ });
4882
+ }
4883
+ function abortPromise(signal) {
4884
+ let resolve;
4885
+ const promise = new Promise((done) => {
4886
+ resolve = done;
4887
+ });
4888
+ const onAbort = () => resolve();
4889
+ if (signal.aborted) {
4890
+ resolve();
4891
+ } else {
4892
+ signal.addEventListener("abort", onAbort, { once: true });
4893
+ }
4894
+ return {
4895
+ promise,
4896
+ dispose: () => signal.removeEventListener("abort", onAbort)
4897
+ };
4898
+ }
4537
4899
  function normalizeSubscription(subscription) {
4538
4900
  return {
4539
4901
  ...subscription,
4540
4902
  pathPrefix: normalizePathPrefix(subscription.pathPrefix),
4541
- abilities: subscription.abilities ?? []
4903
+ abilities: subscription.abilities ? [...subscription.abilities] : []
4542
4904
  };
4543
4905
  }
4544
4906
  function subscriptionSignature(subscription) {
@@ -4604,9 +4966,6 @@ async function responseError(service, message, response) {
4604
4966
  meta: { ...error.meta, status: response.status, statusText: response.statusText }
4605
4967
  };
4606
4968
  }
4607
- function isAbortError(error) {
4608
- return error instanceof DOMException && error.name === "AbortError";
4609
- }
4610
4969
  async function* parseSseStream(body, signal) {
4611
4970
  if (!body) {
4612
4971
  throw new Error("Hook stream response does not expose a readable body");
@@ -4635,11 +4994,24 @@ async function* parseSseStream(body, signal) {
4635
4994
  async function* readBodyChunks(body, signal) {
4636
4995
  const asyncIterable = body;
4637
4996
  if (typeof asyncIterable?.[Symbol.asyncIterator] === "function") {
4638
- for await (const chunk of asyncIterable) {
4639
- if (signal?.aborted) {
4640
- break;
4997
+ const iterator = asyncIterable[Symbol.asyncIterator]();
4998
+ const cancel = () => {
4999
+ try {
5000
+ const closing = iterator.return?.();
5001
+ if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
5002
+ } catch {
5003
+ }
5004
+ };
5005
+ signal?.addEventListener("abort", cancel, { once: true });
5006
+ try {
5007
+ while (!signal?.aborted) {
5008
+ const next = await iterator.next();
5009
+ if (next.done) break;
5010
+ yield next.value;
4641
5011
  }
4642
- yield chunk;
5012
+ } finally {
5013
+ signal?.removeEventListener("abort", cancel);
5014
+ cancel();
4643
5015
  }
4644
5016
  return;
4645
5017
  }
@@ -4648,21 +5020,23 @@ async function* readBodyChunks(body, signal) {
4648
5020
  throw new Error("Unsupported hook stream body type");
4649
5021
  }
4650
5022
  const reader = stream.getReader();
5023
+ const cancelReader = () => {
5024
+ try {
5025
+ const cancellation = reader.cancel?.();
5026
+ if (cancellation) void cancellation.then(() => void 0, () => void 0);
5027
+ } catch {
5028
+ }
5029
+ };
5030
+ signal?.addEventListener("abort", cancelReader, { once: true });
4651
5031
  try {
4652
5032
  while (!signal?.aborted) {
4653
5033
  const { done, value } = await reader.read();
4654
- if (done) {
4655
- break;
4656
- }
4657
- if (value) {
4658
- yield value;
4659
- }
5034
+ if (done) break;
5035
+ if (value) yield value;
4660
5036
  }
4661
5037
  } finally {
4662
- try {
4663
- await reader.cancel?.();
4664
- } catch {
4665
- }
5038
+ signal?.removeEventListener("abort", cancelReader);
5039
+ cancelReader();
4666
5040
  reader.releaseLock?.();
4667
5041
  }
4668
5042
  }
@@ -7306,10 +7680,10 @@ var EncryptionService = class extends BaseService {
7306
7680
  get crypto() {
7307
7681
  return this._config.crypto;
7308
7682
  }
7309
- async discoverNetwork(identifier, ownerDid) {
7683
+ async discoverNetwork(identifier2, ownerDid) {
7310
7684
  this.assertActive();
7311
7685
  const result = await discoverNetwork({
7312
- identifier,
7686
+ identifier: identifier2,
7313
7687
  ...ownerDid !== void 0 ? { ownerDid } : {},
7314
7688
  ...this._config.node !== void 0 ? { node: this._config.node } : {},
7315
7689
  ...this._config.wellKnown !== void 0 ? { wellKnown: this._config.wellKnown } : {}