@adcp/sdk 14.0.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.
- package/dist/lib/adapters/implicit-account-store.d.mts +12 -7
- package/dist/lib/adapters/implicit-account-store.d.ts +12 -7
- package/dist/lib/adapters/implicit-account-store.js +69 -15
- package/dist/lib/adapters/implicit-account-store.mjs +69 -15
- package/dist/lib/core/AgentClient.d.mts +1 -0
- package/dist/lib/core/AgentClient.d.ts +1 -0
- package/dist/lib/core/AgentClient.js +3 -0
- package/dist/lib/core/AgentClient.mjs +3 -0
- package/dist/lib/core/SingleAgentClient.d.mts +35 -2
- package/dist/lib/core/SingleAgentClient.d.ts +35 -2
- package/dist/lib/core/SingleAgentClient.js +377 -36
- package/dist/lib/core/SingleAgentClient.mjs +387 -38
- package/dist/lib/core/TaskExecutor.d.mts +3 -1
- package/dist/lib/core/TaskExecutor.d.ts +3 -1
- package/dist/lib/core/TaskExecutor.js +17 -12
- package/dist/lib/core/TaskExecutor.mjs +17 -12
- package/dist/lib/core/account-key.d.mts +3 -0
- package/dist/lib/core/account-key.d.ts +3 -0
- package/dist/lib/core/account-key.js +41 -0
- package/dist/lib/core/account-key.mjs +17 -0
- package/dist/lib/core/account-resolution.d.mts +2 -0
- package/dist/lib/core/account-resolution.d.ts +2 -0
- package/dist/lib/core/buyer-account-registry.d.mts +93 -0
- package/dist/lib/core/buyer-account-registry.d.ts +93 -0
- package/dist/lib/core/buyer-account-registry.js +602 -0
- package/dist/lib/core/buyer-account-registry.mjs +578 -0
- package/dist/lib/core/product-cache.d.mts +18 -0
- package/dist/lib/core/product-cache.d.ts +18 -0
- package/dist/lib/core/product-cache.js +137 -0
- package/dist/lib/core/product-cache.mjs +112 -0
- package/dist/lib/errors/index.d.mts +40 -1
- package/dist/lib/errors/index.d.ts +40 -1
- package/dist/lib/errors/index.js +69 -3
- package/dist/lib/errors/index.mjs +64 -3
- package/dist/lib/governance/authorization.d.mts +17 -1
- package/dist/lib/governance/authorization.d.ts +17 -1
- package/dist/lib/governance/authorization.js +55 -7
- package/dist/lib/governance/authorization.mjs +59 -7
- package/dist/lib/governance/index.d.mts +2 -2
- package/dist/lib/governance/index.d.ts +2 -2
- package/dist/lib/governance/index.js +2 -0
- package/dist/lib/governance/index.mjs +3 -1
- package/dist/lib/index.d.mts +7 -4
- package/dist/lib/index.d.ts +7 -4
- package/dist/lib/index.js +29 -0
- package/dist/lib/index.mjs +31 -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/protocols/a2a.js +9 -1
- package/dist/lib/protocols/a2a.mjs +9 -1
- package/dist/lib/protocols/index.js +9 -2
- package/dist/lib/protocols/index.mjs +9 -2
- package/dist/lib/protocols/mcp-modern.js +2 -1
- package/dist/lib/protocols/mcp-modern.mjs +2 -1
- package/dist/lib/protocols/mcp.js +5 -2
- package/dist/lib/protocols/mcp.mjs +5 -2
- package/dist/lib/protocols/rawResponseCapture.d.mts +6 -0
- package/dist/lib/protocols/rawResponseCapture.d.ts +6 -0
- package/dist/lib/protocols/rawResponseCapture.js +41 -29
- package/dist/lib/protocols/rawResponseCapture.mjs +40 -29
- package/dist/lib/protocols/signedRequestRejection.d.mts +9 -0
- package/dist/lib/protocols/signedRequestRejection.d.ts +9 -0
- package/dist/lib/protocols/signedRequestRejection.js +209 -0
- package/dist/lib/protocols/signedRequestRejection.mjs +189 -0
- package/dist/lib/protocols/transportDiagnostics.d.mts +1 -0
- package/dist/lib/protocols/transportDiagnostics.d.ts +1 -0
- package/dist/lib/protocols/transportDiagnostics.js +2 -0
- package/dist/lib/protocols/transportDiagnostics.mjs +1 -0
- package/dist/lib/registry/types.generated.d.mts +112 -45
- package/dist/lib/registry/types.generated.d.ts +112 -45
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/account-provisioning.d.mts +2 -0
- package/dist/lib/server/account-provisioning.d.ts +2 -0
- package/dist/lib/server/account-provisioning.js +30 -0
- package/dist/lib/server/account-provisioning.mjs +6 -0
- package/dist/lib/server/account-reference-warnings.d.mts +12 -0
- package/dist/lib/server/account-reference-warnings.d.ts +12 -0
- package/dist/lib/server/account-reference-warnings.js +48 -0
- package/dist/lib/server/account-reference-warnings.mjs +23 -0
- package/dist/lib/server/auth-signature.js +1 -0
- package/dist/lib/server/auth-signature.mjs +1 -0
- package/dist/lib/server/create-adcp-server.d.mts +34 -0
- package/dist/lib/server/create-adcp-server.d.ts +34 -0
- package/dist/lib/server/create-adcp-server.js +225 -14
- package/dist/lib/server/create-adcp-server.mjs +225 -14
- package/dist/lib/server/decisioning/account.d.mts +2 -0
- package/dist/lib/server/decisioning/account.d.ts +2 -0
- package/dist/lib/server/decisioning/runtime/from-platform.js +49 -10
- package/dist/lib/server/decisioning/runtime/from-platform.mjs +49 -10
- package/dist/lib/server/index.d.mts +2 -2
- package/dist/lib/server/index.d.ts +2 -2
- package/dist/lib/server/index.js +2 -0
- package/dist/lib/server/index.mjs +3 -1
- package/dist/lib/signing/agent-resolver/consistency.d.mts +6 -13
- package/dist/lib/signing/agent-resolver/consistency.d.ts +6 -13
- package/dist/lib/signing/agent-resolver/consistency.js +0 -1
- package/dist/lib/signing/agent-resolver/consistency.mjs +0 -1
- package/dist/lib/signing/agent-resolver/errors.d.mts +3 -1
- package/dist/lib/signing/agent-resolver/errors.d.ts +3 -1
- 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 +11 -0
- package/dist/lib/signing/agent-resolver/fetch-helpers.d.ts +11 -0
- package/dist/lib/signing/agent-resolver/fetch-helpers.js +37 -2
- package/dist/lib/signing/agent-resolver/fetch-helpers.mjs +40 -3
- package/dist/lib/signing/agent-resolver/jwks-set.js +24 -3
- package/dist/lib/signing/agent-resolver/jwks-set.mjs +24 -3
- package/dist/lib/signing/agent-resolver/legacy-brand.d.mts +15 -0
- package/dist/lib/signing/agent-resolver/legacy-brand.d.ts +15 -0
- package/dist/lib/signing/agent-resolver/legacy-brand.js +60 -0
- package/dist/lib/signing/agent-resolver/legacy-brand.mjs +36 -0
- package/dist/lib/signing/agent-resolver/operator-authorization.d.mts +13 -0
- package/dist/lib/signing/agent-resolver/operator-authorization.d.ts +13 -0
- package/dist/lib/signing/agent-resolver/operator-authorization.js +108 -0
- package/dist/lib/signing/agent-resolver/operator-authorization.mjs +84 -0
- package/dist/lib/signing/agent-resolver/resolve-agent.d.mts +14 -4
- package/dist/lib/signing/agent-resolver/resolve-agent.d.ts +14 -4
- package/dist/lib/signing/agent-resolver/resolve-agent.js +148 -137
- package/dist/lib/signing/agent-resolver/resolve-agent.mjs +157 -139
- package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.mts +7 -1
- package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.ts +7 -1
- package/dist/lib/signing/agent-resolver/resolved-agent-jwks.js +50 -17
- package/dist/lib/signing/agent-resolver/resolved-agent-jwks.mjs +50 -17
- package/dist/lib/signing/agent-resolver/select-agent.d.mts +14 -15
- package/dist/lib/signing/agent-resolver/select-agent.d.ts +14 -15
- package/dist/lib/signing/agent-resolver/select-agent.js +98 -13
- package/dist/lib/signing/agent-resolver/select-agent.mjs +95 -13
- package/dist/lib/signing/brand-jwks.d.mts +29 -75
- package/dist/lib/signing/brand-jwks.d.ts +29 -75
- package/dist/lib/signing/brand-jwks.js +120 -182
- package/dist/lib/signing/brand-jwks.mjs +120 -182
- package/dist/lib/signing/errors.d.mts +7 -3
- package/dist/lib/signing/errors.d.ts +7 -3
- package/dist/lib/signing/errors.js +7 -2
- package/dist/lib/signing/errors.mjs +7 -2
- package/dist/lib/signing/jwks-https.d.mts +7 -0
- package/dist/lib/signing/jwks-https.d.ts +7 -0
- package/dist/lib/signing/jwks-https.js +31 -8
- package/dist/lib/signing/jwks-https.mjs +31 -8
- package/dist/lib/signing/jwks.d.mts +8 -0
- package/dist/lib/signing/jwks.d.ts +8 -0
- package/dist/lib/signing/middleware.js +2 -1
- package/dist/lib/signing/middleware.mjs +2 -1
- package/dist/lib/signing/publisher-pins.d.mts +11 -0
- package/dist/lib/signing/publisher-pins.d.ts +11 -0
- package/dist/lib/signing/publisher-pins.js +125 -0
- package/dist/lib/signing/publisher-pins.mjs +101 -0
- package/dist/lib/signing/server.d.mts +1 -0
- package/dist/lib/signing/server.d.ts +1 -0
- package/dist/lib/signing/types.d.mts +5 -0
- package/dist/lib/signing/types.d.ts +5 -0
- package/dist/lib/signing/verifier.js +50 -5
- package/dist/lib/signing/verifier.mjs +50 -5
- package/dist/lib/signing/webhook-verifier.d.mts +7 -2
- package/dist/lib/signing/webhook-verifier.d.ts +7 -2
- package/dist/lib/signing/webhook-verifier.js +42 -2
- package/dist/lib/signing/webhook-verifier.mjs +42 -2
- package/dist/lib/testing/storyboard/account-policy.d.mts +2 -0
- package/dist/lib/testing/storyboard/account-policy.d.ts +2 -0
- package/dist/lib/testing/storyboard/account-policy.js +35 -0
- package/dist/lib/testing/storyboard/account-policy.mjs +11 -0
- package/dist/lib/testing/storyboard/context.js +6 -0
- package/dist/lib/testing/storyboard/context.mjs +6 -0
- package/dist/lib/testing/storyboard/request-builder.js +12 -2
- package/dist/lib/testing/storyboard/request-builder.mjs +12 -2
- package/dist/lib/testing/storyboard/runner.js +3 -2
- package/dist/lib/testing/storyboard/runner.mjs +3 -2
- 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/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 +13 -29
- package/dist/lib/wholesale-feed-sync/sync.d.ts +13 -29
- package/dist/lib/wholesale-feed-sync/sync.js +208 -281
- package/dist/lib/wholesale-feed-sync/sync.mjs +213 -281
- package/dist/lib/wholesale-feed-sync/types.d.mts +4 -3
- package/dist/lib/wholesale-feed-sync/types.d.ts +4 -3
- package/docs/README.md +6 -0
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/guides/BUILD-AN-AGENT.md +2 -2
- package/docs/guides/BUYER-QUICKSTART-3.2.md +2 -0
- package/docs/guides/BUYER-STORAGE.md +3 -0
- package/docs/guides/FIRST-CALL-TO-A-SELLER.md +106 -0
- package/docs/guides/SIGNING-GUIDE.md +16 -7
- package/docs/guides/account-resolution.md +132 -10
- package/docs/llms.txt +3 -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-4.x-to-5.x.md +1 -0
- package/docs/migration-agent-resolution-3.3.md +125 -0
- package/docs/recipes/verifying-inbound-webhooks.md +60 -15
- package/package.json +3 -2
- 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,350 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var mirror_exports = {};
|
|
20
|
+
__export(mirror_exports, {
|
|
21
|
+
InMemoryWholesaleFeedMirrorStore: () => InMemoryWholesaleFeedMirrorStore,
|
|
22
|
+
applyWholesaleFeedEvent: () => applyWholesaleFeedEvent,
|
|
23
|
+
applyWholesaleFeedWebhook: () => applyWholesaleFeedWebhook,
|
|
24
|
+
diffWholesaleFeed: () => diffWholesaleFeed,
|
|
25
|
+
refreshWholesaleFeed: () => refreshWholesaleFeed
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(mirror_exports);
|
|
28
|
+
var import_node_util = require("node:util");
|
|
29
|
+
var import_node_crypto = require("node:crypto");
|
|
30
|
+
var import_webhook_notification = require('./webhook-notification.js');
|
|
31
|
+
class InMemoryWholesaleFeedMirrorStore {
|
|
32
|
+
/** Fail closed at capacity rather than evicting receipts inside the delivery retry window. */
|
|
33
|
+
constructor(maxWebhookReceipts = 1e4) {
|
|
34
|
+
this.maxWebhookReceipts = maxWebhookReceipts;
|
|
35
|
+
if (!Number.isSafeInteger(maxWebhookReceipts) || maxWebhookReceipts < 1)
|
|
36
|
+
throw new Error("maxWebhookReceipts must be a positive integer.");
|
|
37
|
+
}
|
|
38
|
+
maxWebhookReceipts;
|
|
39
|
+
records = /* @__PURE__ */ new Map();
|
|
40
|
+
receipts = /* @__PURE__ */ new Map();
|
|
41
|
+
receiptCount = 0;
|
|
42
|
+
async hasWebhookReceipt(scope, idempotencyKey) {
|
|
43
|
+
return this.receipts.get(this.key(scope))?.has(idempotencyKey) ?? false;
|
|
44
|
+
}
|
|
45
|
+
key(scope) {
|
|
46
|
+
return JSON.stringify([scope.agentUrl, scope.accountKey, scope.entity]);
|
|
47
|
+
}
|
|
48
|
+
async read(scope) {
|
|
49
|
+
return structuredClone(this.records.get(this.key(scope)) ?? { revision: 0, items: [], cacheScope: "public" });
|
|
50
|
+
}
|
|
51
|
+
/** Seed a detached record when restoring a shell snapshot. */
|
|
52
|
+
restore(scope, snapshot) {
|
|
53
|
+
this.records.set(this.key(scope), structuredClone(snapshot));
|
|
54
|
+
}
|
|
55
|
+
async commit(scope, expectedRevision, change) {
|
|
56
|
+
const key = this.key(scope);
|
|
57
|
+
const prior = this.records.get(key) ?? { revision: 0, items: [], cacheScope: "public" };
|
|
58
|
+
if (prior.revision !== expectedRevision) return false;
|
|
59
|
+
const items = new Map(prior.items.map((item) => [itemId(scope.entity, item), item]));
|
|
60
|
+
for (const id of change.deletes) items.delete(id);
|
|
61
|
+
for (const item of change.upserts) items.set(itemId(scope.entity, item), structuredClone(item));
|
|
62
|
+
const { upserts: _upserts, deletes: _deletes, webhookReceipt, ...metadata } = change;
|
|
63
|
+
const next = {
|
|
64
|
+
...prior,
|
|
65
|
+
...structuredClone(metadata),
|
|
66
|
+
revision: prior.revision + 1,
|
|
67
|
+
items: [...items.values()],
|
|
68
|
+
error: void 0
|
|
69
|
+
};
|
|
70
|
+
if (webhookReceipt) {
|
|
71
|
+
const receipts = this.receipts.get(key) ?? /* @__PURE__ */ new Set();
|
|
72
|
+
if (!receipts.has(webhookReceipt.idempotencyKey)) {
|
|
73
|
+
if (this.receiptCount >= this.maxWebhookReceipts)
|
|
74
|
+
throw new Error("Webhook receipt capacity reached; use durable storage or increase maxWebhookReceipts.");
|
|
75
|
+
receipts.add(webhookReceipt.idempotencyKey);
|
|
76
|
+
++this.receiptCount;
|
|
77
|
+
}
|
|
78
|
+
this.receipts.set(key, receipts);
|
|
79
|
+
}
|
|
80
|
+
this.records.set(key, next);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
async recordError(scope, expectedRevision, error) {
|
|
84
|
+
const key = this.key(scope);
|
|
85
|
+
const prior = this.records.get(key) ?? { revision: 0, items: [], cacheScope: "public" };
|
|
86
|
+
if (prior.revision !== expectedRevision) return false;
|
|
87
|
+
this.records.set(key, { ...prior, revision: prior.revision + 1, error: structuredClone(error) });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function refreshWholesaleFeed(options) {
|
|
92
|
+
return refreshMirror(options);
|
|
93
|
+
}
|
|
94
|
+
async function refreshMirror(options, receipt, expectedWebhookVersion) {
|
|
95
|
+
options = { ...options, scope: structuredClone(options.scope), account: structuredClone(options.account) };
|
|
96
|
+
const { store, scope, client } = options;
|
|
97
|
+
const previous = await store.read(scope);
|
|
98
|
+
if (options.isCurrent && !options.isCurrent()) return { outcome: "superseded" };
|
|
99
|
+
let cursor;
|
|
100
|
+
const cursors = /* @__PURE__ */ new Set();
|
|
101
|
+
const items = /* @__PURE__ */ new Map();
|
|
102
|
+
let wholesaleFeedVersion;
|
|
103
|
+
let pricingVersion;
|
|
104
|
+
let cacheScope = previous.cacheScope;
|
|
105
|
+
let unchanged = false;
|
|
106
|
+
let page = 0;
|
|
107
|
+
try {
|
|
108
|
+
do {
|
|
109
|
+
if (++page > 1e4) throw new Error("Wholesale feed pagination exceeded its page limit.");
|
|
110
|
+
const params = {
|
|
111
|
+
...scope.entity === "product" ? { buying_mode: "wholesale" } : { discovery_mode: "wholesale" },
|
|
112
|
+
pagination: { max_results: 100, ...cursor && { cursor } },
|
|
113
|
+
...options.account && { account: options.account },
|
|
114
|
+
...!cursor && options.conditional !== false && previous.wholesaleFeedVersion && {
|
|
115
|
+
if_wholesale_feed_version: previous.wholesaleFeedVersion,
|
|
116
|
+
...previous.pricingVersion && { if_pricing_version: previous.pricingVersion }
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const result = await (scope.entity === "product" ? client.getProducts(params) : client.getSignals(params));
|
|
120
|
+
if (options.isCurrent && !options.isCurrent()) return { outcome: "superseded" };
|
|
121
|
+
if (result.success === false || result.status !== void 0 && !["completed", "success"].includes(result.status)) {
|
|
122
|
+
throw Object.assign(new Error(result.error ?? "Wholesale feed refresh failed."), {
|
|
123
|
+
adcpError: result.adcpError
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const body = result.data;
|
|
127
|
+
if (!body) throw new Error("Wholesale feed did not return a completed catalog.");
|
|
128
|
+
if (body.unchanged) {
|
|
129
|
+
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) {
|
|
130
|
+
throw new Error("Wholesale feed unchanged response must match both requested version tokens.");
|
|
131
|
+
}
|
|
132
|
+
unchanged = true;
|
|
133
|
+
}
|
|
134
|
+
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)) {
|
|
135
|
+
throw new Error("Wholesale feed versions changed during pagination.");
|
|
136
|
+
}
|
|
137
|
+
wholesaleFeedVersion = body.wholesale_feed_version ?? wholesaleFeedVersion;
|
|
138
|
+
pricingVersion = body.pricing_version ?? pricingVersion;
|
|
139
|
+
cacheScope = body.cache_scope ?? cacheScope;
|
|
140
|
+
if (unchanged) break;
|
|
141
|
+
const rows = scope.entity === "product" ? body.products : body.signals;
|
|
142
|
+
if (!Array.isArray(rows)) throw new Error("Wholesale feed response is missing its catalog rows.");
|
|
143
|
+
for (const item of rows) {
|
|
144
|
+
const id = itemId(scope.entity, item);
|
|
145
|
+
if (!id) throw new Error("Wholesale feed row is missing its entity id.");
|
|
146
|
+
items.set(id, item);
|
|
147
|
+
}
|
|
148
|
+
cursor = body.pagination?.has_more ? body.pagination.cursor : void 0;
|
|
149
|
+
if (body.pagination?.has_more && (!cursor || cursors.has(cursor)))
|
|
150
|
+
throw new Error("Wholesale feed pagination did not advance.");
|
|
151
|
+
if (cursor) cursors.add(cursor);
|
|
152
|
+
} while (cursor);
|
|
153
|
+
if (receipt && expectedWebhookVersion !== void 0 && wholesaleFeedVersion === previous.wholesaleFeedVersion && wholesaleFeedVersion !== expectedWebhookVersion)
|
|
154
|
+
throw new Error("Wholesale repair has not observed the webhook version; retry delivery.");
|
|
155
|
+
} catch (cause) {
|
|
156
|
+
if (options.isCurrent && !options.isCurrent()) return { outcome: "superseded" };
|
|
157
|
+
const errorCause = cause instanceof Error ? cause : new Error(String(cause));
|
|
158
|
+
const error = cause?.adcpError ?? {
|
|
159
|
+
code: "SERVICE_UNAVAILABLE",
|
|
160
|
+
recovery: "transient",
|
|
161
|
+
message: "Wholesale feed refresh failed.",
|
|
162
|
+
synthetic: true
|
|
163
|
+
};
|
|
164
|
+
if (!await store.recordError(scope, previous.revision, error)) return { outcome: "superseded" };
|
|
165
|
+
return { outcome: "degraded", error, cause: errorCause };
|
|
166
|
+
}
|
|
167
|
+
const nextItems = unchanged ? previous.items : [...items.values()];
|
|
168
|
+
const next = {
|
|
169
|
+
...previous,
|
|
170
|
+
items: nextItems,
|
|
171
|
+
wholesaleFeedVersion,
|
|
172
|
+
pricingVersion,
|
|
173
|
+
cacheScope,
|
|
174
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
175
|
+
revision: previous.revision + 1,
|
|
176
|
+
error: void 0,
|
|
177
|
+
...receipt && {
|
|
178
|
+
lastWebhookEventId: latestEventId(previous.lastWebhookEventId, receipt.eventId),
|
|
179
|
+
lastWebhookIdempotencyKey: receipt.idempotencyKey
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const change = rowDiff(scope.entity, previous.items, nextItems);
|
|
183
|
+
if (options.isCurrent && !options.isCurrent()) return { outcome: "superseded" };
|
|
184
|
+
if (!await store.commit(scope, previous.revision, {
|
|
185
|
+
...change,
|
|
186
|
+
wholesaleFeedVersion,
|
|
187
|
+
pricingVersion,
|
|
188
|
+
cacheScope,
|
|
189
|
+
lastSyncedAt: next.lastSyncedAt,
|
|
190
|
+
...receipt && {
|
|
191
|
+
webhookReceipt: receipt,
|
|
192
|
+
lastWebhookEventId: next.lastWebhookEventId,
|
|
193
|
+
lastWebhookIdempotencyKey: receipt.idempotencyKey
|
|
194
|
+
}
|
|
195
|
+
}))
|
|
196
|
+
return { outcome: "superseded" };
|
|
197
|
+
return unchanged ? { outcome: "unchanged", snapshot: next } : {
|
|
198
|
+
outcome: "applied",
|
|
199
|
+
snapshot: next,
|
|
200
|
+
diff: diffWholesaleFeed(scope.entity, previous.items, nextItems, cacheScope, true)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function applyWholesaleFeedWebhook(options) {
|
|
204
|
+
options = {
|
|
205
|
+
...options,
|
|
206
|
+
scope: structuredClone(options.scope),
|
|
207
|
+
account: structuredClone(options.account),
|
|
208
|
+
webhookScope: structuredClone(options.webhookScope),
|
|
209
|
+
webhook: structuredClone(options.webhook)
|
|
210
|
+
};
|
|
211
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
212
|
+
const outcome = await applyWebhookOnce(options);
|
|
213
|
+
if (outcome.outcome !== "superseded" || options.isCurrent && !options.isCurrent()) return outcome;
|
|
214
|
+
}
|
|
215
|
+
return { outcome: "superseded" };
|
|
216
|
+
}
|
|
217
|
+
async function applyWebhookOnce(options) {
|
|
218
|
+
const { webhook, scope, store } = options;
|
|
219
|
+
(0, import_webhook_notification.parseWholesaleFeedWebhookNotification)(webhook);
|
|
220
|
+
const event = webhook.event;
|
|
221
|
+
if (!event || webhook.notification_type !== event.event_type || webhook.notification_id !== event.event_id)
|
|
222
|
+
throw new Error("Wholesale webhook event identity does not match its envelope.");
|
|
223
|
+
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;
|
|
224
|
+
if (entity !== scope.entity) throw new Error("Wholesale webhook entity does not match this mirror.");
|
|
225
|
+
const accountId = options.webhookScope?.accountId ?? (options.account && "account_id" in options.account ? options.account.account_id : void 0);
|
|
226
|
+
if (!accountId) throw new Error("Wholesale webhook application requires an expected account_id.");
|
|
227
|
+
if (webhook.account_id !== accountId || options.webhookScope?.subscriberId && options.webhookScope.subscriberId !== webhook.subscriber_id)
|
|
228
|
+
throw new Error("Wholesale webhook subscription does not match this mirror.");
|
|
229
|
+
const applies = event.payload.applies_to;
|
|
230
|
+
if (applies?.scope && applies.scope !== webhook.cache_scope || webhook.cache_scope === "account" && applies?.account_ids && !applies.account_ids.includes(accountId))
|
|
231
|
+
throw new Error("Wholesale webhook cache scope does not match this mirror.");
|
|
232
|
+
const previous = await store.read(scope);
|
|
233
|
+
if (await store.hasWebhookReceipt(scope, webhook.idempotency_key) || previous.lastWebhookEventId === event.event_id)
|
|
234
|
+
return { outcome: "unchanged", snapshot: previous };
|
|
235
|
+
if (previous.cacheScope !== webhook.cache_scope)
|
|
236
|
+
return refreshMirror(
|
|
237
|
+
{ ...options, conditional: false },
|
|
238
|
+
{ eventId: event.event_id, idempotencyKey: webhook.idempotency_key },
|
|
239
|
+
webhook.wholesale_feed_version
|
|
240
|
+
);
|
|
241
|
+
const stale = previous.lastWebhookEventId && isUuidV7(event.event_id) && isUuidV7(previous.lastWebhookEventId) && event.event_id.toLowerCase() <= previous.lastWebhookEventId.toLowerCase();
|
|
242
|
+
if (event.event_type === "wholesale_feed.bulk_change" || stale || !previous.wholesaleFeedVersion || webhook.previous_wholesale_feed_version !== previous.wholesaleFeedVersion) {
|
|
243
|
+
return refreshMirror(
|
|
244
|
+
options,
|
|
245
|
+
{ eventId: event.event_id, idempotencyKey: webhook.idempotency_key },
|
|
246
|
+
stale ? void 0 : webhook.wholesale_feed_version
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const items = applyWholesaleFeedEvent(scope.entity, previous.items, event);
|
|
250
|
+
if (!items)
|
|
251
|
+
return refreshMirror(
|
|
252
|
+
options,
|
|
253
|
+
{ eventId: event.event_id, idempotencyKey: webhook.idempotency_key },
|
|
254
|
+
webhook.wholesale_feed_version
|
|
255
|
+
);
|
|
256
|
+
const next = {
|
|
257
|
+
...previous,
|
|
258
|
+
items,
|
|
259
|
+
revision: previous.revision + 1,
|
|
260
|
+
wholesaleFeedVersion: webhook.wholesale_feed_version,
|
|
261
|
+
cacheScope: webhook.cache_scope,
|
|
262
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
263
|
+
lastWebhookEventId: event.event_id,
|
|
264
|
+
lastWebhookIdempotencyKey: webhook.idempotency_key,
|
|
265
|
+
error: void 0
|
|
266
|
+
};
|
|
267
|
+
if (options.isCurrent && !options.isCurrent()) return { outcome: "superseded" };
|
|
268
|
+
if (!await store.commit(scope, previous.revision, {
|
|
269
|
+
...rowDiff(scope.entity, previous.items, items),
|
|
270
|
+
wholesaleFeedVersion: next.wholesaleFeedVersion,
|
|
271
|
+
pricingVersion: next.pricingVersion,
|
|
272
|
+
cacheScope: next.cacheScope,
|
|
273
|
+
lastSyncedAt: next.lastSyncedAt,
|
|
274
|
+
lastWebhookEventId: event.event_id,
|
|
275
|
+
lastWebhookIdempotencyKey: webhook.idempotency_key,
|
|
276
|
+
webhookReceipt: { eventId: event.event_id, idempotencyKey: webhook.idempotency_key }
|
|
277
|
+
}))
|
|
278
|
+
return { outcome: "superseded" };
|
|
279
|
+
return { outcome: "applied", snapshot: next, diff: [event] };
|
|
280
|
+
}
|
|
281
|
+
function applyWholesaleFeedEvent(entity, previous, event) {
|
|
282
|
+
const items = new Map(previous.map((item) => [itemId(entity, item), item]));
|
|
283
|
+
const payload = event.payload;
|
|
284
|
+
const id = entity === "product" ? payload.product_id : payload.signal_agent_segment_id;
|
|
285
|
+
if (!id) return void 0;
|
|
286
|
+
if (event.event_type.endsWith(".removed")) items.delete(id);
|
|
287
|
+
else if (event.event_type.endsWith(".priced")) {
|
|
288
|
+
const prior = items.get(id);
|
|
289
|
+
if (!prior || !Array.isArray(payload.pricing_options)) return void 0;
|
|
290
|
+
items.set(id, { ...prior, pricing_options: payload.pricing_options });
|
|
291
|
+
} else {
|
|
292
|
+
const item = entity === "product" ? payload.product : payload.signal;
|
|
293
|
+
if (!item || itemId(entity, item) !== id) return void 0;
|
|
294
|
+
items.set(id, item);
|
|
295
|
+
}
|
|
296
|
+
return [...items.values()];
|
|
297
|
+
}
|
|
298
|
+
function diffWholesaleFeed(entity, previous, next, cacheScope, includeAllChanges = false) {
|
|
299
|
+
const before = new Map(previous.map((item) => [itemId(entity, item), item]));
|
|
300
|
+
const after = new Map(next.map((item) => [itemId(entity, item), item]));
|
|
301
|
+
const events = [];
|
|
302
|
+
const emit = (id, action, data) => events.push({
|
|
303
|
+
event_id: (0, import_node_crypto.randomUUID)(),
|
|
304
|
+
event_type: `${entity}.${action}`,
|
|
305
|
+
entity_type: entity,
|
|
306
|
+
entity_id: id,
|
|
307
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
308
|
+
payload: {
|
|
309
|
+
[entity === "product" ? "product_id" : "signal_agent_segment_id"]: id,
|
|
310
|
+
...data,
|
|
311
|
+
applies_to: { scope: cacheScope }
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
for (const [id, item] of after) {
|
|
315
|
+
const prior = before.get(id);
|
|
316
|
+
if (!prior) emit(id, "created", { [entity]: item });
|
|
317
|
+
else if (!(0, import_node_util.isDeepStrictEqual)(prior.pricing_options, item.pricing_options)) {
|
|
318
|
+
emit(id, "priced", { pricing_options: item.pricing_options ?? [] });
|
|
319
|
+
if (includeAllChanges && !(0, import_node_util.isDeepStrictEqual)({ ...prior, pricing_options: item.pricing_options }, item))
|
|
320
|
+
emit(id, "updated", { [entity]: item });
|
|
321
|
+
} else if (!(0, import_node_util.isDeepStrictEqual)(prior, item)) emit(id, "updated", { [entity]: item });
|
|
322
|
+
}
|
|
323
|
+
for (const id of before.keys()) if (!after.has(id)) emit(id, "removed", {});
|
|
324
|
+
return events;
|
|
325
|
+
}
|
|
326
|
+
function itemId(entity, item) {
|
|
327
|
+
return entity === "product" ? item.product_id : item.signal_agent_segment_id;
|
|
328
|
+
}
|
|
329
|
+
function rowDiff(entity, previous, next) {
|
|
330
|
+
const before = new Map(previous.map((item) => [itemId(entity, item), item]));
|
|
331
|
+
const ids = new Set(next.map((item) => itemId(entity, item)));
|
|
332
|
+
return {
|
|
333
|
+
upserts: next.filter((item) => !(0, import_node_util.isDeepStrictEqual)(before.get(itemId(entity, item)), item)),
|
|
334
|
+
deletes: [...before.keys()].filter((id) => !ids.has(id))
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function isUuidV7(value) {
|
|
338
|
+
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);
|
|
339
|
+
}
|
|
340
|
+
function latestEventId(previous, next) {
|
|
341
|
+
return previous && isUuidV7(previous) && (!isUuidV7(next) || previous.toLowerCase() >= next.toLowerCase()) ? previous : next;
|
|
342
|
+
}
|
|
343
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
344
|
+
0 && (module.exports = {
|
|
345
|
+
InMemoryWholesaleFeedMirrorStore,
|
|
346
|
+
applyWholesaleFeedEvent,
|
|
347
|
+
applyWholesaleFeedWebhook,
|
|
348
|
+
diffWholesaleFeed,
|
|
349
|
+
refreshWholesaleFeed
|
|
350
|
+
});
|
|
@@ -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
|
+
};
|