@unicitylabs/sphere-sdk 0.14.11-dev.5 → 0.14.11-dev.7

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.
@@ -456,8 +456,22 @@ interface DeliveryPort {
456
456
  */
457
457
  incomingEpoch(): string | null;
458
458
  ack(deliveryId: string, disposition: 'claimed' | 'rejected', reason?: 'invalid' | 'not-owned' | 'storage-rejected' | 'other'): Promise<void>;
459
+ /** Settle many acks at once (#757). Contract pinned by the delivery-port contract suite. */
460
+ ackBatch?(acks: readonly AckRequest[]): Promise<readonly AckOutcome[]>;
459
461
  onWake?(cb: () => void): () => void;
460
462
  }
463
+ interface AckRequest {
464
+ readonly deliveryId: string;
465
+ readonly disposition: 'claimed' | 'rejected';
466
+ readonly reason?: 'invalid' | 'not-owned' | 'storage-rejected' | 'other';
467
+ }
468
+ type AckOutcome = {
469
+ readonly deliveryId: string;
470
+ readonly status: 'settled';
471
+ } | {
472
+ readonly deliveryId: string;
473
+ readonly status: 'conflict';
474
+ };
461
475
 
462
476
  type StoragePortClient = Pick<WalletApiV2Client, 'listInventory' | 'blobUrls' | 'uploadUrls' | 'apply' | 'fetchBlob' | 'uploadBlob'>;
463
477
  declare class WalletApiStoragePort implements StoragePort {
@@ -528,9 +542,19 @@ declare class WalletApiDeliveryPort implements DeliveryPort {
528
542
  private openEnvelope;
529
543
  private fetchVerified;
530
544
  ack(deliveryId: string, disposition: 'claimed' | 'rejected', reason?: 'invalid' | 'not-owned' | 'storage-rejected' | 'other'): Promise<void>;
545
+ /**
546
+ * §5.7/#757 batched settle. Claims go out grouped, rejects grouped by reason
547
+ * (the endpoint takes one reason per call), and the seen-set is written ONCE
548
+ * after every HTTP call has succeeded — never on a throw, so an aborted batch
549
+ * re-lists rather than being silently forgotten.
550
+ */
551
+ ackBatch(acks: readonly AckRequest[]): Promise<readonly AckOutcome[]>;
552
+ private settleClaims;
553
+ private settleRejects;
531
554
  onWake(cb: () => void): () => void;
532
555
  private readSeen;
533
- private addSeen;
556
+ /** SerialChain settles-not-fulfills: a rejected write never bricks later acks. */
557
+ private addSeenAll;
534
558
  }
535
559
 
536
560
  /**
@@ -456,8 +456,22 @@ interface DeliveryPort {
456
456
  */
457
457
  incomingEpoch(): string | null;
458
458
  ack(deliveryId: string, disposition: 'claimed' | 'rejected', reason?: 'invalid' | 'not-owned' | 'storage-rejected' | 'other'): Promise<void>;
459
+ /** Settle many acks at once (#757). Contract pinned by the delivery-port contract suite. */
460
+ ackBatch?(acks: readonly AckRequest[]): Promise<readonly AckOutcome[]>;
459
461
  onWake?(cb: () => void): () => void;
460
462
  }
463
+ interface AckRequest {
464
+ readonly deliveryId: string;
465
+ readonly disposition: 'claimed' | 'rejected';
466
+ readonly reason?: 'invalid' | 'not-owned' | 'storage-rejected' | 'other';
467
+ }
468
+ type AckOutcome = {
469
+ readonly deliveryId: string;
470
+ readonly status: 'settled';
471
+ } | {
472
+ readonly deliveryId: string;
473
+ readonly status: 'conflict';
474
+ };
461
475
 
462
476
  type StoragePortClient = Pick<WalletApiV2Client, 'listInventory' | 'blobUrls' | 'uploadUrls' | 'apply' | 'fetchBlob' | 'uploadBlob'>;
