@adcp/sdk 14.0.0-rc.47 → 14.0.0-rc.48
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/registry/types.generated.d.mts +7 -0
- package/dist/lib/registry/types.generated.d.ts +7 -0
- package/dist/lib/reporting/consumer-postgres.d.mts +81 -0
- package/dist/lib/reporting/consumer-postgres.d.ts +81 -0
- package/dist/lib/reporting/consumer-postgres.js +628 -0
- package/dist/lib/reporting/consumer-postgres.mjs +601 -0
- package/dist/lib/reporting/consumer-runtime.d.mts +93 -0
- package/dist/lib/reporting/consumer-runtime.d.ts +93 -0
- package/dist/lib/reporting/consumer-runtime.js +655 -0
- package/dist/lib/reporting/consumer-runtime.mjs +633 -0
- package/dist/lib/reporting/index.d.mts +8 -2
- package/dist/lib/reporting/index.d.ts +8 -2
- package/dist/lib/reporting/index.js +35 -2
- package/dist/lib/reporting/index.mjs +34 -1
- package/dist/lib/reporting/ledger/managed-postgres.d.mts +15 -1
- package/dist/lib/reporting/ledger/managed-postgres.d.ts +15 -1
- package/dist/lib/reporting/ledger/managed-postgres.js +150 -7
- package/dist/lib/reporting/ledger/managed-postgres.mjs +150 -7
- package/dist/lib/reporting/ledger/notification-activity.d.mts +56 -8
- package/dist/lib/reporting/ledger/notification-activity.d.ts +56 -8
- package/dist/lib/reporting/ledger/notification-activity.js +379 -152
- package/dist/lib/reporting/ledger/notification-activity.mjs +379 -152
- package/dist/lib/reporting/ledger/postgres.js +12 -3
- package/dist/lib/reporting/ledger/postgres.mjs +12 -3
- package/dist/lib/reporting/ledger/types.d.mts +14 -0
- package/dist/lib/reporting/ledger/types.d.ts +14 -0
- package/dist/lib/reporting/reconciliation.d.mts +76 -10
- package/dist/lib/reporting/reconciliation.d.ts +76 -10
- package/dist/lib/reporting/reconciliation.js +397 -44
- package/dist/lib/reporting/reconciliation.mjs +396 -44
- package/dist/lib/reporting/service/index.d.mts +95 -2
- package/dist/lib/reporting/service/index.d.ts +95 -2
- package/dist/lib/reporting/service/index.js +333 -4
- package/dist/lib/reporting/service/index.mjs +346 -5
- package/dist/lib/reporting/webhook-activity.d.mts +75 -0
- package/dist/lib/reporting/webhook-activity.d.ts +75 -0
- package/dist/lib/reporting/webhook-activity.js +720 -0
- package/dist/lib/reporting/webhook-activity.mjs +689 -0
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/create-adcp-server.d.mts +7 -4
- package/dist/lib/server/create-adcp-server.d.ts +7 -4
- package/dist/lib/server/create-adcp-server.js +4 -4
- package/dist/lib/server/create-adcp-server.mjs +4 -4
- package/dist/lib/server/decisioning/runtime/from-platform.js +23 -10
- package/dist/lib/server/decisioning/runtime/from-platform.mjs +23 -10
- package/dist/lib/server/decisioning/specialisms/reporting.d.mts +6 -2
- package/dist/lib/server/decisioning/specialisms/reporting.d.ts +6 -2
- package/dist/lib/server/notification-subscriptions/runtime.js +33 -10
- package/dist/lib/server/notification-subscriptions/runtime.mjs +33 -10
- package/dist/lib/server/notification-subscriptions/types.d.mts +4 -2
- package/dist/lib/server/notification-subscriptions/types.d.ts +4 -2
- package/dist/lib/server/webhook-delivery/recovery.js +3 -0
- package/dist/lib/server/webhook-delivery/recovery.mjs +3 -0
- package/dist/lib/server/webhook-emitter.d.mts +23 -2
- package/dist/lib/server/webhook-emitter.d.ts +23 -2
- package/dist/lib/server/webhook-emitter.js +85 -16
- package/dist/lib/server/webhook-emitter.mjs +85 -16
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -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/docs/TYPE-SUMMARY.md +6 -2
- package/docs/guides/REPORTING-LEDGER.md +124 -7
- package/docs/guides/REPORTING-OPERATIONS.md +194 -0
- package/docs/guides/REPORTING-RECONCILIATION.md +151 -4
- package/docs/llms.txt +4 -2
- package/docs/migration-14.x-rc-worksheet.md +49 -4
- package/examples/reliable-reporting-service/README.md +69 -0
- package/package.json +1 -1
|
@@ -0,0 +1,628 @@
|
|
|
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 consumer_postgres_exports = {};
|
|
20
|
+
__export(consumer_postgres_exports, {
|
|
21
|
+
REPORTING_CONSUMER_POSTGRES_MIGRATION: () => REPORTING_CONSUMER_POSTGRES_MIGRATION,
|
|
22
|
+
ReportingConsumerPersistenceConflictError: () => ReportingConsumerPersistenceConflictError,
|
|
23
|
+
createPostgresReportingConsumerRuntimeV1: () => createPostgresReportingConsumerRuntimeV1,
|
|
24
|
+
getReportingConsumerPostgresMigration: () => getReportingConsumerPostgresMigration
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(consumer_postgres_exports);
|
|
27
|
+
var import_jcs = require('../utils/jcs.js');
|
|
28
|
+
const DEFAULT_PREFIX = "adcp_reporting_consumer";
|
|
29
|
+
const MAX_JSON_BYTES = 256 * 1024;
|
|
30
|
+
const MAX_CHECKPOINT_BYTES = 16 * 1024;
|
|
31
|
+
class ReportingConsumerPersistenceConflictError extends Error {
|
|
32
|
+
name = "ReportingConsumerPersistenceConflictError";
|
|
33
|
+
}
|
|
34
|
+
function getReportingConsumerPostgresMigration(options = {}) {
|
|
35
|
+
const prefix = tablePrefix(options.tablePrefix);
|
|
36
|
+
const checkpoints = quoteIdentifier(`${prefix}_checkpoints`);
|
|
37
|
+
const statuses = quoteIdentifier(`${prefix}_statuses`);
|
|
38
|
+
const cursors = quoteIdentifier(`${prefix}_cursors`);
|
|
39
|
+
const leases = quoteIdentifier(`${prefix}_leases`);
|
|
40
|
+
const notifications = quoteIdentifier(`${prefix}_notifications`);
|
|
41
|
+
return `
|
|
42
|
+
CREATE TABLE IF NOT EXISTS ${checkpoints} (
|
|
43
|
+
namespace TEXT NOT NULL,
|
|
44
|
+
key_sha256 TEXT NOT NULL,
|
|
45
|
+
value_sha256 TEXT NOT NULL,
|
|
46
|
+
value JSONB NOT NULL,
|
|
47
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
48
|
+
changed_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
49
|
+
PRIMARY KEY (namespace, key_sha256),
|
|
50
|
+
CHECK (key_sha256 ~ '^[a-f0-9]{64}$'),
|
|
51
|
+
CHECK (value_sha256 ~ '^[a-f0-9]{64}$'),
|
|
52
|
+
CHECK (jsonb_typeof(value) = 'object')
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS ${statuses} (
|
|
56
|
+
namespace TEXT NOT NULL,
|
|
57
|
+
key_sha256 TEXT NOT NULL,
|
|
58
|
+
value_sha256 TEXT NOT NULL,
|
|
59
|
+
value JSONB NOT NULL,
|
|
60
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
61
|
+
changed_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
62
|
+
PRIMARY KEY (namespace, key_sha256),
|
|
63
|
+
CHECK (key_sha256 ~ '^[a-f0-9]{64}$'),
|
|
64
|
+
CHECK (value_sha256 ~ '^[a-f0-9]{64}$'),
|
|
65
|
+
CHECK (jsonb_typeof(value) = 'object')
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS ${cursors} (
|
|
69
|
+
namespace TEXT NOT NULL,
|
|
70
|
+
scope_key TEXT NOT NULL,
|
|
71
|
+
account_id TEXT NOT NULL,
|
|
72
|
+
checkpoint TEXT NOT NULL,
|
|
73
|
+
generation BIGINT NOT NULL DEFAULT 1,
|
|
74
|
+
changed_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
75
|
+
PRIMARY KEY (namespace, scope_key, account_id),
|
|
76
|
+
CHECK (scope_key ~ '^[a-f0-9]{64}$'),
|
|
77
|
+
CHECK (generation > 0)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
CREATE TABLE IF NOT EXISTS ${leases} (
|
|
81
|
+
namespace TEXT NOT NULL,
|
|
82
|
+
scope_key TEXT NOT NULL,
|
|
83
|
+
account_id TEXT NOT NULL,
|
|
84
|
+
owner_token TEXT NOT NULL,
|
|
85
|
+
generation BIGINT NOT NULL DEFAULT 1,
|
|
86
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
87
|
+
changed_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
88
|
+
PRIMARY KEY (namespace, scope_key, account_id),
|
|
89
|
+
CHECK (scope_key ~ '^[a-f0-9]{64}$'),
|
|
90
|
+
CHECK (generation > 0)
|
|
91
|
+
);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS ${prefix}_leases_expiry
|
|
93
|
+
ON ${leases}(namespace, expires_at);
|
|
94
|
+
|
|
95
|
+
CREATE TABLE IF NOT EXISTS ${notifications} (
|
|
96
|
+
namespace TEXT NOT NULL,
|
|
97
|
+
scope_key TEXT NOT NULL,
|
|
98
|
+
account_id TEXT NOT NULL,
|
|
99
|
+
idempotency_key TEXT NOT NULL,
|
|
100
|
+
payload_sha256 TEXT NOT NULL,
|
|
101
|
+
processed_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
102
|
+
PRIMARY KEY (namespace, scope_key, account_id, idempotency_key),
|
|
103
|
+
CHECK (scope_key ~ '^[a-f0-9]{64}$'),
|
|
104
|
+
CHECK (payload_sha256 ~ '^[a-f0-9]{64}$')
|
|
105
|
+
);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS ${prefix}_notifications_retention
|
|
107
|
+
ON ${notifications}(namespace, processed_at);
|
|
108
|
+
`.trim();
|
|
109
|
+
}
|
|
110
|
+
const REPORTING_CONSUMER_POSTGRES_MIGRATION = getReportingConsumerPostgresMigration();
|
|
111
|
+
function createPostgresReportingConsumerRuntimeV1(options) {
|
|
112
|
+
if (!options?.db || typeof options.db.query !== "function") {
|
|
113
|
+
throw new TypeError("createPostgresReportingConsumerRuntimeV1 requires a PostgreSQL queryable");
|
|
114
|
+
}
|
|
115
|
+
assertBoundedString(options.namespace, "namespace", 255);
|
|
116
|
+
const prefix = tablePrefix(options.tablePrefix);
|
|
117
|
+
const checkpoints = quoteIdentifier(`${prefix}_checkpoints`);
|
|
118
|
+
const statuses = quoteIdentifier(`${prefix}_statuses`);
|
|
119
|
+
const cursors = quoteIdentifier(`${prefix}_cursors`);
|
|
120
|
+
const leases = quoteIdentifier(`${prefix}_leases`);
|
|
121
|
+
const notifications = quoteIdentifier(`${prefix}_notifications`);
|
|
122
|
+
const namespace = options.namespace;
|
|
123
|
+
const checkpointStore = {
|
|
124
|
+
async get(key) {
|
|
125
|
+
const keySha = checkpointKey(key);
|
|
126
|
+
const result = await query(
|
|
127
|
+
"read receipt checkpoint",
|
|
128
|
+
`SELECT value FROM ${checkpoints} WHERE namespace = $1 AND key_sha256 = $2`,
|
|
129
|
+
[namespace, keySha]
|
|
130
|
+
);
|
|
131
|
+
return result.rows[0] ? structuredClone(result.rows[0].value) : void 0;
|
|
132
|
+
},
|
|
133
|
+
async put(key, checkpoint) {
|
|
134
|
+
const keySha = checkpointKey(key);
|
|
135
|
+
const value = jsonObject(checkpoint, "reporting checkpoint");
|
|
136
|
+
const valueSha = (0, import_jcs.canonicalJsonSha256)(value);
|
|
137
|
+
const result = await query(
|
|
138
|
+
"write receipt checkpoint",
|
|
139
|
+
`INSERT INTO ${checkpoints} (namespace, key_sha256, value_sha256, value)
|
|
140
|
+
VALUES ($1,$2,$3,$4::jsonb)
|
|
141
|
+
ON CONFLICT (namespace, key_sha256) DO UPDATE SET
|
|
142
|
+
value_sha256 = ${checkpoints}.value_sha256
|
|
143
|
+
RETURNING value_sha256`,
|
|
144
|
+
[namespace, keySha, valueSha, JSON.stringify(value)]
|
|
145
|
+
);
|
|
146
|
+
if (result.rows[0]?.value_sha256 !== valueSha) {
|
|
147
|
+
throw new ReportingConsumerPersistenceConflictError(
|
|
148
|
+
"A different receipt checkpoint already owns this immutable reporting revision and destination"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
async getAdjustment(key) {
|
|
153
|
+
const keySha = adjustmentCheckpointKey(key);
|
|
154
|
+
const result = await query(
|
|
155
|
+
"read adjustment receipt checkpoint",
|
|
156
|
+
`SELECT value FROM ${checkpoints} WHERE namespace = $1 AND key_sha256 = $2`,
|
|
157
|
+
[namespace, keySha]
|
|
158
|
+
);
|
|
159
|
+
return result.rows[0] ? structuredClone(result.rows[0].value) : void 0;
|
|
160
|
+
},
|
|
161
|
+
async putAdjustment(key, checkpoint) {
|
|
162
|
+
const keySha = adjustmentCheckpointKey(key);
|
|
163
|
+
const value = jsonObject(checkpoint, "reporting adjustment checkpoint");
|
|
164
|
+
const valueSha = (0, import_jcs.canonicalJsonSha256)(value);
|
|
165
|
+
const result = await query(
|
|
166
|
+
"write adjustment receipt checkpoint",
|
|
167
|
+
`INSERT INTO ${checkpoints} (namespace, key_sha256, value_sha256, value)
|
|
168
|
+
VALUES ($1,$2,$3,$4::jsonb)
|
|
169
|
+
ON CONFLICT (namespace, key_sha256) DO UPDATE SET
|
|
170
|
+
value_sha256 = ${checkpoints}.value_sha256
|
|
171
|
+
RETURNING value_sha256`,
|
|
172
|
+
[namespace, keySha, valueSha, JSON.stringify(value)]
|
|
173
|
+
);
|
|
174
|
+
if (result.rows[0]?.value_sha256 !== valueSha) {
|
|
175
|
+
throw new ReportingConsumerPersistenceConflictError(
|
|
176
|
+
"A different adjustment checkpoint already owns this immutable reporting correction"
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const pendingConsumerStatusStore = {
|
|
182
|
+
async get(key) {
|
|
183
|
+
const keySha = pendingStatusKey(key);
|
|
184
|
+
const result = await query(
|
|
185
|
+
"read pending consumer status",
|
|
186
|
+
`SELECT value FROM ${statuses} WHERE namespace = $1 AND key_sha256 = $2`,
|
|
187
|
+
[namespace, keySha]
|
|
188
|
+
);
|
|
189
|
+
return result.rows[0] ? structuredClone(result.rows[0].value) : void 0;
|
|
190
|
+
},
|
|
191
|
+
async put(key, pending, lease) {
|
|
192
|
+
const keySha = pendingStatusKey(key);
|
|
193
|
+
const scopeKey = consumerScopeKey(key.consumerScope);
|
|
194
|
+
const accountId = accountKey(key.accountId);
|
|
195
|
+
const fence = lease ? leaseFence(lease, scopeKey, accountId) : void 0;
|
|
196
|
+
const value = jsonObject(pending, "pending consumer status");
|
|
197
|
+
const valueSha = (0, import_jcs.canonicalJsonSha256)(value);
|
|
198
|
+
const result = await query(
|
|
199
|
+
"write pending consumer status",
|
|
200
|
+
`WITH current_lease AS MATERIALIZED (
|
|
201
|
+
SELECT 1 FROM ${leases}
|
|
202
|
+
WHERE $5::text IS NOT NULL
|
|
203
|
+
AND namespace = $1 AND scope_key = $7 AND account_id = $8
|
|
204
|
+
AND owner_token = $5 AND generation = $6 AND expires_at > clock_timestamp()
|
|
205
|
+
FOR UPDATE
|
|
206
|
+
)
|
|
207
|
+
INSERT INTO ${statuses} (namespace, key_sha256, value_sha256, value)
|
|
208
|
+
SELECT $1,$2,$3,$4::jsonb
|
|
209
|
+
WHERE $5::text IS NULL OR EXISTS (SELECT 1 FROM current_lease)
|
|
210
|
+
ON CONFLICT (namespace, key_sha256) DO UPDATE SET
|
|
211
|
+
value_sha256 = EXCLUDED.value_sha256,
|
|
212
|
+
value = EXCLUDED.value,
|
|
213
|
+
changed_at = clock_timestamp()
|
|
214
|
+
RETURNING key_sha256`,
|
|
215
|
+
[
|
|
216
|
+
namespace,
|
|
217
|
+
keySha,
|
|
218
|
+
valueSha,
|
|
219
|
+
JSON.stringify(value),
|
|
220
|
+
fence?.ownerToken ?? null,
|
|
221
|
+
fence?.generation ?? null,
|
|
222
|
+
scopeKey,
|
|
223
|
+
accountId
|
|
224
|
+
]
|
|
225
|
+
);
|
|
226
|
+
if (result.rowCount !== 1) {
|
|
227
|
+
throw new ReportingConsumerPersistenceConflictError("The pending consumer status lease is no longer current");
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
async clear(key, expected, lease) {
|
|
231
|
+
const scopeKey = consumerScopeKey(key.consumerScope);
|
|
232
|
+
const accountId = accountKey(key.accountId);
|
|
233
|
+
const fence = lease ? leaseFence(lease, scopeKey, accountId) : void 0;
|
|
234
|
+
const valueSha = expected ? (0, import_jcs.canonicalJsonSha256)(jsonObject(expected, "pending consumer status")) : null;
|
|
235
|
+
await query(
|
|
236
|
+
"clear pending consumer status",
|
|
237
|
+
`WITH current_lease AS MATERIALIZED (
|
|
238
|
+
SELECT 1 FROM ${leases}
|
|
239
|
+
WHERE $4::text IS NOT NULL
|
|
240
|
+
AND namespace = $1 AND scope_key = $6 AND account_id = $7
|
|
241
|
+
AND owner_token = $4 AND generation = $5 AND expires_at > clock_timestamp()
|
|
242
|
+
FOR UPDATE
|
|
243
|
+
)
|
|
244
|
+
DELETE FROM ${statuses}
|
|
245
|
+
WHERE namespace = $1 AND key_sha256 = $2
|
|
246
|
+
AND ($3::text IS NULL OR value_sha256 = $3)
|
|
247
|
+
AND ($4::text IS NULL OR EXISTS (SELECT 1 FROM current_lease))`,
|
|
248
|
+
[
|
|
249
|
+
namespace,
|
|
250
|
+
pendingStatusKey(key),
|
|
251
|
+
valueSha,
|
|
252
|
+
fence?.ownerToken ?? null,
|
|
253
|
+
fence?.generation ?? null,
|
|
254
|
+
scopeKey,
|
|
255
|
+
accountId
|
|
256
|
+
]
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const changesCheckpointStore = {
|
|
261
|
+
async get(key) {
|
|
262
|
+
const scopeKey = consumerScopeKey(key.consumerScope);
|
|
263
|
+
const accountId = accountKey(key.accountId);
|
|
264
|
+
const result = await query(
|
|
265
|
+
"read changes checkpoint",
|
|
266
|
+
`SELECT checkpoint, generation::text AS generation FROM ${cursors}
|
|
267
|
+
WHERE namespace = $1 AND scope_key = $2 AND account_id = $3`,
|
|
268
|
+
[namespace, scopeKey, accountId]
|
|
269
|
+
);
|
|
270
|
+
const row = result.rows[0];
|
|
271
|
+
return row ? { checkpoint: row.checkpoint, generation: safeGeneration(row.generation) } : void 0;
|
|
272
|
+
},
|
|
273
|
+
async compareAndSet(key, expected, checkpoint, lease) {
|
|
274
|
+
const scopeKey = consumerScopeKey(key.consumerScope);
|
|
275
|
+
const accountId = accountKey(key.accountId);
|
|
276
|
+
const fence = lease ? leaseFence(lease, scopeKey, accountId) : void 0;
|
|
277
|
+
assertCheckpoint(checkpoint);
|
|
278
|
+
if (expected !== null) assertCheckpoint(expected);
|
|
279
|
+
const result = expected === null ? await query(
|
|
280
|
+
"initialize changes checkpoint",
|
|
281
|
+
`WITH current_lease AS MATERIALIZED (
|
|
282
|
+
SELECT 1 FROM ${leases}
|
|
283
|
+
WHERE $5::text IS NOT NULL
|
|
284
|
+
AND namespace = $1 AND scope_key = $2 AND account_id = $3
|
|
285
|
+
AND owner_token = $5 AND generation = $6 AND expires_at > clock_timestamp()
|
|
286
|
+
FOR UPDATE
|
|
287
|
+
)
|
|
288
|
+
INSERT INTO ${cursors} (namespace, scope_key, account_id, checkpoint)
|
|
289
|
+
SELECT $1,$2,$3,$4
|
|
290
|
+
WHERE $5::text IS NULL OR EXISTS (SELECT 1 FROM current_lease)
|
|
291
|
+
ON CONFLICT (namespace, scope_key, account_id) DO NOTHING
|
|
292
|
+
RETURNING generation::text AS generation`,
|
|
293
|
+
[namespace, scopeKey, accountId, checkpoint, fence?.ownerToken ?? null, fence?.generation ?? null]
|
|
294
|
+
) : await query(
|
|
295
|
+
"advance changes checkpoint",
|
|
296
|
+
`WITH current_lease AS MATERIALIZED (
|
|
297
|
+
SELECT 1 FROM ${leases}
|
|
298
|
+
WHERE $6::text IS NOT NULL
|
|
299
|
+
AND namespace = $1 AND scope_key = $2 AND account_id = $3
|
|
300
|
+
AND owner_token = $6 AND generation = $7 AND expires_at > clock_timestamp()
|
|
301
|
+
FOR UPDATE
|
|
302
|
+
)
|
|
303
|
+
UPDATE ${cursors} SET
|
|
304
|
+
checkpoint = $4,
|
|
305
|
+
generation = CASE WHEN checkpoint = $4 THEN generation ELSE generation + 1 END,
|
|
306
|
+
changed_at = CASE WHEN checkpoint = $4 THEN changed_at ELSE clock_timestamp() END
|
|
307
|
+
WHERE namespace = $1 AND scope_key = $2 AND account_id = $3 AND checkpoint = $5
|
|
308
|
+
AND ($6::text IS NULL OR EXISTS (SELECT 1 FROM current_lease))
|
|
309
|
+
RETURNING generation::text AS generation`,
|
|
310
|
+
[
|
|
311
|
+
namespace,
|
|
312
|
+
scopeKey,
|
|
313
|
+
accountId,
|
|
314
|
+
checkpoint,
|
|
315
|
+
expected,
|
|
316
|
+
fence?.ownerToken ?? null,
|
|
317
|
+
fence?.generation ?? null
|
|
318
|
+
]
|
|
319
|
+
);
|
|
320
|
+
if (result.rowCount !== 1) return "conflict";
|
|
321
|
+
return expected === checkpoint ? "unchanged" : "applied";
|
|
322
|
+
},
|
|
323
|
+
async clear(key, expected, lease) {
|
|
324
|
+
const scopeKey = consumerScopeKey(key.consumerScope);
|
|
325
|
+
const accountId = accountKey(key.accountId);
|
|
326
|
+
const fence = lease ? leaseFence(lease, scopeKey, accountId) : void 0;
|
|
327
|
+
if (expected !== void 0) assertCheckpoint(expected);
|
|
328
|
+
const result = await query(
|
|
329
|
+
"clear changes checkpoint",
|
|
330
|
+
`WITH current_lease AS MATERIALIZED (
|
|
331
|
+
SELECT 1 FROM ${leases}
|
|
332
|
+
WHERE $5::text IS NOT NULL
|
|
333
|
+
AND namespace = $1 AND scope_key = $2 AND account_id = $3
|
|
334
|
+
AND owner_token = $5 AND generation = $6 AND expires_at > clock_timestamp()
|
|
335
|
+
FOR UPDATE
|
|
336
|
+
)
|
|
337
|
+
DELETE FROM ${cursors}
|
|
338
|
+
WHERE namespace = $1 AND scope_key = $2 AND account_id = $3
|
|
339
|
+
AND ($4::text IS NULL OR checkpoint = $4)
|
|
340
|
+
AND ($5::text IS NULL OR EXISTS (SELECT 1 FROM current_lease))`,
|
|
341
|
+
[namespace, scopeKey, accountId, expected ?? null, fence?.ownerToken ?? null, fence?.generation ?? null]
|
|
342
|
+
);
|
|
343
|
+
return (result.rowCount ?? 0) > 0;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const workLeases = {
|
|
347
|
+
async claim(input) {
|
|
348
|
+
const scopeKey = consumerScopeKey(input.key.consumerScope);
|
|
349
|
+
const accountId = accountKey(input.key.accountId);
|
|
350
|
+
assertOwner(input.ownerToken);
|
|
351
|
+
assertLeaseMilliseconds(input.leaseMilliseconds);
|
|
352
|
+
const result = await query(
|
|
353
|
+
"claim consumer work lease",
|
|
354
|
+
`INSERT INTO ${leases} (namespace, scope_key, account_id, owner_token, generation, expires_at)
|
|
355
|
+
VALUES ($1,$2,$3,$4,1,clock_timestamp() + ($5::bigint * INTERVAL '1 millisecond'))
|
|
356
|
+
ON CONFLICT (namespace, scope_key, account_id) DO UPDATE SET
|
|
357
|
+
owner_token = EXCLUDED.owner_token,
|
|
358
|
+
generation = ${leases}.generation + 1,
|
|
359
|
+
expires_at = clock_timestamp() + ($5::bigint * INTERVAL '1 millisecond'),
|
|
360
|
+
changed_at = clock_timestamp()
|
|
361
|
+
WHERE ${leases}.expires_at <= clock_timestamp()
|
|
362
|
+
RETURNING generation::text AS generation, expires_at`,
|
|
363
|
+
[namespace, scopeKey, accountId, input.ownerToken, input.leaseMilliseconds]
|
|
364
|
+
);
|
|
365
|
+
const row = result.rows[0];
|
|
366
|
+
return row ? {
|
|
367
|
+
scopeKey: `${scopeKey}:${accountId}`,
|
|
368
|
+
ownerToken: input.ownerToken,
|
|
369
|
+
generation: safeGeneration(row.generation),
|
|
370
|
+
expiresAt: row.expires_at.toISOString()
|
|
371
|
+
} : null;
|
|
372
|
+
},
|
|
373
|
+
async renew(lease, leaseMilliseconds) {
|
|
374
|
+
const { scopeKey, accountId } = splitLeaseScope(lease.scopeKey);
|
|
375
|
+
assertOwner(lease.ownerToken);
|
|
376
|
+
safeGeneration(lease.generation);
|
|
377
|
+
assertLeaseMilliseconds(leaseMilliseconds);
|
|
378
|
+
const result = await query(
|
|
379
|
+
"renew consumer work lease",
|
|
380
|
+
`UPDATE ${leases}
|
|
381
|
+
SET expires_at = clock_timestamp() + ($6::bigint * INTERVAL '1 millisecond'),
|
|
382
|
+
changed_at = clock_timestamp()
|
|
383
|
+
WHERE namespace = $1 AND scope_key = $2 AND account_id = $3
|
|
384
|
+
AND owner_token = $4 AND generation = $5 AND expires_at > clock_timestamp()
|
|
385
|
+
RETURNING expires_at`,
|
|
386
|
+
[namespace, scopeKey, accountId, lease.ownerToken, lease.generation, leaseMilliseconds]
|
|
387
|
+
);
|
|
388
|
+
return result.rows[0] ? { ...lease, expiresAt: result.rows[0].expires_at.toISOString() } : null;
|
|
389
|
+
},
|
|
390
|
+
async release(lease) {
|
|
391
|
+
const { scopeKey, accountId } = splitLeaseScope(lease.scopeKey);
|
|
392
|
+
assertOwner(lease.ownerToken);
|
|
393
|
+
safeGeneration(lease.generation);
|
|
394
|
+
const result = await query(
|
|
395
|
+
"release consumer work lease",
|
|
396
|
+
`UPDATE ${leases}
|
|
397
|
+
SET expires_at = clock_timestamp(), changed_at = clock_timestamp()
|
|
398
|
+
WHERE namespace = $1 AND scope_key = $2 AND account_id = $3
|
|
399
|
+
AND owner_token = $4 AND generation = $5
|
|
400
|
+
AND expires_at > clock_timestamp()`,
|
|
401
|
+
[namespace, scopeKey, accountId, lease.ownerToken, lease.generation]
|
|
402
|
+
);
|
|
403
|
+
return result.rowCount === 1;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const notificationStore = {
|
|
407
|
+
async isProcessed(input) {
|
|
408
|
+
const values = notificationIdentity(input);
|
|
409
|
+
const result = await query(
|
|
410
|
+
"read processed notification",
|
|
411
|
+
`SELECT payload_sha256 FROM ${notifications}
|
|
412
|
+
WHERE namespace = $1 AND scope_key = $2 AND account_id = $3 AND idempotency_key = $4`,
|
|
413
|
+
[namespace, values.scopeKey, values.accountId, values.idempotencyKey]
|
|
414
|
+
);
|
|
415
|
+
const existing = result.rows[0]?.payload_sha256;
|
|
416
|
+
if (existing !== void 0 && existing !== values.payloadSha256) {
|
|
417
|
+
throw new ReportingConsumerPersistenceConflictError(
|
|
418
|
+
"Reporting notification idempotency identity names different content"
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return existing !== void 0;
|
|
422
|
+
},
|
|
423
|
+
async markProcessed(input) {
|
|
424
|
+
const values = notificationIdentity(input);
|
|
425
|
+
const result = await query(
|
|
426
|
+
"mark processed notification",
|
|
427
|
+
`INSERT INTO ${notifications}
|
|
428
|
+
(namespace, scope_key, account_id, idempotency_key, payload_sha256)
|
|
429
|
+
VALUES ($1,$2,$3,$4,$5)
|
|
430
|
+
ON CONFLICT (namespace, scope_key, account_id, idempotency_key) DO UPDATE SET
|
|
431
|
+
payload_sha256 = ${notifications}.payload_sha256
|
|
432
|
+
RETURNING payload_sha256`,
|
|
433
|
+
[namespace, values.scopeKey, values.accountId, values.idempotencyKey, values.payloadSha256]
|
|
434
|
+
);
|
|
435
|
+
if (result.rows[0]?.payload_sha256 !== values.payloadSha256) {
|
|
436
|
+
throw new ReportingConsumerPersistenceConflictError(
|
|
437
|
+
"Reporting notification idempotency identity names different content"
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
async pruneProcessed(pruneOptions = {}) {
|
|
442
|
+
const retentionDays = pruneOptions.retentionDays ?? 90;
|
|
443
|
+
const limit = pruneOptions.limit ?? 1e3;
|
|
444
|
+
if (!Number.isSafeInteger(retentionDays) || retentionDays < 30 || retentionDays > 3650) {
|
|
445
|
+
throw new TypeError("retentionDays must be an integer from 30 through 3650");
|
|
446
|
+
}
|
|
447
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
|
|
448
|
+
throw new TypeError("limit must be an integer from 1 through 10000");
|
|
449
|
+
}
|
|
450
|
+
const result = await query(
|
|
451
|
+
"prune processed notifications",
|
|
452
|
+
`WITH expired AS (
|
|
453
|
+
SELECT ctid FROM ${notifications}
|
|
454
|
+
WHERE namespace = $1
|
|
455
|
+
AND processed_at < clock_timestamp() - ($2::integer * INTERVAL '1 day')
|
|
456
|
+
ORDER BY processed_at FOR UPDATE SKIP LOCKED LIMIT $3
|
|
457
|
+
)
|
|
458
|
+
DELETE FROM ${notifications} target USING expired WHERE target.ctid = expired.ctid`,
|
|
459
|
+
[namespace, retentionDays, limit]
|
|
460
|
+
);
|
|
461
|
+
return result.rowCount ?? 0;
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
async function query(operation, text, values) {
|
|
465
|
+
try {
|
|
466
|
+
return await options.db.query(text, values);
|
|
467
|
+
} catch (cause) {
|
|
468
|
+
throw new Error(`PostgreSQL reporting consumer ${operation} failed`, { cause });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const migration = getReportingConsumerPostgresMigration({ tablePrefix: prefix });
|
|
472
|
+
return {
|
|
473
|
+
checkpointStore,
|
|
474
|
+
pendingConsumerStatusStore,
|
|
475
|
+
changesCheckpointStore,
|
|
476
|
+
workLeases,
|
|
477
|
+
notifications: notificationStore,
|
|
478
|
+
migrations: { persistence: migration, all: [migration] },
|
|
479
|
+
async probe() {
|
|
480
|
+
await query("persistence probe", `SELECT namespace, key_sha256, value_sha256, value FROM ${checkpoints} LIMIT 0`);
|
|
481
|
+
await query("status probe", `SELECT namespace, key_sha256, value_sha256, value FROM ${statuses} LIMIT 0`);
|
|
482
|
+
await query(
|
|
483
|
+
"cursor probe",
|
|
484
|
+
`SELECT namespace, scope_key, account_id, checkpoint, generation FROM ${cursors} LIMIT 0`
|
|
485
|
+
);
|
|
486
|
+
await query(
|
|
487
|
+
"lease probe",
|
|
488
|
+
`SELECT namespace, scope_key, account_id, owner_token, generation FROM ${leases} LIMIT 0`
|
|
489
|
+
);
|
|
490
|
+
await query(
|
|
491
|
+
"notification probe",
|
|
492
|
+
`SELECT namespace, scope_key, account_id, idempotency_key, payload_sha256 FROM ${notifications} LIMIT 0`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function notificationIdentity(input) {
|
|
498
|
+
const scopeKey = consumerScopeKey(input.consumerScope);
|
|
499
|
+
const accountId = accountKey(input.accountId);
|
|
500
|
+
assertBoundedString(input.idempotencyKey, "notification idempotencyKey", 255);
|
|
501
|
+
if (input.idempotencyKey.length < 16 || !/^[A-Za-z0-9_.:-]+$/.test(input.idempotencyKey)) {
|
|
502
|
+
throw new TypeError("notification idempotencyKey is invalid");
|
|
503
|
+
}
|
|
504
|
+
if (!/^[a-f0-9]{64}$/.test(input.payloadSha256)) {
|
|
505
|
+
throw new TypeError("notification payloadSha256 is invalid");
|
|
506
|
+
}
|
|
507
|
+
return { scopeKey, accountId, idempotencyKey: input.idempotencyKey, payloadSha256: input.payloadSha256 };
|
|
508
|
+
}
|
|
509
|
+
function checkpointKey(key) {
|
|
510
|
+
assertBoundedString(key.consumerScope, "consumerScope", 4096);
|
|
511
|
+
assertBoundedString(key.accountId, "accountId", 512);
|
|
512
|
+
assertBoundedString(key.reportingObligationId, "reportingObligationId", 512);
|
|
513
|
+
assertBoundedString(key.reportingRevisionId, "reportingRevisionId", 512);
|
|
514
|
+
assertBoundedString(key.reportingMaterializationId, "reportingMaterializationId", 512);
|
|
515
|
+
assertBoundedString(key.destinationRef, "destinationRef", 2048);
|
|
516
|
+
if (key.contextFingerprint !== void 0) {
|
|
517
|
+
assertBoundedString(key.contextFingerprint, "contextFingerprint", 128);
|
|
518
|
+
}
|
|
519
|
+
return (0, import_jcs.canonicalJsonSha256)(key);
|
|
520
|
+
}
|
|
521
|
+
function adjustmentCheckpointKey(key) {
|
|
522
|
+
assertBoundedString(key.consumerScope, "consumerScope", 4096);
|
|
523
|
+
assertBoundedString(key.accountId, "accountId", 512);
|
|
524
|
+
assertBoundedString(key.reportingAdjustmentId, "reportingAdjustmentId", 512);
|
|
525
|
+
assertBoundedString(key.adjustsReportingRevisionId, "adjustsReportingRevisionId", 512);
|
|
526
|
+
if (key.supersedesReportingReceiptId !== void 0) {
|
|
527
|
+
assertBoundedString(key.supersedesReportingReceiptId, "supersedesReportingReceiptId", 512);
|
|
528
|
+
}
|
|
529
|
+
if (key.contextFingerprint !== void 0) {
|
|
530
|
+
assertBoundedString(key.contextFingerprint, "contextFingerprint", 128);
|
|
531
|
+
}
|
|
532
|
+
return (0, import_jcs.canonicalJsonSha256)(key);
|
|
533
|
+
}
|
|
534
|
+
function pendingStatusKey(key) {
|
|
535
|
+
assertBoundedString(key.consumerScope, "consumerScope", 4096);
|
|
536
|
+
assertBoundedString(key.accountId, "accountId", 512);
|
|
537
|
+
assertBoundedString(key.deliveryConfigId, "deliveryConfigId", 512);
|
|
538
|
+
safeGeneration(key.deliveryConfigVersion);
|
|
539
|
+
assertBoundedString(key.reportDefinitionId, "reportDefinitionId", 512);
|
|
540
|
+
assertBoundedString(key.periodStart, "periodStart", 128);
|
|
541
|
+
assertBoundedString(key.periodEnd, "periodEnd", 128);
|
|
542
|
+
return (0, import_jcs.canonicalJsonSha256)(key);
|
|
543
|
+
}
|
|
544
|
+
function consumerScopeKey(scope) {
|
|
545
|
+
assertBoundedString(scope, "consumerScope", 4096);
|
|
546
|
+
return (0, import_jcs.canonicalJsonSha256)({ scope });
|
|
547
|
+
}
|
|
548
|
+
function accountKey(accountId) {
|
|
549
|
+
assertBoundedString(accountId, "accountId", 512);
|
|
550
|
+
return accountId;
|
|
551
|
+
}
|
|
552
|
+
function jsonObject(value, name) {
|
|
553
|
+
let encoded;
|
|
554
|
+
try {
|
|
555
|
+
encoded = JSON.stringify(value);
|
|
556
|
+
} catch {
|
|
557
|
+
throw new TypeError(`${name} must be JSON serializable`);
|
|
558
|
+
}
|
|
559
|
+
if (encoded === void 0 || Buffer.byteLength(encoded, "utf8") > MAX_JSON_BYTES) {
|
|
560
|
+
throw new RangeError(`${name} exceeds ${MAX_JSON_BYTES} bytes`);
|
|
561
|
+
}
|
|
562
|
+
const parsed = JSON.parse(encoded);
|
|
563
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
564
|
+
throw new TypeError(`${name} must be a JSON object`);
|
|
565
|
+
}
|
|
566
|
+
return parsed;
|
|
567
|
+
}
|
|
568
|
+
function assertCheckpoint(value) {
|
|
569
|
+
assertBoundedString(value, "changes checkpoint", MAX_CHECKPOINT_BYTES);
|
|
570
|
+
}
|
|
571
|
+
function assertOwner(value) {
|
|
572
|
+
assertBoundedString(value, "ownerToken", 255);
|
|
573
|
+
if (value.length < 8) throw new TypeError("ownerToken must contain at least 8 characters");
|
|
574
|
+
}
|
|
575
|
+
function assertLeaseMilliseconds(value) {
|
|
576
|
+
if (!Number.isSafeInteger(value) || value < 1e3 || value > 3e5) {
|
|
577
|
+
throw new TypeError("leaseMilliseconds must be an integer from 1000 through 300000");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function safeGeneration(value) {
|
|
581
|
+
const generation = typeof value === "number" ? value : Number(value);
|
|
582
|
+
if (!Number.isSafeInteger(generation) || generation < 1) throw new Error("Reporting consumer generation is invalid");
|
|
583
|
+
return generation;
|
|
584
|
+
}
|
|
585
|
+
function splitLeaseScope(value) {
|
|
586
|
+
const separator = value.indexOf(":");
|
|
587
|
+
const scopeKey = value.slice(0, separator);
|
|
588
|
+
const accountId = value.slice(separator + 1);
|
|
589
|
+
if (separator !== 64 || !/^[a-f0-9]{64}$/.test(scopeKey) || accountId.length === 0) {
|
|
590
|
+
throw new TypeError("Reporting consumer work lease scope is invalid");
|
|
591
|
+
}
|
|
592
|
+
accountKey(accountId);
|
|
593
|
+
return { scopeKey, accountId };
|
|
594
|
+
}
|
|
595
|
+
function leaseFence(lease, expectedScopeKey, expectedAccountId) {
|
|
596
|
+
const { scopeKey, accountId } = splitLeaseScope(lease.scopeKey);
|
|
597
|
+
assertOwner(lease.ownerToken);
|
|
598
|
+
const generation = safeGeneration(lease.generation);
|
|
599
|
+
if (scopeKey !== expectedScopeKey || accountId !== expectedAccountId) {
|
|
600
|
+
throw new TypeError("Reporting consumer work lease does not match the checkpoint partition");
|
|
601
|
+
}
|
|
602
|
+
return { ownerToken: lease.ownerToken, generation };
|
|
603
|
+
}
|
|
604
|
+
function tablePrefix(value) {
|
|
605
|
+
const prefix = value ?? DEFAULT_PREFIX;
|
|
606
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || Buffer.byteLength(prefix, "utf8") > 32) {
|
|
607
|
+
throw new TypeError("tablePrefix must be a lowercase PostgreSQL identifier of at most 32 bytes");
|
|
608
|
+
}
|
|
609
|
+
return prefix;
|
|
610
|
+
}
|
|
611
|
+
function quoteIdentifier(value) {
|
|
612
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(value) || Buffer.byteLength(value, "utf8") > 63) {
|
|
613
|
+
throw new TypeError("Derived reporting consumer table name is invalid");
|
|
614
|
+
}
|
|
615
|
+
return `"${value}"`;
|
|
616
|
+
}
|
|
617
|
+
function assertBoundedString(value, name, maxBytes) {
|
|
618
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0") || Buffer.byteLength(value) > maxBytes) {
|
|
619
|
+
throw new TypeError(`${name} must be a non-empty UTF-8 string of at most ${maxBytes} bytes without NUL`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
623
|
+
0 && (module.exports = {
|
|
624
|
+
REPORTING_CONSUMER_POSTGRES_MIGRATION,
|
|
625
|
+
ReportingConsumerPersistenceConflictError,
|
|
626
|
+
createPostgresReportingConsumerRuntimeV1,
|
|
627
|
+
getReportingConsumerPostgresMigration
|
|
628
|
+
});
|