@adcp/sdk 14.1.0 → 14.3.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.
- package/dist/lib/core/SingleAgentClient.d.mts +4 -1
- package/dist/lib/core/SingleAgentClient.d.ts +4 -1
- package/dist/lib/core/SingleAgentClient.js +25 -4
- package/dist/lib/core/SingleAgentClient.mjs +25 -4
- package/dist/lib/core/TaskExecutor.d.mts +3 -1
- package/dist/lib/core/TaskExecutor.d.ts +3 -1
- package/dist/lib/core/TaskExecutor.js +15 -11
- package/dist/lib/core/TaskExecutor.mjs +15 -11
- package/dist/lib/core/buyer-account-registry.d.mts +30 -2
- package/dist/lib/core/buyer-account-registry.d.ts +30 -2
- package/dist/lib/core/buyer-account-registry.js +126 -42
- package/dist/lib/core/buyer-account-registry.mjs +127 -43
- package/dist/lib/index.d.mts +3 -2
- package/dist/lib/index.d.ts +3 -2
- package/dist/lib/index.js +9 -0
- package/dist/lib/index.mjs +12 -1
- package/dist/lib/net/agent-transport-fetch.d.mts +4 -0
- package/dist/lib/net/agent-transport-fetch.d.ts +4 -0
- package/dist/lib/net/agent-transport-fetch.js +18 -5
- package/dist/lib/net/agent-transport-fetch.mjs +17 -5
- package/dist/lib/reporting/inspection.d.mts +3 -3
- package/dist/lib/reporting/inspection.d.ts +3 -3
- package/dist/lib/reporting/inspection.js +229 -84
- package/dist/lib/reporting/inspection.mjs +229 -84
- package/dist/lib/reporting/ledger/status-ingest.js +3 -19
- package/dist/lib/reporting/ledger/status-ingest.mjs +2 -18
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/create-adcp-server.js +5 -2
- package/dist/lib/server/create-adcp-server.mjs +5 -2
- package/dist/lib/signing/agent-resolver/errors.d.mts +2 -0
- package/dist/lib/signing/agent-resolver/errors.d.ts +2 -0
- package/dist/lib/signing/agent-resolver/errors.js +6 -0
- package/dist/lib/signing/agent-resolver/errors.mjs +6 -0
- package/dist/lib/signing/agent-resolver/fetch-helpers.d.mts +9 -0
- package/dist/lib/signing/agent-resolver/fetch-helpers.d.ts +9 -0
- package/dist/lib/signing/agent-resolver/fetch-helpers.js +35 -1
- package/dist/lib/signing/agent-resolver/fetch-helpers.mjs +38 -2
- package/dist/lib/signing/agent-resolver/legacy-brand.js +1 -1
- package/dist/lib/signing/agent-resolver/legacy-brand.mjs +2 -2
- package/dist/lib/signing/agent-resolver/resolve-agent.d.mts +1 -1
- package/dist/lib/signing/agent-resolver/resolve-agent.d.ts +1 -1
- package/dist/lib/signing/agent-resolver/resolve-agent.js +47 -5
- package/dist/lib/signing/agent-resolver/resolve-agent.mjs +55 -6
- package/dist/lib/signing/brand-jwks.d.mts +2 -0
- package/dist/lib/signing/brand-jwks.d.ts +2 -0
- package/dist/lib/signing/brand-jwks.js +10 -2
- package/dist/lib/signing/brand-jwks.mjs +10 -2
- package/dist/lib/signing/canonicalize.d.mts +2 -0
- package/dist/lib/signing/canonicalize.d.ts +2 -0
- package/dist/lib/signing/canonicalize.js +1 -1
- package/dist/lib/signing/canonicalize.mjs +1 -1
- package/dist/lib/signing/errors.d.mts +4 -2
- package/dist/lib/signing/errors.d.ts +4 -2
- package/dist/lib/signing/errors.js +3 -1
- package/dist/lib/signing/errors.mjs +3 -1
- package/dist/lib/signing/verifier.js +2 -2
- package/dist/lib/signing/verifier.mjs +2 -2
- package/dist/lib/signing/webhook-verifier.js +2 -2
- package/dist/lib/signing/webhook-verifier.mjs +3 -3
- package/dist/lib/testing/storyboard/request-signing/builder.js +13 -1
- package/dist/lib/testing/storyboard/request-signing/builder.mjs +13 -1
- package/dist/lib/testing/storyboard/request-signing/grader.js +1 -0
- package/dist/lib/testing/storyboard/request-signing/grader.mjs +1 -0
- package/dist/lib/testing/storyboard/request-signing/vector-loader.js +1 -1
- package/dist/lib/testing/storyboard/request-signing/vector-loader.mjs +1 -1
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/types/accept-proposal.d.ts +19 -1
- package/dist/lib/types/buy-products.d.ts +19 -1
- package/dist/lib/types/check-governance.d.ts +19 -1
- package/dist/lib/types/comply-test-controller.d.ts +19 -1
- package/dist/lib/types/control-media-buy.d.ts +19 -1
- package/dist/lib/types/core.generated.d.mts +14 -1
- package/dist/lib/types/core.generated.d.ts +14 -1
- package/dist/lib/types/create-media-buy.d.ts +14 -1
- package/dist/lib/types/get-media-buys.d.ts +14 -1
- package/dist/lib/types/get-products.d.ts +19 -1
- package/dist/lib/types/list-products.d.ts +19 -1
- package/dist/lib/types/refine-proposals.d.ts +19 -1
- package/dist/lib/types/request-proposals.d.ts +19 -1
- package/dist/lib/types/schemas.generated.d.ts +12 -3
- package/dist/lib/types/schemas.generated.js +4 -1
- package/dist/lib/types/schemas.generated.mjs +4 -1
- package/dist/lib/types/tools.generated.d.mts +14 -1
- package/dist/lib/types/tools.generated.d.ts +14 -1
- package/dist/lib/types/update-media-buy.d.ts +14 -1
- package/dist/lib/utils/pointer-utils.d.mts +2 -0
- package/dist/lib/utils/pointer-utils.d.ts +2 -0
- package/dist/lib/utils/pointer-utils.js +43 -0
- package/dist/lib/utils/pointer-utils.mjs +19 -0
- package/dist/lib/validation/diagnostic-limits.d.mts +4 -0
- package/dist/lib/validation/diagnostic-limits.d.ts +4 -0
- package/dist/lib/validation/diagnostic-limits.js +57 -0
- package/dist/lib/validation/diagnostic-limits.mjs +32 -0
- package/dist/lib/validation/schema-errors.d.mts +3 -1
- package/dist/lib/validation/schema-errors.d.ts +3 -1
- package/dist/lib/validation/schema-errors.js +18 -3
- package/dist/lib/validation/schema-errors.mjs +18 -3
- package/dist/lib/validation/schema-validator.js +56 -4
- package/dist/lib/validation/schema-validator.mjs +56 -4
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/dist/lib/webhooks/index.d.mts +24 -0
- package/dist/lib/webhooks/index.d.ts +24 -0
- package/dist/lib/webhooks/index.js +50 -24
- package/dist/lib/webhooks/index.mjs +49 -24
- package/dist/lib/wholesale-feed-sync/index.d.mts +2 -0
- package/dist/lib/wholesale-feed-sync/index.d.ts +2 -0
- package/dist/lib/wholesale-feed-sync/index.js +7 -0
- package/dist/lib/wholesale-feed-sync/index.mjs +4 -0
- package/dist/lib/wholesale-feed-sync/mirror.d.mts +97 -0
- package/dist/lib/wholesale-feed-sync/mirror.d.ts +97 -0
- package/dist/lib/wholesale-feed-sync/mirror.js +350 -0
- package/dist/lib/wholesale-feed-sync/mirror.mjs +322 -0
- package/dist/lib/wholesale-feed-sync/sync.d.mts +12 -29
- package/dist/lib/wholesale-feed-sync/sync.d.ts +12 -29
- package/dist/lib/wholesale-feed-sync/sync.js +153 -297
- package/dist/lib/wholesale-feed-sync/sync.mjs +158 -297
- package/docs/README.md +6 -0
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/guides/BUYER-STORAGE.md +3 -0
- package/docs/guides/FIRST-CALL-TO-A-SELLER.md +3 -1
- package/docs/guides/account-resolution.md +46 -10
- package/docs/llms.txt +2 -2
- package/docs/migration-14.0-to-14.1.md +85 -0
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/docs/migration-agent-resolution-3.3.md +5 -3
- package/docs/recipes/verifying-inbound-webhooks.md +4 -0
- package/package.json +3 -2
- package/skills/call-adcp-agent/SKILL.md +4 -1
- package/skills/adcp-brand.previous/SKILL.md +0 -200
- package/skills/adcp-creative.previous/SKILL.md +0 -305
- package/skills/adcp-governance.previous/SKILL.md +0 -566
- package/skills/adcp-measurement.previous/SKILL.md +0 -136
- package/skills/adcp-media-buy.previous/SKILL.md +0 -556
- package/skills/adcp-si.previous/SKILL.md +0 -206
- package/skills/adcp-signals.previous/SKILL.md +0 -204
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { parseWholesaleFeedWebhookNotification } from "./webhook-notification.mjs";
|
|
4
|
+
class InMemoryWholesaleFeedMirrorStore {
|
|
5
|
+
/** Fail closed at capacity rather than evicting receipts inside the delivery retry window. */
|
|
6
|
+
constructor(maxWebhookReceipts = 1e4) {
|
|
7
|
+
this.maxWebhookReceipts = maxWebhookReceipts;
|
|
8
|
+
if (!Number.isSafeInteger(maxWebhookReceipts) || maxWebhookReceipts < 1)
|
|
9
|
+
throw new Error("maxWebhookReceipts must be a positive integer.");
|
|
10
|
+
}
|
|
11
|
+
maxWebhookReceipts;
|
|
12
|
+
records = /* @__PURE__ */ new Map();
|
|
13
|
+
receipts = /* @__PURE__ */ new Map();
|
|
14
|
+
receiptCount = 0;
|
|
15
|
+
async hasWebhookReceipt(scope, idempotencyKey) {
|
|
16
|
+
return this.receipts.get(this.key(scope))?.has(idempotencyKey) ?? false;
|
|
17
|
+
}
|
|
18
|
+
key(scope) {
|
|
19
|
+
return JSON.stringify([scope.agentUrl, scope.accountKey, scope.entity]);
|
|
20
|
+
}
|
|
21
|
+
async read(scope) {
|
|
22
|
+
return structuredClone(this.records.get(this.key(scope)) ?? { revision: 0, items: [], cacheScope: "public" });
|
|
23
|
+
}
|
|
24
|
+
/** Seed a detached record when restoring a shell snapshot. */
|
|
25
|
+
restore(scope, snapshot) {
|
|
26
|
+
this.records.set(this.key(scope), structuredClone(snapshot));
|
|
27
|
+
}
|
|
28
|
+
async commit(scope, expectedRevision, change) {
|
|
29
|
+
const key = this.key(scope);
|
|
30
|
+
const prior = this.records.get(key) ?? { revision: 0, items: [], cacheScope: "public" };
|
|
31
|
+
if (prior.revision !== expectedRevision) return false;
|
|
32
|
+
const items = new Map(prior.items.map((item) => [itemId(scope.entity, item), item]));
|
|
33
|
+
for (const id of change.deletes) items.delete(id);
|
|
34
|
+
for (const item of change.upserts) items.set(itemId(scope.entity, item), structuredClone(item));
|
|
35
|
+
const { upserts: _upserts, deletes: _deletes, webhookReceipt, ...metadata } = change;
|
|
36
|
+
const next = {
|
|
37
|
+
...prior,
|
|
38
|
+
...structuredClone(metadata),
|
|
39
|
+
revision: prior.revision + 1,
|
|
40
|
+
items: [...items.values()],
|
|
41
|
+
error: void 0
|
|
42
|
+
};
|
|
43
|
+
if (webhookReceipt) {
|
|
44
|
+
const receipts = this.receipts.get(key) ?? /* @__PURE__ */ new Set();
|
|
45
|
+
if (!receipts.has(webhookReceipt.idempotencyKey)) {
|
|
46
|
+
if (this.receiptCount >= this.maxWebhookReceipts)
|
|
47
|
+
throw new Error("Webhook receipt capacity reached; use durable storage or increase maxWebhookReceipts.");
|
|
48
|
+
receipts.add(webhookReceipt.idempotencyKey);
|
|
49
|
+
++this.receiptCount;
|
|
50
|
+
}
|
|
51
|
+
this.receipts.set(key, receipts);
|
|
52
|
+
}
|
|
53
|
+
this.records.set(key, next);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
async recordError(scope, expectedRevision, error) {
|
|
57
|
+
const key = this.key(scope);
|
|
58
|
+
const prior = this.records.get(key) ?? { revision: 0, items: [], cacheScope: "public" };
|
|
59
|
+
if (prior.revision !== expectedRevision) return false;
|
|
60
|
+
this.records.set(key, { ...prior, revision: prior.revision + 1, error: structuredClone(error) });
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function refreshWholesaleFeed(options) {
|
|
65
|
+
return refreshMirror(options);
|
|
66
|
+
}
|
|
67
|
+
async function refreshMirror(options, receipt, expectedWebhookVersion) {
|
|
68
|
+
options = { ...options, scope: structuredClone(options.scope), account: structuredClone(options.account) };
|
|
69
|
+
const { store, scope, client } = options;
|
|
70
|
+
const previous = await store.read(scope);
|
|
71
|
+
if (options.isCurrent && !options.isCurrent()) return { outcome: "superseded" };
|
|
72
|
+
let cursor;
|
|
73
|
+
const cursors = /* @__PURE__ */ new Set();
|
|
74
|
+
const items = /* @__PURE__ */ new Map();
|
|
75
|
+
let wholesaleFeedVersion;
|
|
76
|
+
let pricingVersion;
|
|
77
|
+
let cacheScope = previous.cacheScope;
|
|
78
|
+
let unchanged = false;
|
|
79
|
+
let page = 0;
|
|
80
|
+
try {
|
|
81
|
+
do {
|
|
82
|
+
if (++page > 1e4) throw new Error("Wholesale feed pagination exceeded its page limit.");
|
|
83
|
+
const params = {
|
|
84
|
+
...scope.entity === "product" ? { buying_mode: "wholesale" } : { discovery_mode: "wholesale" },
|
|
85
|
+
pagination: { max_results: 100, ...cursor && { cursor } },
|
|
86
|
+
...options.account && { account: options.account },
|
|
87
|
+
...!cursor && options.conditional !== false && previous.wholesaleFeedVersion && {
|
|
88
|
+
if_wholesale_feed_version: previous.wholesaleFeedVersion,
|
|
89
|
+
...previous.pricingVersion && { if_pricing_version: previous.pricingVersion }
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const result = await (scope.entity === "product" ? client.getProducts(params) : client.getSignals(params));
|
|
93
|
+
if (options.isCurrent && !options.isCurrent()) return { outcome: "superseded" };
|
|
94
|
+
if (result.success === false || result.status !== void 0 && !["completed", "success"].includes(result.status)) {
|
|
95
|
+
throw Object.assign(new Error(result.error ?? "Wholesale feed refresh failed."), {
|
|
96
|
+
adcpError: result.adcpError
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const body = result.data;
|
|
100
|
+
if (!body) throw new Error("Wholesale feed did not return a completed catalog.");
|
|
101
|
+
if (body.unchanged) {
|
|
102
|
+
if (cursor || !("if_wholesale_feed_version" in params) || body.wholesale_feed_version !== previous.wholesaleFeedVersion || "if_pricing_version" in params && body.pricing_version !== previous.pricingVersion || body.cache_scope !== void 0 && body.cache_scope !== previous.cacheScope) {
|
|
103
|
+
throw new Error("Wholesale feed unchanged response must match both requested version tokens.");
|
|
104
|
+
}
|
|
105
|
+
unchanged = true;
|
|
106
|
+
}
|
|
107
|
+
if (page > 1 && (body.wholesale_feed_version !== void 0 && body.wholesale_feed_version !== wholesaleFeedVersion || body.pricing_version !== void 0 && body.pricing_version !== pricingVersion || (body.cache_scope ?? cacheScope) !== cacheScope)) {
|
|
108
|
+
throw new Error("Wholesale feed versions changed during pagination.");
|
|
109
|
+
}
|
|
110
|
+
wholesaleFeedVersion = body.wholesale_feed_version ?? wholesaleFeedVersion;
|
|
111
|
+
pricingVersion = body.pricing_version ?? pricingVersion;
|
|
112
|
+
cacheScope = body.cache_scope ?? cacheScope;
|
|
113
|
+
if (unchanged) break;
|
|
114
|
+
const rows = scope.entity === "product" ? body.products : body.signals;
|
|
115
|
+
if (!Array.isArray(rows)) throw new Error("Wholesale feed response is missing its catalog rows.");
|
|
116
|
+
for (const item of rows) {
|
|
117
|
+
const id = itemId(scope.entity, item);
|
|
118
|
+
if (!id) throw new Error("Wholesale feed row is missing its entity id.");
|
|
119
|
+
items.set(id, item);
|
|
120
|
+
}
|
|
121
|
+
cursor = body.pagination?.has_more ? body.pagination.cursor : void 0;
|
|
122
|
+
if (body.pagination?.has_more && (!cursor || cursors.has(cursor)))
|
|
123
|
+
throw new Error("Wholesale feed pagination did not advance.");
|
|
124
|
+
if (cursor) cursors.add(cursor);
|
|
125
|
+
} while (cursor);
|
|
126
|
+
if (receipt && expectedWebhookVersion !== void 0 && wholesaleFeedVersion === previous.wholesaleFeedVersion && wholesaleFeedVersion !== expectedWebhookVersion)
|
|
127
|
+
throw new Error("Wholesale repair has not observed the webhook version; retry delivery.");
|
|
128
|
+
} catch (cause) {
|
|
129
|
+
if (options.isCurrent && !options.isCurrent()) return { outcome: "superseded" };
|
|
130
|
+
const errorCause = cause instanceof Error ? cause : new Error(String(cause));
|
|
131
|
+
const error = cause?.adcpError ?? {
|
|
132
|
+
code: "SERVICE_UNAVAILABLE",
|
|
133
|
+
recovery: "transient",
|
|
134
|
+
message: "Wholesale feed refresh failed.",
|
|
135
|
+
synthetic: true
|
|
136
|
+
};
|
|
137
|
+
if (!await store.recordError(scope, previous.revision, error)) return { outcome: "superseded" };
|
|
138
|
+
return { outcome: "degraded", error, cause: errorCause };
|
|
139
|
+
}
|
|
140
|
+
const nextItems = unchanged ? previous.items : [...items.values()];
|
|
141
|
+
const next = {
|
|
142
|
+
...previous,
|
|
143
|
+
items: nextItems,
|
|
144
|
+
wholesaleFeedVersion,
|
|
145
|
+
pricingVersion,
|
|
146
|
+
cacheScope,
|
|
147
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
148
|
+
revision: previous.revision + 1,
|
|
149
|
+
error: void 0,
|
|
150
|
+
...receipt && {
|
|
151
|
+
lastWebhookEventId: latestEventId(previous.lastWebhookEventId, receipt.eventId),
|
|
152
|
+
lastWebhookIdempotencyKey: receipt.idempotencyKey
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const change = rowDiff(scope.entity, previous.items, nextItems);
|
|
156
|
+
if (options.isCurrent && !options.isCurrent()) return { outcome: "superseded" };
|
|
157
|
+
if (!await store.commit(scope, previous.revision, {
|
|
158
|
+
...change,
|
|
159
|
+
wholesaleFeedVersion,
|
|
160
|
+
pricingVersion,
|
|
161
|
+
cacheScope,
|
|
162
|
+
lastSyncedAt: next.lastSyncedAt,
|
|
163
|
+
...receipt && {
|
|
164
|
+
webhookReceipt: receipt,
|
|
165
|
+
lastWebhookEventId: next.lastWebhookEventId,
|
|
166
|
+
lastWebhookIdempotencyKey: receipt.idempotencyKey
|
|
167
|
+
}
|
|
168
|
+
}))
|
|
169
|
+
return { outcome: "superseded" };
|
|
170
|
+
return unchanged ? { outcome: "unchanged", snapshot: next } : {
|
|
171
|
+
outcome: "applied",
|
|
172
|
+
snapshot: next,
|
|
173
|
+
diff: diffWholesaleFeed(scope.entity, previous.items, nextItems, cacheScope, true)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function applyWholesaleFeedWebhook(options) {
|
|
177
|
+
options = {
|
|
178
|
+
...options,
|
|
179
|
+
scope: structuredClone(options.scope),
|
|
180
|
+
account: structuredClone(options.account),
|
|
181
|
+
webhookScope: structuredClone(options.webhookScope),
|
|
182
|
+
webhook: structuredClone(options.webhook)
|
|
183
|
+
};
|
|
184
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
185
|
+
const outcome = await applyWebhookOnce(options);
|
|
186
|
+
if (outcome.outcome !== "superseded" || options.isCurrent && !options.isCurrent()) return outcome;
|
|
187
|
+
}
|
|
188
|
+
return { outcome: "superseded" };
|
|
189
|
+
}
|
|
190
|
+
async function applyWebhookOnce(options) {
|
|
191
|
+
const { webhook, scope, store } = options;
|
|
192
|
+
parseWholesaleFeedWebhookNotification(webhook);
|
|
193
|
+
const event = webhook.event;
|
|
194
|
+
if (!event || webhook.notification_type !== event.event_type || webhook.notification_id !== event.event_id)
|
|
195
|
+
throw new Error("Wholesale webhook event identity does not match its envelope.");
|
|
196
|
+
const entity = event.event_type === "wholesale_feed.bulk_change" ? event.payload.affected_entity_type : event.event_type.startsWith("product.") ? "product" : event.event_type.startsWith("signal.") ? "signal" : void 0;
|
|
197
|
+
if (entity !== scope.entity) throw new Error("Wholesale webhook entity does not match this mirror.");
|
|
198
|
+
const accountId = options.webhookScope?.accountId ?? (options.account && "account_id" in options.account ? options.account.account_id : void 0);
|
|
199
|
+
if (!accountId) throw new Error("Wholesale webhook application requires an expected account_id.");
|
|
200
|
+
if (webhook.account_id !== accountId || options.webhookScope?.subscriberId && options.webhookScope.subscriberId !== webhook.subscriber_id)
|
|
201
|
+
throw new Error("Wholesale webhook subscription does not match this mirror.");
|
|
202
|
+
const applies = event.payload.applies_to;
|
|
203
|
+
if (applies?.scope && applies.scope !== webhook.cache_scope || webhook.cache_scope === "account" && applies?.account_ids && !applies.account_ids.includes(accountId))
|
|
204
|
+
throw new Error("Wholesale webhook cache scope does not match this mirror.");
|
|
205
|
+
const previous = await store.read(scope);
|
|
206
|
+
if (await store.hasWebhookReceipt(scope, webhook.idempotency_key) || previous.lastWebhookEventId === event.event_id)
|
|
207
|
+
return { outcome: "unchanged", snapshot: previous };
|
|
208
|
+
if (previous.cacheScope !== webhook.cache_scope)
|
|
209
|
+
return refreshMirror(
|
|
210
|
+
{ ...options, conditional: false },
|
|
211
|
+
{ eventId: event.event_id, idempotencyKey: webhook.idempotency_key },
|
|
212
|
+
webhook.wholesale_feed_version
|
|
213
|
+
);
|
|
214
|
+
const stale = previous.lastWebhookEventId && isUuidV7(event.event_id) && isUuidV7(previous.lastWebhookEventId) && event.event_id.toLowerCase() <= previous.lastWebhookEventId.toLowerCase();
|
|
215
|
+
if (event.event_type === "wholesale_feed.bulk_change" || stale || !previous.wholesaleFeedVersion || webhook.previous_wholesale_feed_version !== previous.wholesaleFeedVersion) {
|
|
216
|
+
return refreshMirror(
|
|
217
|
+
options,
|
|
218
|
+
{ eventId: event.event_id, idempotencyKey: webhook.idempotency_key },
|
|
219
|
+
stale ? void 0 : webhook.wholesale_feed_version
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const items = applyWholesaleFeedEvent(scope.entity, previous.items, event);
|
|
223
|
+
if (!items)
|
|
224
|
+
return refreshMirror(
|
|
225
|
+
options,
|
|
226
|
+
{ eventId: event.event_id, idempotencyKey: webhook.idempotency_key },
|
|
227
|
+
webhook.wholesale_feed_version
|
|
228
|
+
);
|
|
229
|
+
const next = {
|
|
230
|
+
...previous,
|
|
231
|
+
items,
|
|
232
|
+
revision: previous.revision + 1,
|
|
233
|
+
wholesaleFeedVersion: webhook.wholesale_feed_version,
|
|
234
|
+
cacheScope: webhook.cache_scope,
|
|
235
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
236
|
+
lastWebhookEventId: event.event_id,
|
|
237
|
+
lastWebhookIdempotencyKey: webhook.idempotency_key,
|
|
238
|
+
error: void 0
|
|
239
|
+
};
|
|
240
|
+
if (options.isCurrent && !options.isCurrent()) return { outcome: "superseded" };
|
|
241
|
+
if (!await store.commit(scope, previous.revision, {
|
|
242
|
+
...rowDiff(scope.entity, previous.items, items),
|
|
243
|
+
wholesaleFeedVersion: next.wholesaleFeedVersion,
|
|
244
|
+
pricingVersion: next.pricingVersion,
|
|
245
|
+
cacheScope: next.cacheScope,
|
|
246
|
+
lastSyncedAt: next.lastSyncedAt,
|
|
247
|
+
lastWebhookEventId: event.event_id,
|
|
248
|
+
lastWebhookIdempotencyKey: webhook.idempotency_key,
|
|
249
|
+
webhookReceipt: { eventId: event.event_id, idempotencyKey: webhook.idempotency_key }
|
|
250
|
+
}))
|
|
251
|
+
return { outcome: "superseded" };
|
|
252
|
+
return { outcome: "applied", snapshot: next, diff: [event] };
|
|
253
|
+
}
|
|
254
|
+
function applyWholesaleFeedEvent(entity, previous, event) {
|
|
255
|
+
const items = new Map(previous.map((item) => [itemId(entity, item), item]));
|
|
256
|
+
const payload = event.payload;
|
|
257
|
+
const id = entity === "product" ? payload.product_id : payload.signal_agent_segment_id;
|
|
258
|
+
if (!id) return void 0;
|
|
259
|
+
if (event.event_type.endsWith(".removed")) items.delete(id);
|
|
260
|
+
else if (event.event_type.endsWith(".priced")) {
|
|
261
|
+
const prior = items.get(id);
|
|
262
|
+
if (!prior || !Array.isArray(payload.pricing_options)) return void 0;
|
|
263
|
+
items.set(id, { ...prior, pricing_options: payload.pricing_options });
|
|
264
|
+
} else {
|
|
265
|
+
const item = entity === "product" ? payload.product : payload.signal;
|
|
266
|
+
if (!item || itemId(entity, item) !== id) return void 0;
|
|
267
|
+
items.set(id, item);
|
|
268
|
+
}
|
|
269
|
+
return [...items.values()];
|
|
270
|
+
}
|
|
271
|
+
function diffWholesaleFeed(entity, previous, next, cacheScope, includeAllChanges = false) {
|
|
272
|
+
const before = new Map(previous.map((item) => [itemId(entity, item), item]));
|
|
273
|
+
const after = new Map(next.map((item) => [itemId(entity, item), item]));
|
|
274
|
+
const events = [];
|
|
275
|
+
const emit = (id, action, data) => events.push({
|
|
276
|
+
event_id: randomUUID(),
|
|
277
|
+
event_type: `${entity}.${action}`,
|
|
278
|
+
entity_type: entity,
|
|
279
|
+
entity_id: id,
|
|
280
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
281
|
+
payload: {
|
|
282
|
+
[entity === "product" ? "product_id" : "signal_agent_segment_id"]: id,
|
|
283
|
+
...data,
|
|
284
|
+
applies_to: { scope: cacheScope }
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
for (const [id, item] of after) {
|
|
288
|
+
const prior = before.get(id);
|
|
289
|
+
if (!prior) emit(id, "created", { [entity]: item });
|
|
290
|
+
else if (!isDeepStrictEqual(prior.pricing_options, item.pricing_options)) {
|
|
291
|
+
emit(id, "priced", { pricing_options: item.pricing_options ?? [] });
|
|
292
|
+
if (includeAllChanges && !isDeepStrictEqual({ ...prior, pricing_options: item.pricing_options }, item))
|
|
293
|
+
emit(id, "updated", { [entity]: item });
|
|
294
|
+
} else if (!isDeepStrictEqual(prior, item)) emit(id, "updated", { [entity]: item });
|
|
295
|
+
}
|
|
296
|
+
for (const id of before.keys()) if (!after.has(id)) emit(id, "removed", {});
|
|
297
|
+
return events;
|
|
298
|
+
}
|
|
299
|
+
function itemId(entity, item) {
|
|
300
|
+
return entity === "product" ? item.product_id : item.signal_agent_segment_id;
|
|
301
|
+
}
|
|
302
|
+
function rowDiff(entity, previous, next) {
|
|
303
|
+
const before = new Map(previous.map((item) => [itemId(entity, item), item]));
|
|
304
|
+
const ids = new Set(next.map((item) => itemId(entity, item)));
|
|
305
|
+
return {
|
|
306
|
+
upserts: next.filter((item) => !isDeepStrictEqual(before.get(itemId(entity, item)), item)),
|
|
307
|
+
deletes: [...before.keys()].filter((id) => !ids.has(id))
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function isUuidV7(value) {
|
|
311
|
+
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);
|
|
312
|
+
}
|
|
313
|
+
function latestEventId(previous, next) {
|
|
314
|
+
return previous && isUuidV7(previous) && (!isUuidV7(next) || previous.toLowerCase() >= next.toLowerCase()) ? previous : next;
|
|
315
|
+
}
|
|
316
|
+
export {
|
|
317
|
+
InMemoryWholesaleFeedMirrorStore,
|
|
318
|
+
applyWholesaleFeedEvent,
|
|
319
|
+
applyWholesaleFeedWebhook,
|
|
320
|
+
diffWholesaleFeed,
|
|
321
|
+
refreshWholesaleFeed
|
|
322
|
+
};
|
|
@@ -4,12 +4,8 @@ import type { WholesaleFeedSyncConfig, WholesaleFeedSyncEvents, WholesaleFeedSyn
|
|
|
4
4
|
type Product = LegacyWholesaleProduct;
|
|
5
5
|
type Signal = LegacyWholesaleSignal;
|
|
6
6
|
/**
|
|
7
|
-
* In-memory
|
|
8
|
-
*
|
|
9
|
-
* Discovers the agent's wholesale-feed capabilities at `start()`, picks the
|
|
10
|
-
* highest-capability sync strategy the agent supports, and maintains a
|
|
11
|
-
* local index for zero-latency lookups. Falls back gracefully to manual
|
|
12
|
-
* bootstrap when the agent does not advertise conditional-fetch tokens.
|
|
7
|
+
* In-memory wholesale product/signal mirror. `start()` discovers capabilities;
|
|
8
|
+
* versioned sellers use auto-poll, others require manual refresh.
|
|
13
9
|
*
|
|
14
10
|
* @example
|
|
15
11
|
* ```ts
|
|
@@ -63,11 +59,10 @@ export declare class WholesaleFeedSync extends EventEmitter<WholesaleFeedSyncEve
|
|
|
63
59
|
private probeTimer;
|
|
64
60
|
private capabilityTimer;
|
|
65
61
|
private lifecycleEpoch;
|
|
62
|
+
private mirrorRevision;
|
|
66
63
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* agents (products-feed + signals-wholesale-only, or vice versa) this
|
|
70
|
-
* may differ from `signals.mode`.
|
|
64
|
+
* Product index view. `mode` identifies the product sync strategy and may
|
|
65
|
+
* differ from `signals.mode` for agents with mixed feed capabilities.
|
|
71
66
|
*/
|
|
72
67
|
readonly products: {
|
|
73
68
|
list: () => Product[];
|
|
@@ -86,30 +81,17 @@ export declare class WholesaleFeedSync extends EventEmitter<WholesaleFeedSyncEve
|
|
|
86
81
|
readonly mode: WholesaleFeedSyncMode;
|
|
87
82
|
_mode: WholesaleFeedSyncMode;
|
|
88
83
|
/**
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
91
|
-
* agent only supports brief-mode discovery — `signals.list()` will
|
|
92
|
-
* be empty until adopters call into the agent with their own briefs.
|
|
84
|
+
* True when `get_signals` supports `discovery_mode: 'wholesale'`.
|
|
85
|
+
* Otherwise `list()` is empty; use client brief discovery instead.
|
|
93
86
|
*/
|
|
94
87
|
queryable: boolean;
|
|
95
88
|
};
|
|
96
|
-
/**
|
|
97
|
-
* One-shot console warning when adopters call `signals.list()` or
|
|
98
|
-
* `signals.search()` against an agent that doesn't support wholesale
|
|
99
|
-
* signal enumeration. Without this, empty results read as "no signals
|
|
100
|
-
* match" rather than "the agent doesn't browse, only briefs."
|
|
101
|
-
*/
|
|
102
89
|
private warnIfSignalsNotQueryable;
|
|
103
90
|
constructor(config: WholesaleFeedSyncConfig);
|
|
104
91
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* Safe to call repeatedly — concurrent calls await the in-flight
|
|
110
|
-
* bootstrap and return when it completes (no duplicate bootstrap, no
|
|
111
|
-
* silent drop). Sequential calls re-probe capabilities and re-bootstrap,
|
|
112
|
-
* equivalent to calling `refresh()` after a mode upgrade.
|
|
92
|
+
* Discover capabilities, bootstrap the mirror, and schedule auto-poll probes
|
|
93
|
+
* for versioned sellers. Concurrent calls share the bootstrap; sequential
|
|
94
|
+
* calls re-probe capabilities and refresh the mirror.
|
|
113
95
|
*/
|
|
114
96
|
start(): Promise<void>;
|
|
115
97
|
private startInner;
|
|
@@ -139,11 +121,12 @@ export declare class WholesaleFeedSync extends EventEmitter<WholesaleFeedSyncEve
|
|
|
139
121
|
get lastSyncedAt(): Date | undefined;
|
|
140
122
|
get lastEventAt(): Date | undefined;
|
|
141
123
|
private resolveMode;
|
|
124
|
+
private bootstrapWithRetries;
|
|
142
125
|
private bootstrap;
|
|
143
126
|
private handleBootstrapFailure;
|
|
144
|
-
/** Returns `true` when the seller short-circuited with `unchanged: true`. */
|
|
145
127
|
private bootstrapProducts;
|
|
146
128
|
private bootstrapSignals;
|
|
129
|
+
private bootstrapFeed;
|
|
147
130
|
private recoverFromBulkChange;
|
|
148
131
|
private recoverFromVersionMismatch;
|
|
149
132
|
private scheduleProbe;
|
|
@@ -4,12 +4,8 @@ import type { WholesaleFeedSyncConfig, WholesaleFeedSyncEvents, WholesaleFeedSyn
|
|
|
4
4
|
type Product = LegacyWholesaleProduct;
|
|
5
5
|
type Signal = LegacyWholesaleSignal;
|
|
6
6
|
/**
|
|
7
|
-
* In-memory
|
|
8
|
-
*
|
|
9
|
-
* Discovers the agent's wholesale-feed capabilities at `start()`, picks the
|
|
10
|
-
* highest-capability sync strategy the agent supports, and maintains a
|
|
11
|
-
* local index for zero-latency lookups. Falls back gracefully to manual
|
|
12
|
-
* bootstrap when the agent does not advertise conditional-fetch tokens.
|
|
7
|
+
* In-memory wholesale product/signal mirror. `start()` discovers capabilities;
|
|
8
|
+
* versioned sellers use auto-poll, others require manual refresh.
|
|
13
9
|
*
|
|
14
10
|
* @example
|
|
15
11
|
* ```ts
|
|
@@ -63,11 +59,10 @@ export declare class WholesaleFeedSync extends EventEmitter<WholesaleFeedSyncEve
|
|
|
63
59
|
private probeTimer;
|
|
64
60
|
private capabilityTimer;
|
|
65
61
|
private lifecycleEpoch;
|
|
62
|
+
private mirrorRevision;
|
|
66
63
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* agents (products-feed + signals-wholesale-only, or vice versa) this
|
|
70
|
-
* may differ from `signals.mode`.
|
|
64
|
+
* Product index view. `mode` identifies the product sync strategy and may
|
|
65
|
+
* differ from `signals.mode` for agents with mixed feed capabilities.
|
|
71
66
|
*/
|
|
72
67
|
readonly products: {
|
|
73
68
|
list: () => Product[];
|
|
@@ -86,30 +81,17 @@ export declare class WholesaleFeedSync extends EventEmitter<WholesaleFeedSyncEve
|
|
|
86
81
|
readonly mode: WholesaleFeedSyncMode;
|
|
87
82
|
_mode: WholesaleFeedSyncMode;
|
|
88
83
|
/**
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
91
|
-
* agent only supports brief-mode discovery — `signals.list()` will
|
|
92
|
-
* be empty until adopters call into the agent with their own briefs.
|
|
84
|
+
* True when `get_signals` supports `discovery_mode: 'wholesale'`.
|
|
85
|
+
* Otherwise `list()` is empty; use client brief discovery instead.
|
|
93
86
|
*/
|
|
94
87
|
queryable: boolean;
|
|
95
88
|
};
|
|
96
|
-
/**
|
|
97
|
-
* One-shot console warning when adopters call `signals.list()` or
|
|
98
|
-
* `signals.search()` against an agent that doesn't support wholesale
|
|
99
|
-
* signal enumeration. Without this, empty results read as "no signals
|
|
100
|
-
* match" rather than "the agent doesn't browse, only briefs."
|
|
101
|
-
*/
|
|
102
89
|
private warnIfSignalsNotQueryable;
|
|
103
90
|
constructor(config: WholesaleFeedSyncConfig);
|
|
104
91
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* Safe to call repeatedly — concurrent calls await the in-flight
|
|
110
|
-
* bootstrap and return when it completes (no duplicate bootstrap, no
|
|
111
|
-
* silent drop). Sequential calls re-probe capabilities and re-bootstrap,
|
|
112
|
-
* equivalent to calling `refresh()` after a mode upgrade.
|
|
92
|
+
* Discover capabilities, bootstrap the mirror, and schedule auto-poll probes
|
|
93
|
+
* for versioned sellers. Concurrent calls share the bootstrap; sequential
|
|
94
|
+
* calls re-probe capabilities and refresh the mirror.
|
|
113
95
|
*/
|
|
114
96
|
start(): Promise<void>;
|
|
115
97
|
private startInner;
|
|
@@ -139,11 +121,12 @@ export declare class WholesaleFeedSync extends EventEmitter<WholesaleFeedSyncEve
|
|
|
139
121
|
get lastSyncedAt(): Date | undefined;
|
|
140
122
|
get lastEventAt(): Date | undefined;
|
|
141
123
|
private resolveMode;
|
|
124
|
+
private bootstrapWithRetries;
|
|
142
125
|
private bootstrap;
|
|
143
126
|
private handleBootstrapFailure;
|
|
144
|
-
/** Returns `true` when the seller short-circuited with `unchanged: true`. */
|
|
145
127
|
private bootstrapProducts;
|
|
146
128
|
private bootstrapSignals;
|
|
129
|
+
private bootstrapFeed;
|
|
147
130
|
private recoverFromBulkChange;
|
|
148
131
|
private recoverFromVersionMismatch;
|
|
149
132
|
private scheduleProbe;
|