@adcp/sdk 14.1.0 → 14.2.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 (106) hide show
  1. package/dist/lib/core/SingleAgentClient.d.mts +4 -1
  2. package/dist/lib/core/SingleAgentClient.d.ts +4 -1
  3. package/dist/lib/core/SingleAgentClient.js +25 -4
  4. package/dist/lib/core/SingleAgentClient.mjs +25 -4
  5. package/dist/lib/core/TaskExecutor.d.mts +3 -1
  6. package/dist/lib/core/TaskExecutor.d.ts +3 -1
  7. package/dist/lib/core/TaskExecutor.js +15 -11
  8. package/dist/lib/core/TaskExecutor.mjs +15 -11
  9. package/dist/lib/core/buyer-account-registry.d.mts +30 -2
  10. package/dist/lib/core/buyer-account-registry.d.ts +30 -2
  11. package/dist/lib/core/buyer-account-registry.js +126 -42
  12. package/dist/lib/core/buyer-account-registry.mjs +127 -43
  13. package/dist/lib/index.d.mts +3 -2
  14. package/dist/lib/index.d.ts +3 -2
  15. package/dist/lib/index.js +9 -0
  16. package/dist/lib/index.mjs +12 -1
  17. package/dist/lib/net/agent-transport-fetch.d.mts +4 -0
  18. package/dist/lib/net/agent-transport-fetch.d.ts +4 -0
  19. package/dist/lib/net/agent-transport-fetch.js +18 -5
  20. package/dist/lib/net/agent-transport-fetch.mjs +17 -5
  21. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  22. package/dist/lib/signing/agent-resolver/errors.d.mts +2 -0
  23. package/dist/lib/signing/agent-resolver/errors.d.ts +2 -0
  24. package/dist/lib/signing/agent-resolver/errors.js +6 -0
  25. package/dist/lib/signing/agent-resolver/errors.mjs +6 -0
  26. package/dist/lib/signing/agent-resolver/fetch-helpers.d.mts +9 -0
  27. package/dist/lib/signing/agent-resolver/fetch-helpers.d.ts +9 -0
  28. package/dist/lib/signing/agent-resolver/fetch-helpers.js +35 -1
  29. package/dist/lib/signing/agent-resolver/fetch-helpers.mjs +38 -2
  30. package/dist/lib/signing/agent-resolver/legacy-brand.js +1 -1
  31. package/dist/lib/signing/agent-resolver/legacy-brand.mjs +2 -2
  32. package/dist/lib/signing/agent-resolver/resolve-agent.d.mts +1 -1
  33. package/dist/lib/signing/agent-resolver/resolve-agent.d.ts +1 -1
  34. package/dist/lib/signing/agent-resolver/resolve-agent.js +47 -5
  35. package/dist/lib/signing/agent-resolver/resolve-agent.mjs +55 -6
  36. package/dist/lib/signing/brand-jwks.d.mts +2 -0
  37. package/dist/lib/signing/brand-jwks.d.ts +2 -0
  38. package/dist/lib/signing/brand-jwks.js +10 -2
  39. package/dist/lib/signing/brand-jwks.mjs +10 -2
  40. package/dist/lib/signing/errors.d.mts +4 -2
  41. package/dist/lib/signing/errors.d.ts +4 -2
  42. package/dist/lib/signing/errors.js +3 -1
  43. package/dist/lib/signing/errors.mjs +3 -1
  44. package/dist/lib/signing/verifier.js +1 -1
  45. package/dist/lib/signing/verifier.mjs +1 -1
  46. package/dist/lib/signing/webhook-verifier.js +2 -2
  47. package/dist/lib/signing/webhook-verifier.mjs +3 -3
  48. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  49. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  50. package/dist/lib/types/accept-proposal.d.ts +19 -1
  51. package/dist/lib/types/buy-products.d.ts +19 -1
  52. package/dist/lib/types/check-governance.d.ts +19 -1
  53. package/dist/lib/types/comply-test-controller.d.ts +19 -1
  54. package/dist/lib/types/control-media-buy.d.ts +19 -1
  55. package/dist/lib/types/core.generated.d.mts +14 -1
  56. package/dist/lib/types/core.generated.d.ts +14 -1
  57. package/dist/lib/types/create-media-buy.d.ts +14 -1
  58. package/dist/lib/types/get-media-buys.d.ts +14 -1
  59. package/dist/lib/types/get-products.d.ts +19 -1
  60. package/dist/lib/types/list-products.d.ts +19 -1
  61. package/dist/lib/types/refine-proposals.d.ts +19 -1
  62. package/dist/lib/types/request-proposals.d.ts +19 -1
  63. package/dist/lib/types/schemas.generated.d.ts +12 -3
  64. package/dist/lib/types/schemas.generated.js +4 -1
  65. package/dist/lib/types/schemas.generated.mjs +4 -1
  66. package/dist/lib/types/tools.generated.d.mts +14 -1
  67. package/dist/lib/types/tools.generated.d.ts +14 -1
  68. package/dist/lib/types/update-media-buy.d.ts +14 -1
  69. package/dist/lib/version.d.mts +3 -3
  70. package/dist/lib/version.d.ts +3 -3
  71. package/dist/lib/version.js +3 -3
  72. package/dist/lib/version.mjs +3 -3
  73. package/dist/lib/webhooks/index.d.mts +24 -0
  74. package/dist/lib/webhooks/index.d.ts +24 -0
  75. package/dist/lib/webhooks/index.js +50 -24
  76. package/dist/lib/webhooks/index.mjs +49 -24
  77. package/dist/lib/wholesale-feed-sync/index.d.mts +2 -0
  78. package/dist/lib/wholesale-feed-sync/index.d.ts +2 -0
  79. package/dist/lib/wholesale-feed-sync/index.js +7 -0
  80. package/dist/lib/wholesale-feed-sync/index.mjs +4 -0
  81. package/dist/lib/wholesale-feed-sync/mirror.d.mts +97 -0
  82. package/dist/lib/wholesale-feed-sync/mirror.d.ts +97 -0
  83. package/dist/lib/wholesale-feed-sync/mirror.js +350 -0
  84. package/dist/lib/wholesale-feed-sync/mirror.mjs +322 -0
  85. package/dist/lib/wholesale-feed-sync/sync.d.mts +12 -29
  86. package/dist/lib/wholesale-feed-sync/sync.d.ts +12 -29
  87. package/dist/lib/wholesale-feed-sync/sync.js +153 -297
  88. package/dist/lib/wholesale-feed-sync/sync.mjs +158 -297
  89. package/docs/README.md +6 -0
  90. package/docs/TYPE-SUMMARY.md +2 -2
  91. package/docs/guides/BUYER-STORAGE.md +3 -0
  92. package/docs/guides/FIRST-CALL-TO-A-SELLER.md +3 -1
  93. package/docs/guides/account-resolution.md +46 -10
  94. package/docs/llms.txt +2 -2
  95. package/docs/migration-14.0-to-14.1.md +85 -0
  96. package/docs/migration-14.x-rc-worksheet.md +4 -4
  97. package/docs/migration-agent-resolution-3.3.md +5 -3
  98. package/docs/recipes/verifying-inbound-webhooks.md +4 -0
  99. package/package.json +2 -1
  100. package/skills/adcp-brand.previous/SKILL.md +0 -200
  101. package/skills/adcp-creative.previous/SKILL.md +0 -305
  102. package/skills/adcp-governance.previous/SKILL.md +0 -566
  103. package/skills/adcp-measurement.previous/SKILL.md +0 -136
  104. package/skills/adcp-media-buy.previous/SKILL.md +0 -556
  105. package/skills/adcp-si.previous/SKILL.md +0 -206
  106. package/skills/adcp-signals.previous/SKILL.md +0 -204