463
477
  declare class WalletApiStoragePort implements StoragePort {
@@ -528,9 +542,19 @@ declare class WalletApiDeliveryPort implements DeliveryPort {
528
542
  private openEnvelope;
529
543
  private fetchVerified;
530
544
  ack(deliveryId: string, disposition: 'claimed' | 'rejected', reason?: 'invalid' | 'not-owned' | 'storage-rejected' | 'other'): Promise<void>;
545
+ /**
546
+ * §5.7/#757 batched settle. Claims go out grouped, rejects grouped by reason
547
+ * (the endpoint takes one reason per call), and the seen-set is written ONCE
548
+ * after every HTTP call has succeeded — never on a throw, so an aborted batch
549
+ * re-lists rather than being silently forgotten.
550
+ */
551
+ ackBatch(acks: readonly AckRequest[]): Promise<readonly AckOutcome[]>;
552
+ private settleClaims;
553
+ private settleRejects;
531
554
  onWake(cb: () => void): () => void;
532
555
  private readSeen;
533
- private addSeen;
556
+ /** SerialChain settles-not-fulfills: a rejected write never bricks later acks. */
557
+ private addSeenAll;
534
558
  }
535
559
 
536
560
  /**
@@ -12,6 +12,10 @@ var WalletApiHttpError = class extends Error {
12
12
  if (retryAfter !== void 0) this.retryAfter = retryAfter;
13
13
  }
14
14
  };
15
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([0, 408, 425, 429, 500, 502, 503, 504]);
16
+ function isRetryableStatus(err) {
17
+ return err instanceof WalletApiHttpError && RETRYABLE_STATUSES.has(err.status);
18
+ }
15
19
  function isWalletApiHttpError(err, status) {
16
20
  return err instanceof WalletApiHttpError && (status === void 0 || err.status === status);
17
21
  }
@@ -4049,6 +4053,23 @@ function computeDeliveryId(tokenIdHex, stateHashHex) {
4049
4053
  return bytesToHex3(sha256(joined));
4050
4054
  }
4051
4055
  var DELIVERY_SEEN_KEY = "delivery:seen";
4056
+ var ACK_CHUNK = 50;
4057
+ function chunked(items, size) {
4058
+ const out = [];
4059
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
4060
+ return out;
4061
+ }
4062
+ var RetryableAckFailure = class extends Error {
4063
+ retryable = true;
4064
+ constructor(cause) {
4065
+ super(`mailbox ack batch could not be attempted: ${String(cause)}`);
4066
+ this.name = "RetryableAckFailure";
4067
+ this.cause = cause;
4068
+ }
4069
+ };
4070
+ function asRetryable(err) {
4071
+ return isRetryableStatus(err) ? new RetryableAckFailure(err) : err;
4072
+ }
4052
4073
  var WalletApiDeliveryPort = class {
4053
4074
  custody;
4054
4075
  client;
@@ -4214,7 +4235,53 @@ var WalletApiDeliveryPort = class {
4214
4235
  } else {
4215
4236
  await this.client.reject([deliveryId], reason);
4216
4237
  }
4217
- await this.addSeen(deliveryId);
4238
+ await this.addSeenAll([deliveryId]);
4239
+ }
4240
+ /**
4241
+ * §5.7/#757 batched settle. Claims go out grouped, rejects grouped by reason
4242
+ * (the endpoint takes one reason per call), and the seen-set is written ONCE
4243
+ * after every HTTP call has succeeded — never on a throw, so an aborted batch
4244
+ * re-lists rather than being silently forgotten.
4245
+ */
4246
+ async ackBatch(acks) {
4247
+ const settled = [];
4248
+ const outcomes = [];
4249
+ try {
4250
+ await this.settleClaims(acks, settled, outcomes);
4251
+ await this.settleRejects(acks, settled);
4252
+ } catch (err) {
4253
+ throw asRetryable(err);
4254
+ }
4255
+ await this.addSeenAll(settled);
4256
+ return [...outcomes, ...settled.map((deliveryId) => ({ deliveryId, status: "settled" }))];
4257
+ }
4258
+ async settleClaims(acks, settled, outcomes) {
4259
+ const ids = acks.filter((a) => a.disposition === "claimed").map((a) => a.deliveryId);
4260
+ for (const chunk of chunked(ids, ACK_CHUNK)) {
4261
+ const result = await this.client.claim(chunk, this.custody === "inventory");
4262
+ settled.push(...result.claimed);
4263
+ settled.push(...result.alreadyClaimed.map((e) => e.entryId));
4264
+ for (const f of result.failed) {
4265
+ if (f.code === "CONFLICT") outcomes.push({ deliveryId: f.entryId, status: "conflict" });
4266
+ else throw new MailboxClaimFailedError(f.entryId, f.code);
4267
+ }
4268
+ }
4269
+ }
4270
+ async settleRejects(acks, settled) {
4271
+ const byReason = /* @__PURE__ */ new Map();
4272
+ for (const a of acks) {
4273
+ if (a.disposition !== "rejected") continue;
4274
+ const key = a.reason ?? "";
4275
+ const group = byReason.get(key) ?? [];
4276
+ group.push(a.deliveryId);
4277
+ byReason.set(key, group);
4278
+ }
4279
+ for (const [reason, group] of byReason) {
4280
+ for (const chunk of chunked(group, ACK_CHUNK)) {
4281
+ await this.client.reject(chunk, reason === "" ? void 0 : reason);
4282
+ settled.push(...chunk);
4283
+ }
4284
+ }
4218
4285
  }
4219
4286
  onWake(cb) {
4220
4287
  if (this.wake === null) return () => void 0;
@@ -4225,10 +4292,12 @@ var WalletApiDeliveryPort = class {
4225
4292
  const raw = await this.kv.get(DELIVERY_SEEN_KEY);
4226
4293
  return new Set(raw ?? []);
4227
4294
  }
4228
- addSeen(deliveryId) {
4295
+ /** SerialChain settles-not-fulfills: a rejected write never bricks later acks. */
4296
+ addSeenAll(deliveryIds) {
4297
+ if (deliveryIds.length === 0) return Promise.resolve();
4229
4298
  return this.seenChain.enqueue(async () => {
4230
4299
  const seen = await this.readSeen();
4231
- seen.add(deliveryId);
4300
+ for (const id of deliveryIds) seen.add(id);
4232
4301
  await this.kv.set(DELIVERY_SEEN_KEY, [...seen]);
4233
4302
  });
4234
4303
  }