@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.js CHANGED
@@ -1713,6 +1713,18 @@ var MAX_KV_BATCH_READ_ITEMS = 100;
1713
1713
  function isJsonObject(value) {
1714
1714
  return typeof value === "object" && value !== null && !Array.isArray(value);
1715
1715
  }
1716
+ var pathByteEncoder = new TextEncoder();
1717
+ function comparePathsByBytes(a, b) {
1718
+ const left = pathByteEncoder.encode(a);
1719
+ const right = pathByteEncoder.encode(b);
1720
+ const length = Math.min(left.length, right.length);
1721
+ for (let index = 0; index < length; index++) {
1722
+ if (left[index] !== right[index]) {
1723
+ return left[index] - right[index];
1724
+ }
1725
+ }
1726
+ return left.length - right.length;
1727
+ }
1716
1728
  var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
1717
1729
  function revocationCodeOf(body) {
1718
1730
  const match = REVOKED_BODY.exec(body.trim());
@@ -1946,15 +1958,25 @@ var KVService = class extends BaseService {
1946
1958
  )?.[1];
1947
1959
  return contentType?.includes("application/json") ? JSON.parse(text) : text;
1948
1960
  }
1961
+ /**
1962
+ * Validate a batch read response and return its items in the caller's
1963
+ * request order. The node flattens invocation abilities from a `BTreeMap`,
1964
+ * so results arrive sorted by UTF-8 byte order of the path, not in request
1965
+ * order. Items that carry their path in `key` are matched by path; if no
1966
+ * item carries a key, items are assumed to be in the node's byte-sorted
1967
+ * order and the request is aligned the same way. Any mix of keyed and
1968
+ * keyless items, a missing requested path, an unrequested path, or a
1969
+ * malformed item fails closed (returns undefined).
1970
+ */
1949
1971
  normalizeBatchReadResponse(data, paths, requireData) {
1950
1972
  if (!data || typeof data !== "object") return void 0;
1951
1973
  const response = data;
1952
- if (!Array.isArray(response.results) || response.results.length !== paths.length) {
1974
+ const items = response.results;
1975
+ if (!Array.isArray(items) || items.length !== paths.length) {
1953
1976
  return void 0;
1954
1977
  }
1955
- for (let index = 0; index < response.results.length; index++) {
1956
- const item = response.results[index];
1957
- if (!item || typeof item !== "object" || item.key !== paths[index] || typeof item.ok !== "boolean") {
1978
+ for (const item of items) {
1979
+ if (!item || typeof item !== "object" || typeof item.ok !== "boolean") {
1958
1980
  return void 0;
1959
1981
  }
1960
1982
  if (item.ok && (!item.headers || typeof item.headers !== "object" || Object.values(item.headers).some((value) => typeof value !== "string") || requireData && typeof item.dataBase64 !== "string")) {
@@ -1964,7 +1986,24 @@ var KVService = class extends BaseService {
1964
1986
  return void 0;
1965
1987
  }
1966
1988
  }
1967
- return response;
1989
+ if (items.every((item) => typeof item.key === "string")) {
1990
+ const byPath = new Map(items.map((item) => [item.key, item]));
1991
+ if (byPath.size !== items.length) return void 0;
1992
+ const aligned2 = [];
1993
+ for (const path of paths) {
1994
+ const item = byPath.get(path);
1995
+ if (!item) return void 0;
1996
+ aligned2.push(item);
1997
+ }
1998
+ return aligned2;
1999
+ }
2000
+ if (items.some((item) => item.key !== void 0)) return void 0;
2001
+ const requestOrder = paths.map((_, index) => index).sort((a, b) => comparePathsByBytes(paths[a], paths[b]));
2002
+ const aligned = new Array(items.length);
2003
+ for (let position = 0; position < requestOrder.length; position++) {
2004
+ aligned[requestOrder[position]] = items[position];
2005
+ }
2006
+ return aligned;
1968
2007
  }
1969
2008
  async batchRead(keys, action, options) {
1970
2009
  if (!this.requireAuth()) return err(authRequiredError("kv"));
@@ -2057,19 +2096,19 @@ var KVService = class extends BaseService {
2057
2096
  { meta: { status: response.status, statusText: response.statusText } }
2058
2097
  ));
2059
2098
  }
2060
- const payload = this.normalizeBatchReadResponse(
2099
+ const items = this.normalizeBatchReadResponse(
2061
2100
  await response.json(),
2062
2101
  paths,
2063
2102
  action === KVAction.GET
2064
2103
  );
2065
- if (!payload) {
2104
+ if (!items) {
2066
2105
  return err(serviceError(
2067
2106
  ErrorCodes.NETWORK_ERROR,
2068
2107
  "KV batch read response did not match the requested keys",
2069
2108
  "kv"
2070
2109
  ));
2071
2110
  }
2072
- const results = payload.results.map((item, index) => {
2111
+ const results = items.map((item, index) => {
2073
2112
  if (!item.ok) {
2074
2113
  return {
2075
2114
  key: keys[index],
@@ -3996,7 +4035,7 @@ var AsyncQueue = class {
3996
4035
  }
3997
4036
  const waiter = this.waiters.shift();
3998
4037
  if (waiter) {
3999
- waiter({ value, done: false });
4038
+ waiter.resolve({ value, done: false });
4000
4039
  return;
4001
4040
  }
4002
4041
  this.values.push(value);
@@ -4008,7 +4047,22 @@ var AsyncQueue = class {
4008
4047
  this.closed = true;
4009
4048
  while (this.waiters.length > 0) {
4010
4049
  const waiter = this.waiters.shift();
4011
- waiter?.({ value: void 0, done: true });
4050
+ waiter?.resolve({ value: void 0, done: true });
4051
+ }
4052
+ }
4053
+ /**
4054
+ * Fail the queue: pending and future `next()` calls reject with `error`,
4055
+ * so a `for await` consumer sees the iterator throw.
4056
+ */
4057
+ fail(error) {
4058
+ if (this.closed) {
4059
+ return;
4060
+ }
4061
+ this.closed = true;
4062
+ this.failure = error;
4063
+ while (this.waiters.length > 0) {
4064
+ const waiter = this.waiters.shift();
4065
+ waiter?.reject(error);
4012
4066
  }
4013
4067
  }
4014
4068
  next() {
@@ -4016,20 +4070,28 @@ var AsyncQueue = class {
4016
4070
  const value = this.values.shift();
4017
4071
  return Promise.resolve({ value, done: false });
4018
4072
  }
4073
+ if (this.failure !== void 0) {
4074
+ return Promise.reject(this.failure);
4075
+ }
4019
4076
  if (this.closed) {
4020
4077
  return Promise.resolve({ value: void 0, done: true });
4021
4078
  }
4022
- return new Promise((resolve) => {
4023
- this.waiters.push(resolve);
4079
+ return new Promise((resolve, reject) => {
4080
+ this.waiters.push({ resolve, reject });
4024
4081
  });
4025
4082
  }
4026
4083
  };
4084
+ var HOOK_STREAM_RETRY_BASE_DELAY_MS = 250;
4085
+ var HOOK_STREAM_RETRY_MAX_DELAY_MS = 3e4;
4086
+ var HOOK_STREAM_HEALTHY_MS = 5e3;
4027
4087
  var HooksService = class extends BaseService {
4028
4088
  constructor(config = {}) {
4029
4089
  super();
4030
4090
  this._subscribers = /* @__PURE__ */ new Set();
4031
- this._refreshChain = Promise.resolve();
4032
- this._activeSignature = "";
4091
+ /** True while the one supervisor loop runs. Set only by wake(), cleared only at loop exit. */
4092
+ this._supervising = false;
4093
+ /** Bumped on every session change; part of the plan key. */
4094
+ this._sessionGeneration = 0;
4033
4095
  this._config = config;
4034
4096
  }
4035
4097
  get config() {
@@ -4045,35 +4107,32 @@ var HooksService = class extends BaseService {
4045
4107
  if (subscriptions.length === 0) {
4046
4108
  throw new Error("At least one hook subscription is required");
4047
4109
  }
4048
- const normalized = subscriptions.map(normalizeSubscription);
4110
+ const requested = subscriptions.map(normalizeSubscription);
4049
4111
  const subscriber = {
4050
- requested: normalized,
4112
+ requested,
4113
+ signatures: requested.map(subscriptionSignature),
4051
4114
  ttlSeconds: options.ttlSeconds,
4052
4115
  queue: new AsyncQueue()
4053
4116
  };
4054
- this._subscribers.add(subscriber);
4055
- const abortHandler = () => {
4117
+ const leave = () => {
4056
4118
  this._subscribers.delete(subscriber);
4057
4119
  subscriber.queue.close();
4058
- void this.scheduleSharedStreamRefresh();
4120
+ this.wake();
4059
4121
  };
4060
- if (options.signal) {
4061
- if (options.signal.aborted) {
4062
- abortHandler();
4063
- } else {
4064
- options.signal.addEventListener("abort", abortHandler, { once: true });
4065
- }
4122
+ const { signal } = options;
4123
+ if (signal?.aborted) {
4124
+ return;
4066
4125
  }
4067
- void this.scheduleSharedStreamRefresh();
4126
+ signal?.addEventListener("abort", leave, { once: true });
4127
+ this._subscribers.add(subscriber);
4128
+ this.wake();
4068
4129
  try {
4069
4130
  for await (const event of subscriber.queue) {
4070
4131
  yield event;
4071
4132
  }
4072
4133
  } finally {
4073
- if (options.signal) {
4074
- options.signal.removeEventListener("abort", abortHandler);
4075
- }
4076
- abortHandler();
4134
+ signal?.removeEventListener("abort", leave);
4135
+ leave();
4077
4136
  }
4078
4137
  }
4079
4138
  async register(webhook) {
@@ -4214,146 +4273,292 @@ var HooksService = class extends BaseService {
4214
4273
  return err(wrapError("hooks", error));
4215
4274
  }
4216
4275
  }
4217
- async scheduleSharedStreamRefresh() {
4218
- this._refreshChain = this._refreshChain.then(() => this.refreshSharedStream()).catch(() => void 0);
4219
- await this._refreshChain;
4220
- }
4221
- async refreshSharedStream() {
4222
- if (!this.requireAuth() || this._subscribers.size === 0) {
4223
- this.abortSharedStream();
4224
- this._activeSignature = "";
4225
- return;
4226
- }
4227
- const state = this.collectSharedStreamState();
4228
- if (state.signature !== this._activeSignature) {
4229
- this._activeSignature = state.signature;
4230
- this.abortSharedStream();
4276
+ /**
4277
+ * Called after anything that may change what the supervisor should do:
4278
+ * subscribe, unsubscribe, session change, sign-out. Interrupts the running
4279
+ * step if the plan moved away from it, and starts the supervisor if none
4280
+ * runs. Touches only SDK-owned state, so it never throws into callers such
4281
+ * as `ServiceContext.setSession`.
4282
+ */
4283
+ wake() {
4284
+ const phase = this._phase;
4285
+ if (phase && phase.key !== this.plan()?.key) {
4286
+ phase.interrupt.abort();
4231
4287
  }
4232
- if (!this._sharedStreamTask) {
4233
- this._sharedStreamTask = this.runSharedStream(state).catch((error) => {
4234
- if (!isAbortError(error)) {
4235
- throw error;
4236
- }
4237
- }).finally(() => {
4238
- this._sharedStreamTask = void 0;
4239
- this._sharedStreamAbort = void 0;
4240
- if (this._subscribers.size > 0) {
4241
- void this.scheduleSharedStreamRefresh();
4242
- }
4243
- });
4288
+ if (this._supervising || this._subscribers.size === 0) {
4289
+ return;
4244
4290
  }
4291
+ this._supervising = true;
4292
+ this.supervise().catch(() => this.collapse());
4245
4293
  }
4246
- collectSharedStreamState() {
4247
- const merged = /* @__PURE__ */ new Map();
4248
- const ttlCandidates = [];
4249
- for (const subscriber of this._subscribers) {
4250
- if (typeof subscriber.ttlSeconds === "number") {
4251
- ttlCandidates.push(subscriber.ttlSeconds);
4294
+ /**
4295
+ * The only owner of the shared stream. Each iteration is one attempt
4296
+ * (mint → open → drain), then a classification and, unless the attempt was
4297
+ * superseded or released its subscribers, a backoff. Every await is a
4298
+ * `phase` that wake() can interrupt; an interrupted phase yields
4299
+ * `undefined`, so a superseded mint can never act. Outside `attempt` and
4300
+ * `pause`, which each own one catch, the loop touches only SDK state.
4301
+ */
4302
+ async supervise() {
4303
+ let streak = 0;
4304
+ for (; ; ) {
4305
+ const plan = this.plan();
4306
+ if (!plan) {
4307
+ this._supervising = false;
4308
+ return;
4309
+ }
4310
+ const health = { openedAt: 0, delivered: false };
4311
+ const outcome = await this.phase(
4312
+ plan.key,
4313
+ (signal) => this.attempt(plan, signal, health)
4314
+ );
4315
+ if (health.delivered || health.openedAt > 0 && performance.now() - health.openedAt >= HOOK_STREAM_HEALTHY_MS) {
4316
+ streak = 0;
4317
+ }
4318
+ if (!outcome) {
4319
+ continue;
4320
+ }
4321
+ if (this.plan()?.key !== plan.key) {
4322
+ continue;
4323
+ }
4324
+ if (outcome.kind === "stopped") {
4325
+ this.release();
4326
+ continue;
4327
+ }
4328
+ if (outcome.kind === "refused" && outcome.terminal) {
4329
+ this.release(outcome.error);
4330
+ this.report(outcome.error);
4331
+ continue;
4252
4332
  }
4253
- for (const subscription of subscriber.requested) {
4254
- merged.set(subscriptionSignature(subscription), subscription);
4333
+ if (outcome.kind !== "ended") {
4334
+ this.report(outcome.error);
4255
4335
  }
4336
+ streak += 1;
4337
+ await this.phase(
4338
+ plan.key,
4339
+ (signal) => this.pause(streak, signal)
4340
+ );
4341
+ }
4342
+ }
4343
+ /**
4344
+ * Run one step under an interrupt that wake() fires when the plan key moves
4345
+ * on. An interrupted step's result is discarded. The interrupt is also
4346
+ * fired when the step returns, which releases whatever it left open: the
4347
+ * request signal and its listeners, an unread response body, a timer.
4348
+ */
4349
+ async phase(key, step) {
4350
+ const interrupt = new AbortController();
4351
+ this._phase = { key, interrupt };
4352
+ try {
4353
+ if (this.plan()?.key !== key) {
4354
+ interrupt.abort();
4355
+ }
4356
+ const result = await step(interrupt.signal);
4357
+ return interrupt.signal.aborted ? void 0 : result;
4358
+ } finally {
4359
+ this._phase = void 0;
4360
+ interrupt.abort();
4256
4361
  }
4257
- const subscriptions = [...merged.values()].sort(
4258
- (left, right) => subscriptionSignature(left).localeCompare(subscriptionSignature(right))
4259
- );
4260
- const ttlSeconds = ttlCandidates.length > 0 ? Math.min(...ttlCandidates) : void 0;
4261
- const signature = JSON.stringify({
4262
- subscriptions: subscriptions.map(subscriptionSignature),
4263
- ttlSeconds
4264
- });
4265
- return {
4266
- subscriptions,
4267
- ttlSeconds,
4268
- signature
4269
- };
4270
4362
  }
4271
- async runSharedStream(state) {
4272
- const abortController = new AbortController();
4273
- this._sharedStreamAbort = abortController;
4363
+ /**
4364
+ * One mint → open → drain. The single catch site for everything the
4365
+ * attempt touches that the SDK does not own: context accessors, invoke,
4366
+ * fetch, response bodies, the SSE parser. Never rejects.
4367
+ */
4368
+ async attempt(plan, signal, health) {
4369
+ let phase = "mint";
4370
+ let request;
4371
+ const owns = () => !signal.aborted && !request?.signal.aborted && this.plan()?.key === plan.key;
4274
4372
  try {
4275
- const host = this._config.host ?? this.context.hosts[0];
4276
- const ticketResponse = await this.mintHookTicket(
4277
- state.subscriptions,
4278
- state.ttlSeconds,
4279
- abortController.signal
4373
+ const context = this.context;
4374
+ const contextSignal = context.abortSignal;
4375
+ this._lastContextSignal = contextSignal;
4376
+ if (!context.isAuthenticated || contextSignal?.aborted) {
4377
+ return { kind: "stopped" };
4378
+ }
4379
+ request = this.createRequestSignal(signal, 0);
4380
+ const host = this.host;
4381
+ const minted = await awaitOwned(
4382
+ request.signal,
4383
+ () => context.fetch(`${host}/hooks/tickets`, {
4384
+ method: "POST",
4385
+ headers: {
4386
+ ...serviceHeadersToRecord(this.createInvokeHeaders(plan.subscriptions)),
4387
+ "content-type": "application/json"
4388
+ },
4389
+ body: JSON.stringify({
4390
+ subscriptions: plan.subscriptions,
4391
+ ttlSeconds: plan.ttlSeconds
4392
+ }),
4393
+ signal: request.signal
4394
+ })
4280
4395
  );
4281
- const streamResponse = await this.openHookStream(
4282
- host,
4283
- ticketResponse.ticket,
4284
- abortController.signal
4396
+ if (!owns()) return { kind: "stopped" };
4397
+ if (!minted.ok) {
4398
+ const status = minted.status;
4399
+ const error = await awaitOwned(
4400
+ request.signal,
4401
+ () => responseError("hooks", "failed to mint hook ticket", minted)
4402
+ );
4403
+ if (!owns()) return { kind: "stopped" };
4404
+ return { kind: "refused", terminal: isTerminalMintRefusal(status), error };
4405
+ }
4406
+ const body = await awaitOwned(request.signal, () => minted.json());
4407
+ if (!owns()) return { kind: "stopped" };
4408
+ const ticket = body !== null && typeof body === "object" && "ticket" in body ? body.ticket : void 0;
4409
+ if (typeof ticket !== "string" || ticket.length === 0) {
4410
+ throw new Error("Hook ticket response did not include a ticket");
4411
+ }
4412
+ phase = "open";
4413
+ if (!owns()) return { kind: "stopped" };
4414
+ const stream = await awaitOwned(
4415
+ request.signal,
4416
+ () => context.fetch(`${host}/hooks/events?ticket=${encodeURIComponent(ticket)}`, {
4417
+ method: "GET",
4418
+ headers: { accept: "text/event-stream" },
4419
+ signal: request.signal
4420
+ })
4285
4421
  );
4286
- for await (const message of parseSseStream(
4287
- streamResponse.body,
4288
- abortController.signal
4289
- )) {
4290
- if (!message.data) {
4291
- continue;
4292
- }
4293
- const event = parseHookEvent(message);
4294
- for (const subscriber of this._subscribers) {
4295
- if (matchesAnySubscription(event, subscriber.requested)) {
4296
- subscriber.queue.push(event);
4297
- }
4422
+ if (!owns()) return { kind: "stopped" };
4423
+ if (!stream.ok) {
4424
+ return { kind: "refused", terminal: false, error: hookStreamRefusal(stream.status) };
4425
+ }
4426
+ health.openedAt = performance.now();
4427
+ const iterator = parseSseStream(stream.body, request.signal)[Symbol.asyncIterator]();
4428
+ try {
4429
+ for (; ; ) {
4430
+ const next = await awaitOwned(request.signal, () => iterator.next());
4431
+ if (!owns()) return { kind: "stopped" };
4432
+ if (next.done) break;
4433
+ const message = next.value;
4434
+ if (!message.data) continue;
4435
+ const event = parseHookEvent(message);
4436
+ if (!owns()) return { kind: "stopped" };
4437
+ this.dispatch(event);
4438
+ health.delivered = true;
4298
4439
  }
4440
+ } finally {
4441
+ const closing = iterator.return?.();
4442
+ if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
4299
4443
  }
4444
+ return owns() ? { kind: "ended" } : { kind: "stopped" };
4445
+ } catch (thrown) {
4446
+ return request?.signal.aborted || thrown === SUPERSEDED ? { kind: "stopped" } : { kind: "failed", error: hookStreamFailure(phase, thrown) };
4447
+ }
4448
+ }
4449
+ /**
4450
+ * Back off before the next attempt. `config.streamRetry` overrides the
4451
+ * delay and the clock; whatever they do, the pause ends by the delay or the
4452
+ * interrupt, then yields one timer turn so an instant clock cannot starve
4453
+ * due timers or the process.
4454
+ */
4455
+ async pause(attempt, signal) {
4456
+ try {
4457
+ this._lastContextSignal = this.context.abortSignal;
4458
+ } catch {
4459
+ }
4460
+ const contextSignal = this._lastContextSignal;
4461
+ const waits = [];
4462
+ const aborts = [];
4463
+ try {
4464
+ const retry = this._config.streamRetry;
4465
+ const delayMs = retry?.delay?.(attempt) ?? hookStreamRetryDelay(attempt);
4466
+ waits.push((retry?.wait ?? sleep)(delayMs, attempt, signal));
4467
+ } catch {
4468
+ waits.push(Promise.resolve());
4469
+ }
4470
+ aborts.push(abortPromise(signal));
4471
+ if (contextSignal) aborts.push(abortPromise(contextSignal));
4472
+ try {
4473
+ await Promise.race([...waits, ...aborts.map(({ promise }) => promise)]);
4474
+ } catch {
4300
4475
  } finally {
4301
- if (this._sharedStreamAbort === abortController) {
4302
- this._sharedStreamAbort = void 0;
4476
+ for (const abort of aborts) abort.dispose();
4477
+ }
4478
+ await new Promise((resolve) => setTimeout(resolve, 0));
4479
+ }
4480
+ /** The stream the current subscribers need, or undefined when none remain. */
4481
+ plan() {
4482
+ if (this._subscribers.size === 0) {
4483
+ return void 0;
4484
+ }
4485
+ const merged = /* @__PURE__ */ new Map();
4486
+ let ttlSeconds;
4487
+ for (const subscriber of this._subscribers) {
4488
+ subscriber.requested.forEach(
4489
+ (subscription, index) => merged.set(subscriber.signatures[index], subscription)
4490
+ );
4491
+ if (typeof subscriber.ttlSeconds === "number") {
4492
+ ttlSeconds = Math.min(subscriber.ttlSeconds, ttlSeconds ?? Infinity);
4303
4493
  }
4304
4494
  }
4495
+ const signatures = [...merged.keys()].sort();
4496
+ return {
4497
+ key: JSON.stringify([this._sessionGeneration, signatures, ttlSeconds ?? null]),
4498
+ subscriptions: signatures.map((signature) => merged.get(signature)),
4499
+ ttlSeconds
4500
+ };
4305
4501
  }
4306
- abortSharedStream() {
4307
- this._sharedStreamAbort?.abort();
4502
+ dispatch(event) {
4503
+ for (const subscriber of this._subscribers) {
4504
+ if (matchesAnySubscription(event, subscriber.requested)) {
4505
+ subscriber.queue.push(event);
4506
+ }
4507
+ }
4308
4508
  }
4309
- createHookHeaders(action, path) {
4310
- return this.context.invoke(this.session, "hooks", path, action);
4509
+ /**
4510
+ * Detach every subscriber. With `error`, their iterators throw it after
4511
+ * draining buffered events; without, they complete.
4512
+ */
4513
+ release(error) {
4514
+ const subscribers = [...this._subscribers];
4515
+ this._subscribers.clear();
4516
+ for (const { queue } of subscribers) {
4517
+ if (error) {
4518
+ queue.fail(error);
4519
+ } else {
4520
+ queue.close();
4521
+ }
4522
+ }
4311
4523
  }
4312
- async mintHookTicket(subscriptions, ttlSeconds, signal) {
4313
- const host = this._config.host ?? this.context.hosts[0];
4314
- const headers = this.createInvokeHeaders(subscriptions);
4315
- const ticketResponse = await this.context.fetch(`${host}/hooks/tickets`, {
4316
- method: "POST",
4317
- headers: {
4318
- ...serviceHeadersToRecord(headers),
4319
- "content-type": "application/json"
4320
- },
4321
- body: JSON.stringify({
4322
- subscriptions,
4323
- ttlSeconds
4324
- }),
4325
- signal
4326
- });
4327
- if (!ticketResponse.ok) {
4328
- throw await responseError(
4329
- "hooks",
4330
- "failed to mint hook ticket",
4331
- ticketResponse
4332
- );
4524
+ report(error) {
4525
+ if (error.meta && typeof error.meta === "object") {
4526
+ Object.freeze(error.meta);
4333
4527
  }
4334
- const ticketJson = await ticketResponse.json();
4335
- if (!ticketJson?.ticket) {
4336
- throw new Error("Hook ticket response did not include a ticket");
4528
+ Object.freeze(error);
4529
+ try {
4530
+ this.emitError(error, "stream");
4531
+ } catch {
4337
4532
  }
4338
- return ticketJson;
4339
4533
  }
4340
- async openHookStream(host, ticket, signal) {
4341
- const streamResponse = await this.context.fetch(
4342
- `${host}/hooks/events?ticket=${encodeURIComponent(ticket)}`,
4343
- {
4344
- method: "GET",
4345
- headers: { accept: "text/event-stream" },
4346
- signal
4347
- }
4534
+ /**
4535
+ * Unreachable by construction. If the loop ever rejects anyway, fail its
4536
+ * subscribers visibly instead of stranding them or crashing the process.
4537
+ */
4538
+ collapse() {
4539
+ this._supervising = false;
4540
+ this._phase?.interrupt.abort();
4541
+ this._phase = void 0;
4542
+ const error = serviceError(
4543
+ ErrorCodes.NETWORK_ERROR,
4544
+ "hook stream supervisor stopped unexpectedly",
4545
+ "hooks"
4348
4546
  );
4349
- if (!streamResponse.ok) {
4350
- throw await responseError(
4351
- "hooks",
4352
- "failed to open hook stream",
4353
- streamResponse
4354
- );
4355
- }
4356
- return streamResponse;
4547
+ this.report(error);
4548
+ this.release(error);
4549
+ }
4550
+ onSessionChange(session) {
4551
+ super.onSessionChange(session);
4552
+ this._sessionGeneration += 1;
4553
+ this.wake();
4554
+ }
4555
+ onSignOut() {
4556
+ super.onSignOut();
4557
+ this.release();
4558
+ this.wake();
4559
+ }
4560
+ createHookHeaders(action, path) {
4561
+ return this.context.invoke(this.session, "hooks", path, action);
4357
4562
  }
4358
4563
  createInvokeHeaders(subscriptions) {
4359
4564
  const entries = subscriptions.map((subscription) => ({
@@ -4384,11 +4589,168 @@ function buildScopePath(service, pathPrefix) {
4384
4589
  const normalized = normalizePathPrefix(pathPrefix);
4385
4590
  return normalized ? `${service}/${normalized}` : service;
4386
4591
  }
4592
+ function hookStreamRetryDelay(attempt) {
4593
+ const cap = Math.min(
4594
+ HOOK_STREAM_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1),
4595
+ HOOK_STREAM_RETRY_MAX_DELAY_MS
4596
+ );
4597
+ return cap / 2 + Math.random() * (cap / 2);
4598
+ }
4599
+ function isTerminalMintRefusal(status) {
4600
+ return typeof status === "number" && status >= 400 && status < 500 && status !== 408 && status !== 429;
4601
+ }
4602
+ var SUPERSEDED = /* @__PURE__ */ Symbol("hooks attempt superseded");
4603
+ async function awaitOwned(signal, operation) {
4604
+ if (signal.aborted) {
4605
+ if (typeof operation !== "function") {
4606
+ void operation.then(cancelLateResponse, () => void 0);
4607
+ }
4608
+ throw SUPERSEDED;
4609
+ }
4610
+ const promise = typeof operation === "function" ? operation() : operation;
4611
+ return new Promise((resolve, reject) => {
4612
+ let settled = false;
4613
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
4614
+ const onAbort = () => {
4615
+ if (settled) return;
4616
+ settled = true;
4617
+ cleanup();
4618
+ reject(SUPERSEDED);
4619
+ };
4620
+ signal.addEventListener("abort", onAbort, { once: true });
4621
+ promise.then(
4622
+ (value) => {
4623
+ if (settled) {
4624
+ cancelLateResponse(value);
4625
+ return;
4626
+ }
4627
+ settled = true;
4628
+ cleanup();
4629
+ resolve(value);
4630
+ },
4631
+ (error) => {
4632
+ if (settled) return;
4633
+ settled = true;
4634
+ cleanup();
4635
+ reject(error);
4636
+ }
4637
+ );
4638
+ });
4639
+ }
4640
+ function cancelLateResponse(value) {
4641
+ if (!(value instanceof Response) || !value.body) return;
4642
+ try {
4643
+ const cancellation = value.body.cancel();
4644
+ void cancellation.then(() => void 0, () => void 0);
4645
+ } catch {
4646
+ }
4647
+ }
4648
+ function hookStreamRefusal(status) {
4649
+ const code = typeof status === "number" ? status : void 0;
4650
+ return serviceError(
4651
+ ErrorCodes.NETWORK_ERROR,
4652
+ `hook stream open refused: ${code ?? "unknown status"}`,
4653
+ "hooks",
4654
+ { meta: { phase: "open", status: code } }
4655
+ );
4656
+ }
4657
+ var STREAM_ERROR_NAMES = /* @__PURE__ */ new Set([
4658
+ "Error",
4659
+ "TypeError",
4660
+ "AbortError",
4661
+ "TimeoutError",
4662
+ "SyntaxError",
4663
+ "RangeError",
4664
+ "NetworkError"
4665
+ ]);
4666
+ function hookStreamFailure(phase, thrown) {
4667
+ const candidateName = readProperty(thrown, "name");
4668
+ const name = typeof candidateName === "string" && STREAM_ERROR_NAMES.has(candidateName) ? candidateName : "Error";
4669
+ const code = identifier(readProperty(thrown, "code")) ?? identifier(readProperty(readProperty(thrown, "cause"), "code"));
4670
+ return serviceError(
4671
+ ErrorCodes.NETWORK_ERROR,
4672
+ `hook stream ${phase} failed: ${name}${code ? ` (${code})` : ""}`,
4673
+ "hooks",
4674
+ { meta: { phase, errorName: name, ...code ? { errorCode: code } : {} } }
4675
+ );
4676
+ }
4677
+ var STREAM_ERROR_CODES = /* @__PURE__ */ new Set([
4678
+ "ECONNREFUSED",
4679
+ "ECONNRESET",
4680
+ "ECONNABORTED",
4681
+ "ETIMEDOUT",
4682
+ "EPIPE",
4683
+ "ENOTFOUND",
4684
+ "EAI_AGAIN",
4685
+ "EHOSTUNREACH",
4686
+ "ENETUNREACH",
4687
+ "ENETDOWN",
4688
+ "EADDRNOTAVAIL",
4689
+ "UND_ERR_SOCKET",
4690
+ "UND_ERR_CONNECT_TIMEOUT",
4691
+ "UND_ERR_HEADERS_TIMEOUT",
4692
+ "UND_ERR_BODY_TIMEOUT",
4693
+ "UND_ERR_CLOSED",
4694
+ "UND_ERR_ABORTED",
4695
+ "UND_ERR_RESPONSE_STATUS_CODE",
4696
+ "ERR_TLS_CERT_ALTNAME_INVALID",
4697
+ "CERT_HAS_EXPIRED",
4698
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
4699
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
4700
+ "SELF_SIGNED_CERT_IN_CHAIN",
4701
+ "ERR_SSL_WRONG_VERSION_NUMBER"
4702
+ ]);
4703
+ function identifier(value) {
4704
+ return typeof value === "string" && STREAM_ERROR_CODES.has(value) ? value : void 0;
4705
+ }
4706
+ function readProperty(value, key) {
4707
+ if (value === null || typeof value !== "object" && typeof value !== "function") {
4708
+ return void 0;
4709
+ }
4710
+ try {
4711
+ return Reflect.get(value, key);
4712
+ } catch {
4713
+ return void 0;
4714
+ }
4715
+ }
4716
+ function sleep(delayMs, _attempt, signal) {
4717
+ return new Promise((resolve) => {
4718
+ const finish = () => {
4719
+ clearTimeout(timer);
4720
+ signal.removeEventListener("abort", stop);
4721
+ resolve();
4722
+ };
4723
+ const timer = setTimeout(finish, delayMs);
4724
+ timer.unref?.();
4725
+ const stop = () => finish();
4726
+ if (signal.aborted) {
4727
+ finish();
4728
+ } else {
4729
+ signal.addEventListener("abort", stop, { once: true });
4730
+ }
4731
+ });
4732
+ }
4733
+ function abortPromise(signal) {
4734
+ let resolve;
4735
+ const promise = new Promise((done) => {
4736
+ resolve = done;
4737
+ });
4738
+ const onAbort = () => resolve();
4739
+ if (signal.aborted) {
4740
+ resolve();
4741
+ } else {
4742
+ signal.addEventListener("abort", onAbort, { once: true });
4743
+ }
4744
+ return {
4745
+ promise,
4746
+ dispose: () => signal.removeEventListener("abort", onAbort)
4747
+ };
4748
+ }
4387
4749
  function normalizeSubscription(subscription) {
4388
4750
  return {
4389
4751
  ...subscription,
4390
4752
  pathPrefix: normalizePathPrefix(subscription.pathPrefix),
4391
- abilities: subscription.abilities ?? []
4753
+ abilities: subscription.abilities ? [...subscription.abilities] : []
4392
4754
  };
4393
4755
  }
4394
4756
  function subscriptionSignature(subscription) {
@@ -4454,9 +4816,6 @@ async function responseError(service, message, response) {
4454
4816
  meta: { ...error.meta, status: response.status, statusText: response.statusText }
4455
4817
  };
4456
4818
  }
4457
- function isAbortError(error) {
4458
- return error instanceof DOMException && error.name === "AbortError";
4459
- }
4460
4819
  async function* parseSseStream(body, signal) {
4461
4820
  if (!body) {
4462
4821
  throw new Error("Hook stream response does not expose a readable body");
@@ -4485,11 +4844,24 @@ async function* parseSseStream(body, signal) {
4485
4844
  async function* readBodyChunks(body, signal) {
4486
4845
  const asyncIterable = body;
4487
4846
  if (typeof asyncIterable?.[Symbol.asyncIterator] === "function") {
4488
- for await (const chunk of asyncIterable) {
4489
- if (signal?.aborted) {
4490
- break;
4847
+ const iterator = asyncIterable[Symbol.asyncIterator]();
4848
+ const cancel = () => {
4849
+ try {
4850
+ const closing = iterator.return?.();
4851
+ if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
4852
+ } catch {
4853
+ }
4854
+ };
4855
+ signal?.addEventListener("abort", cancel, { once: true });
4856
+ try {
4857
+ while (!signal?.aborted) {
4858
+ const next = await iterator.next();
4859
+ if (next.done) break;
4860
+ yield next.value;
4491
4861
  }
4492
- yield chunk;
4862
+ } finally {
4863
+ signal?.removeEventListener("abort", cancel);
4864
+ cancel();
4493
4865
  }
4494
4866
  return;
4495
4867
  }
@@ -4498,21 +4870,23 @@ async function* readBodyChunks(body, signal) {
4498
4870
  throw new Error("Unsupported hook stream body type");
4499
4871
  }
4500
4872
  const reader = stream.getReader();
4873
+ const cancelReader = () => {
4874
+ try {
4875
+ const cancellation = reader.cancel?.();
4876
+ if (cancellation) void cancellation.then(() => void 0, () => void 0);
4877
+ } catch {
4878
+ }
4879
+ };
4880
+ signal?.addEventListener("abort", cancelReader, { once: true });
4501
4881
  try {
4502
4882
  while (!signal?.aborted) {
4503
4883
  const { done, value } = await reader.read();
4504
- if (done) {
4505
- break;
4506
- }
4507
- if (value) {
4508
- yield value;
4509
- }
4884
+ if (done) break;
4885
+ if (value) yield value;
4510
4886
  }
4511
4887
  } finally {
4512
- try {
4513
- await reader.cancel?.();
4514
- } catch {
4515
- }
4888
+ signal?.removeEventListener("abort", cancelReader);
4889
+ cancelReader();
4516
4890
  reader.releaseLock?.();
4517
4891
  }
4518
4892
  }
@@ -7156,10 +7530,10 @@ var EncryptionService = class extends BaseService {
7156
7530
  get crypto() {
7157
7531
  return this._config.crypto;
7158
7532
  }
7159
- async discoverNetwork(identifier, ownerDid) {
7533
+ async discoverNetwork(identifier2, ownerDid) {
7160
7534
  this.assertActive();
7161
7535
  const result = await discoverNetwork({
7162
- identifier,
7536
+ identifier: identifier2,
7163
7537
  ...ownerDid !== void 0 ? { ownerDid } : {},
7164
7538
  ...this._config.node !== void 0 ? { node: this._config.node } : {},
7165
7539
  ...this._config.wellKnown !== void 0 ? { wellKnown: this._config.wellKnown } : {}