@@ -23,13 +23,17 @@ __export(sync_exports, {
23
23
  module.exports = __toCommonJS(sync_exports);
24
24
  var import_node_events = require("node:events");
25
25
  var import_node_util = require("node:util");
26
- var import_node_crypto = require("node:crypto");
26
+ var import_mirror = require('./mirror.js');
27
27
  var import_webhook_notification = require('./webhook-notification.js');
28
28
  const bootstrapReadFailures = /* @__PURE__ */ new WeakSet();
29
+ function supersededRepairError() {
30
+ const error = new Error("WholesaleFeedSync: repair superseded repeatedly; retry webhook delivery.");
31
+ bootstrapReadFailures.add(error);
32
+ return error;
33
+ }
29
34
  const DEFAULT_PROBE_INTERVAL_MS = 6e5;
30
35
  const DEFAULT_CAPABILITY_REFRESH_INTERVAL_MS = 864e5;
31
36
  const DEFAULT_PERSISTENCE_TIMEOUT_MS = 3e4;
32
- const DEFAULT_BOOTSTRAP_PAGE_LIMIT = 100;
33
37
  const VERSION_MISMATCH_RECOVERY_ATTEMPTS = 3;
34
38
  const VERSION_MISMATCH_RECOVERY_BACKOFF_MS = 5;
35
39
  class WholesaleFeedSync extends import_node_events.EventEmitter {
@@ -70,11 +74,10 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
70
74
  probeTimer = null;
71
75
  capabilityTimer = null;
72
76
  lifecycleEpoch = 0;
77
+ mirrorRevision = 0;
73
78
  /**
74
- * Read-only view of the in-memory product index. The `mode` reflects the
75
- * sync strategy for product events specifically — in mixed-capability
76
- * agents (products-feed + signals-wholesale-only, or vice versa) this
77
- * may differ from `signals.mode`.
79
+ * Product index view. `mode` identifies the product sync strategy and may
80
+ * differ from `signals.mode` for agents with mixed feed capabilities.
78
81
  */
79
82
  products = {
80
83
  list: () => [...this.productIndex.values()],
@@ -109,19 +112,12 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
109
112
  },
110
113
  _mode: "manual",
111
114
  /**
112
- * `true` when the agent supports `discovery_mode: 'wholesale'` on
113
- * `get_signals` (i.e., signals are browsable). When `false`, the
114
- * agent only supports brief-mode discovery — `signals.list()` will
115
- * be empty until adopters call into the agent with their own briefs.
115
+ * True when `get_signals` supports `discovery_mode: 'wholesale'`.
116
+ * Otherwise `list()` is empty; use client brief discovery instead.
116
117
  */
117
118
  queryable: true
118
119
  };
119
- /**
120
- * One-shot console warning when adopters call `signals.list()` or
121
- * `signals.search()` against an agent that doesn't support wholesale
122
- * signal enumeration. Without this, empty results read as "no signals
123
- * match" rather than "the agent doesn't browse, only briefs."
124
- */
120
+ // Warn once when empty signal results reflect missing wholesale support.
125
121
  warnIfSignalsNotQueryable() {
126
122
  if (this.signals.queryable || this.signalsQueryableWarned) return;
127
123
  if (this._state === "idle" || this._state === "bootstrapping") return;
@@ -147,14 +143,9 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
147
143
  }
148
144
  // ====== Lifecycle ======
149
145
  /**
150
- * Probe the agent's capabilities, pick a sync mode, and bootstrap the
151
- * in-memory replica via wholesale enumeration. In `'auto-poll'` mode
152
- * starts the conditional wholesale-feed version probe loop.
153
- *
154
- * Safe to call repeatedly — concurrent calls await the in-flight
155
- * bootstrap and return when it completes (no duplicate bootstrap, no
156
- * silent drop). Sequential calls re-probe capabilities and re-bootstrap,
157
- * equivalent to calling `refresh()` after a mode upgrade.
146
+ * Discover capabilities, bootstrap the mirror, and schedule auto-poll probes
147
+ * for versioned sellers. Concurrent calls share the bootstrap; sequential
148
+ * calls re-probe capabilities and refresh the mirror.
158
149
  */
159
150
  async start() {
160
151
  if (this.startPromise && this.startPromiseEpoch === this.lifecycleEpoch) return this.startPromise;
@@ -174,7 +165,7 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
174
165
  if (!await this.restorePersistedState(epoch)) return;
175
166
  if (!await this.resolveMode(epoch)) return;
176
167
  try {
177
- await this.bootstrap({ epoch });
168
+ await this.bootstrapWithRetries({ epoch });
178
169
  } finally {
179
170
  if (this.isLifecycleCurrent(epoch)) {
180
171
  if (this._mode === "auto-poll") this.scheduleProbe(epoch);
@@ -198,6 +189,7 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
198
189
  */
199
190
  async reset() {
200
191
  this.stop();
192
+ ++this.mirrorRevision;
201
193
  this.productIndex.clear();
202
194
  this.signalIndex.clear();
203
195
  this.productWholesaleFeedVersion = void 0;
@@ -223,7 +215,7 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
223
215
  async refresh() {
224
216
  const epoch = this.lifecycleEpoch;
225
217
  this.emit("resyncing", { reason: "manual" });
226
- await this.bootstrap({ emitDiffs: true, epoch, propagateFailure: true });
218
+ await this.bootstrapWithRetries({ emitDiffs: true, epoch, propagateFailure: true });
227
219
  }
228
220
  /**
229
221
  * Apply one legacy-view account-level wholesale feed webhook to the local mirror.
@@ -272,6 +264,11 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
272
264
  this.rememberLastWebhookEventId(event.event_id);
273
265
  throw err;
274
266
  }
267
+ if (!this._lastSyncedAt && event.event_type !== "wholesale_feed.bulk_change") {
268
+ if (!await this.recoverFromVersionMismatch(event, epoch)) return;
269
+ await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
270
+ return;
271
+ }
275
272
  if (webhook.previous_wholesale_feed_version && currentVersion && webhook.previous_wholesale_feed_version !== currentVersion) {
276
273
  if (!await this.recoverFromVersionMismatch(event, epoch)) return;
277
274
  await this.markWebhookProcessed(dedupeKey, eventDedupeKey);
@@ -352,12 +349,24 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
352
349
  return true;
353
350
  }
354
351
  // ====== Private: bootstrap (wholesale enumeration) ======
352
+ async bootstrapWithRetries(options) {
353
+ for (let attempt = 0; attempt < VERSION_MISMATCH_RECOVERY_ATTEMPTS; attempt++) {
354
+ const revision = this.mirrorRevision;
355
+ if (await this.bootstrap({ ...options, conditional: attempt === 0 ? void 0 : false })) return;
356
+ if (!this.isLifecycleCurrent(options.epoch) || revision === this.mirrorRevision) return;
357
+ }
358
+ const error = new Error("WholesaleFeedSync: refresh superseded repeatedly; retry start or refresh.");
359
+ this.handleBootstrapFailure({ error });
360
+ throw error;
361
+ }
355
362
  async bootstrap(options = {}) {
356
363
  const epoch = options.epoch ?? this.lifecycleEpoch;
357
364
  if (!this.isLifecycleCurrent(epoch)) return false;
358
365
  this.setState("bootstrapping");
359
366
  const previousLastSyncedAt = this._lastSyncedAt;
367
+ const expectedRevision = this.mirrorRevision;
360
368
  let reportedFailure;
369
+ let committed = false;
361
370
  try {
362
371
  const previousProducts = new Map(this.productIndex);
363
372
  const previousSignals = new Map(this.signalIndex);
@@ -369,8 +378,12 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
369
378
  const refreshProducts = entities !== "signals";
370
379
  const refreshSignals = entities !== "products" && this.signals.queryable;
371
380
  if (refreshProducts) {
372
- productResult = await this.bootstrapProducts(epoch);
381
+ productResult = await this.bootstrapProducts(epoch, options.conditional);
373
382
  if (productResult.cancelled) return false;
383
+ if (this.mirrorRevision !== expectedRevision) {
384
+ this.setState(this._lastSyncedAt ? "syncing" : "idle");
385
+ return false;
386
+ }
374
387
  if (productResult.failure) {
375
388
  const failure = productResult.failure;
376
389
  reportedFailure = failure.error;
@@ -384,8 +397,12 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
384
397
  }
385
398
  }
386
399
  if (refreshSignals) {
387
- signalResult = await this.bootstrapSignals(epoch);
400
+ signalResult = await this.bootstrapSignals(epoch, options.conditional);
388
401
  if (signalResult.cancelled) return false;
402
+ if (this.mirrorRevision !== expectedRevision) {
403
+ this.setState(this._lastSyncedAt ? "syncing" : "idle");
404
+ return false;
405
+ }
389
406
  if (signalResult.failure) {
390
407
  const failure = signalResult.failure;
391
408
  reportedFailure = failure.error;
@@ -399,6 +416,12 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
399
416
  }
400
417
  }
401
418
  if (!this.isLifecycleCurrent(epoch)) return false;
419
+ if (this.mirrorRevision !== expectedRevision) {
420
+ this.setState(this._lastSyncedAt ? "syncing" : "idle");
421
+ return false;
422
+ }
423
+ ++this.mirrorRevision;
424
+ committed = true;
402
425
  if (refreshProducts && productResult) {
403
426
  this.commitProductMetadata(productResult.metadata);
404
427
  if (!productResult.unchanged) {
@@ -430,6 +453,7 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
430
453
  return true;
431
454
  } catch (err) {
432
455
  if (!this.isLifecycleCurrent(epoch)) return false;
456
+ if (!committed && this.mirrorRevision !== expectedRevision && err !== reportedFailure) return false;
433
457
  if (err === reportedFailure) throw err;
434
458
  this._lastSyncedAt = previousLastSyncedAt;
435
459
  this.setState(this._lastSyncedAt ? "degraded" : "error");
@@ -446,102 +470,74 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
446
470
  if (this.listenerCount("error") > 0) this.emit("error", failure);
447
471
  return false;
448
472
  }
449
- /** Returns `true` when the seller short-circuited with `unchanged: true`. */
450
- async bootstrapProducts(epoch) {
451
- let cursor;
452
- const into = /* @__PURE__ */ new Map();
453
- let metadata = this.currentProductMetadata();
454
- do {
455
- const params = {
456
- buying_mode: "wholesale",
457
- pagination: { max_results: DEFAULT_BOOTSTRAP_PAGE_LIMIT, ...cursor && { cursor } },
458
- ...this.account && { account: this.account }
459
- };
460
- if (!cursor && metadata.wholesaleFeedVersion && this._capabilities.wholesaleFeedVersioning) {
461
- params.if_wholesale_feed_version = metadata.wholesaleFeedVersion;
462
- if (metadata.pricingVersion) params.if_pricing_version = metadata.pricingVersion;
463
- }
464
- const result = await this.client.getProducts(params);
465
- if (!this.isLifecycleCurrent(epoch)) return { cancelled: true, unchanged: false, items: into, metadata };
466
- if (result.success === false || result.status !== void 0 && result.status !== "completed" && result.status !== "success") {
467
- return {
468
- cancelled: false,
469
- unchanged: false,
470
- items: into,
471
- metadata,
472
- failure: { error: new Error(result.error ?? "Product bootstrap failed"), adcpError: result.adcpError }
473
- };
474
- }
475
- const body = result.data;
476
- if (!body)
477
- return {
478
- cancelled: false,
479
- unchanged: false,
480
- items: into,
481
- metadata,
482
- failure: { error: new Error("Product bootstrap did not return a completed catalog") }
483
- };
484
- if (body.unchanged) {
485
- metadata = mergeFeedMetadata(metadata, body);
486
- return { cancelled: false, unchanged: true, items: into, metadata };
487
- }
488
- const products = Array.isArray(body.products) ? body.products : [];
489
- for (const product of products) {
490
- const id = product.product_id;
491
- if (typeof id === "string") into.set(id, product);
492
- }
493
- metadata = mergeFeedMetadata(metadata, body);
494
- cursor = body.pagination?.has_more ? body.pagination?.cursor : void 0;
495
- } while (cursor);
496
- return { cancelled: false, unchanged: false, items: into, metadata };
473
+ bootstrapProducts(epoch, conditional) {
474
+ return this.bootstrapFeed("product", epoch, conditional);
497
475
  }
498
- async bootstrapSignals(epoch) {
499
- let cursor;
500
- const into = /* @__PURE__ */ new Map();
501
- let metadata = this.currentSignalMetadata();
502
- do {
503
- const params = {
504
- discovery_mode: "wholesale",
505
- pagination: { max_results: DEFAULT_BOOTSTRAP_PAGE_LIMIT, ...cursor && { cursor } },
506
- ...this.account && { account: this.account }
507
- };
508
- if (!cursor && metadata.wholesaleFeedVersion && this._capabilities.wholesaleFeedVersioning) {
509
- params.if_wholesale_feed_version = metadata.wholesaleFeedVersion;
510
- if (metadata.pricingVersion) params.if_pricing_version = metadata.pricingVersion;
511
- }
512
- const result = await this.client.getSignals(params);
513
- if (!this.isLifecycleCurrent(epoch)) return { cancelled: true, unchanged: false, items: into, metadata };
514
- if (result.success === false || result.status !== void 0 && result.status !== "completed" && result.status !== "success") {
515
- return {
516
- cancelled: false,
517
- unchanged: false,
518
- items: into,
519
- metadata,
520
- failure: { error: new Error(result.error ?? "Signal bootstrap failed"), adcpError: result.adcpError }
521
- };
522
- }
523
- const body = result.data;
524
- if (!body)
525
- return {
526
- cancelled: false,
527
- unchanged: false,
528
- items: into,
529
- metadata,
530
- failure: { error: new Error("Signal bootstrap did not return a completed catalog") }
531
- };
532
- if (body.unchanged) {
533
- metadata = mergeFeedMetadata(metadata, body);
534
- return { cancelled: false, unchanged: true, items: into, metadata };
476
+ bootstrapSignals(epoch, conditional) {
477
+ return this.bootstrapFeed("signal", epoch, conditional);
478
+ }
479
+ async bootstrapFeed(entity, epoch, conditional) {
480
+ const store = new import_mirror.InMemoryWholesaleFeedMirrorStore();
481
+ const scope = { agentUrl: "shell", accountKey: "shell", entity };
482
+ const metadata = entity === "product" ? this.currentProductMetadata() : this.currentSignalMetadata();
483
+ const previous = entity === "product" ? [...this.productIndex.values()] : [...this.signalIndex.values()];
484
+ store.restore(scope, { revision: 0, items: previous, ...metadata });
485
+ let readException;
486
+ const read = async (call) => {
487
+ try {
488
+ return await call();
489
+ } catch (cause) {
490
+ readException = cause instanceof Error ? cause : new Error(String(cause));
491
+ throw cause;
535
492
  }
536
- const signals = Array.isArray(body.signals) ? body.signals : [];
537
- for (const signal of signals) {
538
- const id = signal.signal_agent_segment_id;
539
- if (typeof id === "string") into.set(id, signal);
493
+ };
494
+ const outcome = await (0, import_mirror.refreshWholesaleFeed)({
495
+ client: {
496
+ getAdcpCapabilities: this.client.getAdcpCapabilities.bind(this.client),
497
+ getProducts: (params) => read(() => this.client.getProducts(params)),
498
+ getSignals: (params) => read(() => this.client.getSignals(params))
499
+ },
500
+ store,
501
+ scope,
502
+ account: this.account,
503
+ conditional: conditional ?? this._capabilities.wholesaleFeedVersioning,
504
+ isCurrent: () => this.isLifecycleCurrent(epoch)
505
+ });
506
+ if (outcome.outcome === "superseded") return { cancelled: true, unchanged: false, items: /* @__PURE__ */ new Map(), metadata };
507
+ if (outcome.outcome === "degraded") {
508
+ if (readException) throw readException;
509
+ return {
510
+ cancelled: false,
511
+ unchanged: false,
512
+ items: /* @__PURE__ */ new Map(),
513
+ metadata,
514
+ failure: {
515
+ error: new Error(
516
+ `${entity === "product" ? "Product" : "Signal"} bootstrap failed: ${outcome.cause.message}`,
517
+ {
518
+ cause: outcome.cause
519
+ }
520
+ ),
521
+ adcpError: outcome.error
522
+ }
523
+ };
524
+ }
525
+ const snapshot = outcome.snapshot;
526
+ return {
527
+ cancelled: false,
528
+ unchanged: outcome.outcome === "unchanged",
529
+ items: new Map(
530
+ snapshot.items.map((item) => [
531
+ entity === "product" ? item.product_id : item.signal_agent_segment_id,
532
+ item
533
+ ])
534
+ ),
535
+ metadata: {
536
+ wholesaleFeedVersion: snapshot.wholesaleFeedVersion,
537
+ pricingVersion: snapshot.pricingVersion,
538
+ cacheScope: snapshot.cacheScope
540
539
  }
541
- metadata = mergeFeedMetadata(metadata, body);
542
- cursor = body.pagination?.has_more ? body.pagination?.cursor : void 0;
543
- } while (cursor);
544
- return { cancelled: false, unchanged: false, items: into, metadata };
540
+ };
545
541
  }
546
542
  async recoverFromBulkChange(event, epoch = this.lifecycleEpoch) {
547
543
  this.emit("resyncing", { reason: "bulk_change" });
@@ -552,14 +548,22 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
552
548
  );
553
549
  }
554
550
  const entities = affected === "product" ? "products" : "signals";
555
- return this.bootstrap({ emitDiffs: true, entities, epoch, propagateFailure: true });
551
+ for (let attempt = 0; attempt < VERSION_MISMATCH_RECOVERY_ATTEMPTS; attempt++) {
552
+ if (await this.bootstrap({ emitDiffs: true, entities, epoch, propagateFailure: true })) return true;
553
+ if (!this.isLifecycleCurrent(epoch)) return false;
554
+ }
555
+ throw supersededRepairError();
556
556
  }
557
557
  async recoverFromVersionMismatch(event, epoch = this.lifecycleEpoch) {
558
558
  this.emit("resyncing", { reason: "version_mismatch" });
559
559
  const beforeVersion = this.currentWholesaleFeedVersionForEvent(event);
560
560
  for (let attempt = 1; attempt <= VERSION_MISMATCH_RECOVERY_ATTEMPTS; attempt++) {
561
561
  const recovered = await this.bootstrap({ emitDiffs: true, epoch, propagateFailure: true });
562
- if (!recovered) return false;
562
+ if (!recovered) {
563
+ if (!this.isLifecycleCurrent(epoch)) return false;
564
+ if (attempt === VERSION_MISMATCH_RECOVERY_ATTEMPTS) throw supersededRepairError();
565
+ continue;
566
+ }
563
567
  const afterVersion = this.currentWholesaleFeedVersionForEvent(event);
564
568
  if (afterVersion !== beforeVersion) return true;
565
569
  if (attempt < VERSION_MISMATCH_RECOVERY_ATTEMPTS) {
@@ -713,63 +717,16 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
713
717
  }
714
718
  }
715
719
  applyEvent(event) {
716
- switch (event.event_type) {
717
- case "product.created":
718
- case "product.updated": {
719
- const payload = event.payload;
720
- if (typeof payload.product_id !== "string") return;
721
- if (payload.product) {
722
- this.productIndex.set(payload.product_id, payload.product);
723
- return;
724
- }
725
- return;
726
- }
727
- case "product.priced": {
728
- const payload = event.payload;
729
- const existing = this.productIndex.get(payload.product_id);
730
- if (existing && Array.isArray(payload.pricing_options)) {
731
- this.productIndex.set(payload.product_id, {
732
- ...existing,
733
- pricing_options: payload.pricing_options
734
- });
735
- }
736
- return;
737
- }
738
- case "product.removed": {
739
- const payload = event.payload;
740
- if (typeof payload.product_id === "string") this.productIndex.delete(payload.product_id);
741
- return;
742
- }
743
- case "signal.created":
744
- case "signal.updated": {
745
- const payload = event.payload;
746
- if (typeof payload.signal_agent_segment_id !== "string") return;
747
- if (payload.signal) {
748
- this.signalIndex.set(payload.signal_agent_segment_id, payload.signal);
749
- }
750
- return;
751
- }
752
- case "signal.priced": {
753
- const payload = event.payload;
754
- const existing = this.signalIndex.get(payload.signal_agent_segment_id);
755
- if (existing && Array.isArray(payload.pricing_options)) {
756
- this.signalIndex.set(payload.signal_agent_segment_id, {
757
- ...existing,
758
- pricing_options: payload.pricing_options
759
- });
760
- }
761
- return;
762
- }
763
- case "signal.removed": {
764
- const payload = event.payload;
765
- if (typeof payload.signal_agent_segment_id === "string") {
766
- this.signalIndex.delete(payload.signal_agent_segment_id);
767
- }
768
- return;
769
- }
770
- case "wholesale_feed.bulk_change":
771
- return;
772
- }
720
+ ++this.mirrorRevision;
721
+ const entity = event.event_type.startsWith("product.") ? "product" : "signal";
722
+ const items = (0, import_mirror.applyWholesaleFeedEvent)(
723
+ entity,
724
+ entity === "product" ? [...this.productIndex.values()] : [...this.signalIndex.values()],
725
+ event
726
+ );
727
+ if (!items) return;
728
+ if (entity === "product") this.productIndex = new Map(items.map((item) => [item.product_id, item]));
729
+ else this.signalIndex = new Map(items.map((item) => [item.signal_agent_segment_id, item]));
773
730
  }
774
731
  emitTypedEvent(event) {
775
732
  switch (event.event_type) {
@@ -803,107 +760,23 @@ class WholesaleFeedSync extends import_node_events.EventEmitter {
803
760
  }
804
761
  // ====== Private: diff emission (auto-poll / manual refresh) ======
805
762
  emitDiffs(previousProducts, previousSignals) {
806
- const now = (/* @__PURE__ */ new Date()).toISOString();
807
- const makeEvent = (event_type, entity_type, entity_id, payload) => ({
808
- // crypto.randomUUID() emits a v4 UUID, NOT v7. Synthetic events
809
- // are flagged via `synthetic: true` on the emit envelope so
810
- // adopters writing event_ids to a dedupe table know not to
811
- // treat the ID as seller-authored. Real webhook events carry the
812
- // agent's authoritative event_id.
813
- event_id: (0, import_node_crypto.randomUUID)(),
814
- event_type,
815
- entity_type,
816
- entity_id,
817
- created_at: now,
818
- payload
819
- });
820
- const emit = (channel, event) => {
763
+ const events = [
764
+ ...(0, import_mirror.diffWholesaleFeed)(
765
+ "product",
766
+ [...previousProducts.values()],
767
+ [...this.productIndex.values()],
768
+ this.productCacheScope
769
+ ),
770
+ ...(0, import_mirror.diffWholesaleFeed)(
771
+ "signal",
772
+ [...previousSignals.values()],
773
+ [...this.signalIndex.values()],
774
+ this.signalCacheScope
775
+ )
776
+ ];
777
+ for (const event of events) {
821
778
  this.emit("event", { event, synthetic: true });
822
- this.emit(channel, { event, synthetic: true });
823
- };
824
- for (const [id, product] of this.productIndex) {
825
- const prev = previousProducts.get(id);
826
- if (!prev) {
827
- emit(
828
- "product.created",
829
- makeEvent("product.created", "product", id, {
830
- product_id: id,
831
- product,
832
- applies_to: { scope: this.productCacheScope }
833
- })
834
- );
835
- } else if (priceChanged(prev, product)) {
836
- emit(
837
- "product.priced",
838
- makeEvent("product.priced", "product", id, {
839
- product_id: id,
840
- pricing_options: product.pricing_options ?? [],
841
- applies_to: { scope: this.productCacheScope }
842
- })
843
- );
844
- } else if (!(0, import_node_util.isDeepStrictEqual)(prev, product)) {
845
- emit(
846
- "product.updated",
847
- makeEvent("product.updated", "product", id, {
848
- product_id: id,
849
- product,
850
- applies_to: { scope: this.productCacheScope }
851
- })
852
- );
853
- }
854
- }
855
- for (const [id] of previousProducts) {
856
- if (!this.productIndex.has(id)) {
857
- emit(
858
- "product.removed",
859
- makeEvent("product.removed", "product", id, {
860
- product_id: id,
861
- applies_to: { scope: this.productCacheScope }
862
- })
863
- );
864
- }
865
- }
866
- for (const [id, signal] of this.signalIndex) {
867
- const prev = previousSignals.get(id);
868
- if (!prev) {
869
- emit(
870
- "signal.created",
871
- makeEvent("signal.created", "signal", id, {
872
- signal_agent_segment_id: id,
873
- signal,
874
- applies_to: { scope: this.signalCacheScope }
875
- })
876
- );
877
- } else if (signalPriceChanged(prev, signal)) {
878
- emit(
879
- "signal.priced",
880
- makeEvent("signal.priced", "signal", id, {
881
- signal_agent_segment_id: id,
882
- pricing_options: signal.pricing_options ?? [],
883
- applies_to: { scope: this.signalCacheScope }
884
- })
885
- );
886
- } else if (!(0, import_node_util.isDeepStrictEqual)(prev, signal)) {
887
- emit(
888
- "signal.updated",
889
- makeEvent("signal.updated", "signal", id, {
890
- signal_agent_segment_id: id,
891
- signal,
892
- applies_to: { scope: this.signalCacheScope }
893
- })
894
- );
895
- }
896
- }
897
- for (const [id] of previousSignals) {
898
- if (!this.signalIndex.has(id)) {
899
- emit(
900
- "signal.removed",
901
- makeEvent("signal.removed", "signal", id, {
902
- signal_agent_segment_id: id,
903
- applies_to: { scope: this.signalCacheScope }
904
- })
905
- );
906
- }
779
+ this.emit(event.event_type, { event, synthetic: true });
907
780
  }
908
781
  }
909
782
  // ====== Private: search ======
@@ -1080,16 +953,6 @@ function assertOptionalDate(value, field, invalid) {
1080
953
  function parsePersistedDate(value) {
1081
954
  return value === void 0 ? void 0 : new Date(value);
1082
955
  }
1083
- function priceChanged(prev, next) {
1084
- const a = prev.pricing_options;
1085
- const b = next.pricing_options;
1086
- return !(0, import_node_util.isDeepStrictEqual)(a, b);
1087
- }
1088
- function signalPriceChanged(prev, next) {
1089
- const a = prev.pricing_options;
1090
- const b = next.pricing_options;
1091
- return !(0, import_node_util.isDeepStrictEqual)(a, b);
1092
- }
1093
956
  function isUuidV7(value) {
1094
957
  return /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
1095
958
  }
@@ -1099,13 +962,6 @@ function compareUuidV7(a, b) {
1099
962
  const normalizedB = b.toLowerCase();
1100
963
  return normalizedA === normalizedB ? 0 : normalizedA > normalizedB ? 1 : -1;
1101
964
  }
1102
- function mergeFeedMetadata(current, body) {
1103
- return {
1104
- wholesaleFeedVersion: typeof body.wholesale_feed_version === "string" ? body.wholesale_feed_version : current.wholesaleFeedVersion,
1105
- pricingVersion: typeof body.pricing_version === "string" ? body.pricing_version : current.pricingVersion,
1106
- cacheScope: body.cache_scope === "public" || body.cache_scope === "account" ? body.cache_scope : current.cacheScope
1107
- };
1108
- }
1109
965
  async function sleep(ms) {
1110
966
  await new Promise((resolve) => setTimeout(resolve, ms));
1111
967
  }