@adcp/sdk 15.4.0 → 15.6.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/README.md +15 -0
- package/dist/lib/advanced.d.mts +2 -2
- package/dist/lib/advanced.d.ts +2 -2
- package/dist/lib/advanced.js +8 -2
- package/dist/lib/advanced.mjs +8 -2
- package/dist/lib/auth/oauth/index.d.mts +1 -0
- package/dist/lib/auth/oauth/index.d.ts +1 -0
- package/dist/lib/auth/oauth/index.js +5 -0
- package/dist/lib/auth/oauth/index.mjs +3 -0
- package/dist/lib/auth/oauth/pending-web-flow.d.mts +16 -0
- package/dist/lib/auth/oauth/pending-web-flow.d.ts +16 -0
- package/dist/lib/auth/oauth/pending-web-flow.js +66 -0
- package/dist/lib/auth/oauth/pending-web-flow.mjs +41 -0
- package/dist/lib/auth/oauth/web-flow.js +3 -3
- package/dist/lib/auth/oauth/web-flow.mjs +3 -3
- package/dist/lib/core/AgentClient.d.mts +4 -0
- package/dist/lib/core/AgentClient.d.ts +4 -0
- package/dist/lib/core/AgentClient.js +8 -0
- package/dist/lib/core/AgentClient.mjs +8 -0
- package/dist/lib/core/SingleAgentClient.d.mts +154 -0
- package/dist/lib/core/SingleAgentClient.d.ts +154 -0
- package/dist/lib/core/SingleAgentClient.js +661 -23
- package/dist/lib/core/SingleAgentClient.mjs +666 -23
- package/dist/lib/core/agent-discovery-cache.d.mts +103 -0
- package/dist/lib/core/agent-discovery-cache.d.ts +103 -0
- package/dist/lib/core/agent-discovery-cache.js +121 -0
- package/dist/lib/core/agent-discovery-cache.mjs +92 -0
- package/dist/lib/index.d.mts +5 -3
- package/dist/lib/index.d.ts +5 -3
- package/dist/lib/index.js +7 -0
- package/dist/lib/index.mjs +6 -0
- package/dist/lib/protocols/index.d.mts +2 -2
- package/dist/lib/protocols/index.d.ts +2 -2
- package/dist/lib/protocols/index.js +1 -1
- package/dist/lib/protocols/index.mjs +1 -1
- package/dist/lib/protocols/mcp-call-context.d.mts +109 -0
- package/dist/lib/protocols/mcp-call-context.d.ts +109 -0
- package/dist/lib/protocols/mcp-call-context.js +221 -0
- package/dist/lib/protocols/mcp-call-context.mjs +186 -0
- package/dist/lib/protocols/mcp-modern.d.mts +9 -1
- package/dist/lib/protocols/mcp-modern.d.ts +9 -1
- package/dist/lib/protocols/mcp-modern.js +180 -104
- package/dist/lib/protocols/mcp-modern.mjs +193 -104
- package/dist/lib/protocols/mcp-scope.d.mts +15 -0
- package/dist/lib/protocols/mcp-scope.d.ts +15 -0
- package/dist/lib/protocols/mcp.d.mts +4 -2
- package/dist/lib/protocols/mcp.d.ts +4 -2
- package/dist/lib/protocols/mcp.js +178 -154
- package/dist/lib/protocols/mcp.mjs +189 -153
- package/dist/lib/protocols/transportDiagnostics.d.mts +33 -1
- package/dist/lib/protocols/transportDiagnostics.d.ts +33 -1
- package/dist/lib/protocols/transportDiagnostics.js +128 -12
- package/dist/lib/protocols/transportDiagnostics.mjs +128 -12
- package/dist/lib/registry/types.generated.d.mts +63 -1
- package/dist/lib/registry/types.generated.d.ts +63 -1
- package/dist/lib/reporting/azure/index.d.mts +26 -0
- package/dist/lib/reporting/azure/index.d.ts +26 -0
- package/dist/lib/reporting/azure/index.js +247 -0
- package/dist/lib/reporting/azure/index.mjs +230 -0
- package/dist/lib/reporting/bigquery/index.d.mts +127 -0
- package/dist/lib/reporting/bigquery/index.d.ts +127 -0
- package/dist/lib/reporting/bigquery/index.js +379 -0
- package/dist/lib/reporting/bigquery/index.mjs +341 -0
- package/dist/lib/reporting/gcs/index.d.mts +2 -0
- package/dist/lib/reporting/gcs/index.d.ts +2 -0
- package/dist/lib/reporting/gcs/index.js +3 -0
- package/dist/lib/reporting/gcs/index.mjs +2 -0
- package/dist/lib/reporting/gcs/row-provider.d.mts +32 -0
- package/dist/lib/reporting/gcs/row-provider.d.ts +32 -0
- package/dist/lib/reporting/gcs/row-provider.js +205 -0
- package/dist/lib/reporting/gcs/row-provider.mjs +188 -0
- package/dist/lib/reporting/ledger/change-feed.d.mts +41 -0
- package/dist/lib/reporting/ledger/change-feed.d.ts +41 -0
- package/dist/lib/reporting/ledger/change-feed.js +101 -0
- package/dist/lib/reporting/ledger/change-feed.mjs +74 -0
- package/dist/lib/reporting/ledger/handler.js +22 -8
- package/dist/lib/reporting/ledger/handler.mjs +22 -8
- package/dist/lib/reporting/ledger/index.d.mts +4 -0
- package/dist/lib/reporting/ledger/index.d.ts +4 -0
- package/dist/lib/reporting/ledger/index.js +9 -1
- package/dist/lib/reporting/ledger/index.mjs +4 -0
- package/dist/lib/reporting/ledger/managed-postgres.d.mts +2 -0
- package/dist/lib/reporting/ledger/managed-postgres.d.ts +2 -0
- package/dist/lib/reporting/ledger/managed-postgres.js +21 -1
- package/dist/lib/reporting/ledger/managed-postgres.mjs +21 -1
- package/dist/lib/reporting/ledger/managed.d.mts +9 -0
- package/dist/lib/reporting/ledger/managed.d.ts +9 -0
- package/dist/lib/reporting/ledger/managed.js +18 -0
- package/dist/lib/reporting/ledger/managed.mjs +18 -0
- package/dist/lib/reporting/ledger/postgres.d.mts +185 -0
- package/dist/lib/reporting/ledger/postgres.d.ts +185 -0
- package/dist/lib/reporting/ledger/postgres.js +872 -26
- package/dist/lib/reporting/ledger/postgres.mjs +880 -26
- package/dist/lib/reporting/ledger/producer.js +10 -1
- package/dist/lib/reporting/ledger/producer.mjs +10 -1
- package/dist/lib/reporting/ledger/row-storage-errors.d.mts +7 -0
- package/dist/lib/reporting/ledger/row-storage-errors.d.ts +7 -0
- package/dist/lib/reporting/ledger/row-storage-errors.js +41 -0
- package/dist/lib/reporting/ledger/row-storage-errors.mjs +16 -0
- package/dist/lib/reporting/ledger/row-storage-migration.d.mts +41 -0
- package/dist/lib/reporting/ledger/row-storage-migration.d.ts +41 -0
- package/dist/lib/reporting/ledger/row-storage-migration.js +234 -0
- package/dist/lib/reporting/ledger/row-storage-migration.mjs +208 -0
- package/dist/lib/reporting/ledger/row-storage-object.d.mts +112 -0
- package/dist/lib/reporting/ledger/row-storage-object.d.ts +112 -0
- package/dist/lib/reporting/ledger/row-storage-object.js +304 -0
- package/dist/lib/reporting/ledger/row-storage-object.mjs +262 -0
- package/dist/lib/reporting/ledger/row-storage-provider-io.d.mts +44 -0
- package/dist/lib/reporting/ledger/row-storage-provider-io.d.ts +44 -0
- package/dist/lib/reporting/ledger/row-storage-provider-io.js +142 -0
- package/dist/lib/reporting/ledger/row-storage-provider-io.mjs +111 -0
- package/dist/lib/reporting/ledger/row-storage.d.mts +224 -0
- package/dist/lib/reporting/ledger/row-storage.d.ts +224 -0
- package/dist/lib/reporting/ledger/row-storage.js +692 -0
- package/dist/lib/reporting/ledger/row-storage.mjs +680 -0
- package/dist/lib/reporting/ledger/types.d.mts +8 -0
- package/dist/lib/reporting/ledger/types.d.ts +8 -0
- package/dist/lib/reporting/s3/index.d.mts +29 -0
- package/dist/lib/reporting/s3/index.d.ts +29 -0
- package/dist/lib/reporting/s3/index.js +324 -0
- package/dist/lib/reporting/s3/index.mjs +317 -0
- package/dist/lib/reporting/service/index.d.mts +43 -1
- package/dist/lib/reporting/service/index.d.ts +43 -1
- package/dist/lib/reporting/service/index.js +69 -1
- package/dist/lib/reporting/service/index.mjs +71 -1
- package/dist/lib/schemas/browser.d.mts +14 -0
- package/dist/lib/schemas/browser.d.ts +14 -0
- package/dist/lib/schemas/browser.js +48 -0
- package/dist/lib/schemas/browser.mjs +20 -0
- package/dist/lib/schemas/index.d.mts +2 -120
- package/dist/lib/schemas/index.d.ts +2 -120
- package/dist/lib/schemas/index.js +3 -11
- package/dist/lib/schemas/index.mjs +1 -9
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/testing/index.d.mts +1 -0
- package/dist/lib/testing/index.d.ts +1 -0
- package/dist/lib/testing/index.js +3 -0
- package/dist/lib/testing/index.mjs +2 -0
- package/dist/lib/testing/pending-web-flow-store.d.mts +20 -0
- package/dist/lib/testing/pending-web-flow-store.d.ts +20 -0
- package/dist/lib/testing/pending-web-flow-store.js +132 -0
- package/dist/lib/testing/pending-web-flow-store.mjs +98 -0
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/validation/get-products-request.d.mts +122 -0
- package/dist/lib/validation/get-products-request.d.ts +122 -0
- package/dist/lib/validation/get-products-request.js +39 -0
- package/dist/lib/validation/get-products-request.mjs +14 -0
- 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/README.md +1 -1
- package/docs/TYPE-SUMMARY.md +35 -2
- package/docs/ZOD-SCHEMAS.md +13 -1
- package/docs/guides/DISCOVERY-CACHE-AND-SESSION-REUSE.md +142 -0
- package/docs/guides/EXISTING-PLATFORM.md +43 -12
- package/docs/guides/OAUTH-ISSUER-BINDING.md +6 -0
- package/docs/guides/REPORTING-LEDGER.md +374 -0
- package/docs/guides/REPORTING-OPERATIONS.md +76 -1
- package/docs/guides/WEB-OAUTH.md +48 -1
- package/docs/llms.txt +4 -2
- package/docs/migration-14.x-rc-worksheet.md +6 -4
- package/package.json +64 -1
|
@@ -24,6 +24,7 @@ __export(postgres_exports, {
|
|
|
24
24
|
REPORTING_LEDGER_MIGRATION: () => REPORTING_LEDGER_MIGRATION,
|
|
25
25
|
ReportingLedgerContinuityError: () => ReportingLedgerContinuityError,
|
|
26
26
|
ReportingLedgerLeaseLostError: () => ReportingLedgerLeaseLostError,
|
|
27
|
+
ReportingLedgerPeriodRetiredError: () => ReportingLedgerPeriodRetiredError,
|
|
27
28
|
sweepExpiredReportingLedgerState: () => sweepExpiredReportingLedgerState
|
|
28
29
|
});
|
|
29
30
|
module.exports = __toCommonJS(postgres_exports);
|
|
@@ -39,6 +40,8 @@ var import_types = require('./types.js');
|
|
|
39
40
|
var import_types2 = require('./types.js');
|
|
40
41
|
var import_handler = require('./handler.js');
|
|
41
42
|
var import_producer = require('./producer.js');
|
|
43
|
+
var import_change_feed = require('./change-feed.js');
|
|
44
|
+
var import_row_storage = require('./row-storage.js');
|
|
42
45
|
var import_schedule = require('./schedule.js');
|
|
43
46
|
var import_consumer_status_identity = require('./consumer-status-identity.js');
|
|
44
47
|
const REPORTING_LEDGER_MIGRATION = `
|
|
@@ -287,6 +290,29 @@ CREATE TABLE IF NOT EXISTS adcp_reporting_checkpoints (
|
|
|
287
290
|
);
|
|
288
291
|
CREATE INDEX IF NOT EXISTS adcp_reporting_checkpoints_expiry
|
|
289
292
|
ON adcp_reporting_checkpoints (expires_at);
|
|
293
|
+
|
|
294
|
+
-- A retired period keeps one compact record: it refuses replays that would
|
|
295
|
+
-- resurrect the period and preserves the audit conclusion after the period's
|
|
296
|
+
-- ledger records are deleted.
|
|
297
|
+
CREATE TABLE IF NOT EXISTS adcp_reporting_obligation_tombstones (
|
|
298
|
+
obligation_id TEXT PRIMARY KEY,
|
|
299
|
+
account_id TEXT NOT NULL,
|
|
300
|
+
configuration_id TEXT NOT NULL,
|
|
301
|
+
period_ordinal BIGINT NOT NULL,
|
|
302
|
+
period_start TIMESTAMPTZ NOT NULL,
|
|
303
|
+
period_end TIMESTAMPTZ NOT NULL,
|
|
304
|
+
state TEXT NOT NULL CHECK (state IN ('retiring', 'retired')),
|
|
305
|
+
final_revision_id TEXT,
|
|
306
|
+
final_content_sha256 TEXT,
|
|
307
|
+
revision_count INTEGER NOT NULL CHECK (revision_count >= 0),
|
|
308
|
+
adjustment_count INTEGER NOT NULL CHECK (adjustment_count >= 0),
|
|
309
|
+
retiring_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
310
|
+
retired_at TIMESTAMPTZ,
|
|
311
|
+
UNIQUE (configuration_id, period_ordinal),
|
|
312
|
+
CHECK ((state = 'retired') = (retired_at IS NOT NULL))
|
|
313
|
+
);
|
|
314
|
+
CREATE INDEX IF NOT EXISTS adcp_reporting_obligation_tombstones_account
|
|
315
|
+
ON adcp_reporting_obligation_tombstones (account_id, configuration_id, period_ordinal);
|
|
290
316
|
`.trim();
|
|
291
317
|
const REPORTING_LEDGER_FINALITY_FENCE_CONSTRAINT = "adcp_reporting_transitions_finality_recorded";
|
|
292
318
|
const REPORTING_LEDGER_FINALITY_WRITER_FENCE_MIGRATION = `
|
|
@@ -319,6 +345,12 @@ class ReportingLedgerLeaseLostError extends Error {
|
|
|
319
345
|
this.name = "ReportingLedgerLeaseLostError";
|
|
320
346
|
}
|
|
321
347
|
}
|
|
348
|
+
class ReportingLedgerPeriodRetiredError extends Error {
|
|
349
|
+
constructor(message = "Reporting obligation period was retired by retention") {
|
|
350
|
+
super(message);
|
|
351
|
+
this.name = "ReportingLedgerPeriodRetiredError";
|
|
352
|
+
}
|
|
353
|
+
}
|
|
322
354
|
class ReportingLedgerContinuityError extends Error {
|
|
323
355
|
constructor(message = "Reporting ledger revision continuity invariant failed") {
|
|
324
356
|
super(message);
|
|
@@ -371,6 +403,9 @@ class PostgresReportingLedgerStore {
|
|
|
371
403
|
this.consumerMismatchEscalation = (0, import_health.assertReportingConsumerMismatchEscalation)(options.consumerMismatchEscalation);
|
|
372
404
|
this.notificationActivityPort = options.notificationActivityPort;
|
|
373
405
|
this.transactionalNotificationActivity = options.notificationActivityPort !== void 0;
|
|
406
|
+
this.rowStorage = new import_row_storage.ReportingRowStorageV1(pool, typeof options.rowStorage === "object" ? options.rowStorage : {});
|
|
407
|
+
this.rowStorageWrites = options.rowStorage !== void 0 && options.rowStorage !== false;
|
|
408
|
+
this.changeFeed = options.changeFeed === true;
|
|
374
409
|
}
|
|
375
410
|
pool;
|
|
376
411
|
/**
|
|
@@ -386,6 +421,119 @@ class PostgresReportingLedgerStore {
|
|
|
386
421
|
obligatedConsumers;
|
|
387
422
|
transactionalNotificationActivity;
|
|
388
423
|
notificationActivityPort;
|
|
424
|
+
rowStorage;
|
|
425
|
+
rowStorageWrites;
|
|
426
|
+
changeFeed;
|
|
427
|
+
/** Append one host change-feed row inside the caller's account-locked transaction. */
|
|
428
|
+
async appendChange(client, kind, accountId, recordId, obligationId) {
|
|
429
|
+
if (!this.changeFeed) return;
|
|
430
|
+
await client.query(
|
|
431
|
+
`INSERT INTO adcp_reporting_changes (account_id, record_kind, record_id, obligation_id) VALUES ($1, $2, $3, $4)`,
|
|
432
|
+
[accountId, kind, recordId, obligationId]
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
/** Probe and register row storage when enabled. Safe to call repeatedly. */
|
|
436
|
+
async readyRowStorage() {
|
|
437
|
+
if (this.rowStorageWrites) await this.rowStorage.ready();
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Move legacy inline revision and adjustment rows into row storage, a
|
|
441
|
+
* bounded batch at a time. Each row set is re-encoded, proven to reproduce
|
|
442
|
+
* its committed binding, written, and only then removed from the ledger
|
|
443
|
+
* document, in one transaction under the account lock. A document whose
|
|
444
|
+
* rows do not reproduce its binding is never rewritten: it is reported in
|
|
445
|
+
* `quarantined` and left inline. Pass the returned `cursor` back to resume
|
|
446
|
+
* after the last examined document; it is absent once every candidate has
|
|
447
|
+
* been examined. Reclaiming the freed space needs `VACUUM FULL` or an online
|
|
448
|
+
* repack.
|
|
449
|
+
*/
|
|
450
|
+
async migrateInlineRows(input = {}) {
|
|
451
|
+
if (!this.rowStorageWrites) throw new Error("migrateInlineRows requires rowStorage");
|
|
452
|
+
const limit = input.limit ?? 100;
|
|
453
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
|
|
454
|
+
throw new RangeError("migrateInlineRows limit must be between 1 and 10000");
|
|
455
|
+
}
|
|
456
|
+
let after;
|
|
457
|
+
if (input.cursor !== void 0) {
|
|
458
|
+
try {
|
|
459
|
+
const parsed = JSON.parse(Buffer.from(input.cursor, "base64url").toString("utf8"));
|
|
460
|
+
if (typeof parsed.at !== "string" || typeof parsed.id !== "string") throw new Error("invalid");
|
|
461
|
+
after = { at: parsed.at, id: parsed.id };
|
|
462
|
+
} catch {
|
|
463
|
+
throw new RangeError("migrateInlineRows cursor is invalid");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
await this.rowStorage.ready();
|
|
467
|
+
const candidates = await this.query(
|
|
468
|
+
`SELECT kind, id, account_id, to_char(recorded_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS at
|
|
469
|
+
FROM (
|
|
470
|
+
SELECT 'revision' AS kind, revision.revision_id AS id, obligation.account_id, revision.recorded_at
|
|
471
|
+
FROM adcp_reporting_revisions revision
|
|
472
|
+
JOIN adcp_reporting_obligations obligation ON obligation.obligation_id = revision.obligation_id
|
|
473
|
+
WHERE revision.data ? 'rows' AND ($2::text IS NULL OR obligation.account_id = $2)
|
|
474
|
+
UNION ALL
|
|
475
|
+
SELECT 'adjustment', adjustment.adjustment_id, obligation.account_id, adjustment.recorded_at
|
|
476
|
+
FROM adcp_reporting_adjustments adjustment
|
|
477
|
+
JOIN adcp_reporting_obligations obligation ON obligation.obligation_id = adjustment.obligation_id
|
|
478
|
+
WHERE adjustment.data ? 'rows' AND ($2::text IS NULL OR obligation.account_id = $2)
|
|
479
|
+
) inline_rows
|
|
480
|
+
WHERE $3::timestamptz IS NULL OR (recorded_at, id) > ($3::timestamptz, $4::text)
|
|
481
|
+
ORDER BY recorded_at, id
|
|
482
|
+
LIMIT $1`,
|
|
483
|
+
[limit + 1, input.account_id ?? null, after?.at ?? null, after?.id ?? null]
|
|
484
|
+
);
|
|
485
|
+
let migrated = 0;
|
|
486
|
+
const quarantined = [];
|
|
487
|
+
for (const candidate of candidates.rows.slice(0, limit)) {
|
|
488
|
+
input.signal?.throwIfAborted();
|
|
489
|
+
const table = candidate.kind === "revision" ? "adcp_reporting_revisions" : "adcp_reporting_adjustments";
|
|
490
|
+
const idColumn = candidate.kind === "revision" ? "revision_id" : "adjustment_id";
|
|
491
|
+
const outcome = await this.transaction(
|
|
492
|
+
async (client) => {
|
|
493
|
+
const locked = await client.query(
|
|
494
|
+
`SELECT data FROM ${table} WHERE ${idColumn} = $1 AND data ? 'rows' FOR UPDATE`,
|
|
495
|
+
[candidate.id]
|
|
496
|
+
);
|
|
497
|
+
const document = locked.rows[0]?.data;
|
|
498
|
+
if (!document) return "skipped";
|
|
499
|
+
let prepared;
|
|
500
|
+
try {
|
|
501
|
+
prepared = await this.rowStorage.prepare({
|
|
502
|
+
rowSetId: candidate.id,
|
|
503
|
+
rowSetKind: candidate.kind,
|
|
504
|
+
accountId: candidate.account_id,
|
|
505
|
+
obligationId: document.reporting_obligation_id,
|
|
506
|
+
rows: document.rows,
|
|
507
|
+
...candidate.kind === "revision" ? { finality: document.finality, controlTotals: document.wireRevision.control_totals } : {},
|
|
508
|
+
binding: document.binding,
|
|
509
|
+
...input.bindingId ? { bindingId: input.bindingId } : {}
|
|
510
|
+
});
|
|
511
|
+
} catch (error) {
|
|
512
|
+
if ((0, import_row_storage.isReportingRowStoreError)(error) && error.code === "INVALID_INPUT") return "quarantined";
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
if (prepared.binding.kind !== "postgres") {
|
|
516
|
+
throw new import_row_storage.ReportingRowStoreError(
|
|
517
|
+
"INVALID_INPUT",
|
|
518
|
+
"migrateInlineRows writes PostgreSQL-kind row sets; select a postgres binding for migrated rows"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
await this.rowStorage.writeInTransaction(client, prepared);
|
|
522
|
+
await client.query(`UPDATE ${table} SET data = data - 'rows' WHERE ${idColumn} = $1`, [candidate.id]);
|
|
523
|
+
return "migrated";
|
|
524
|
+
},
|
|
525
|
+
{ preBeginAdvisoryLock: accountLock(candidate.account_id) }
|
|
526
|
+
);
|
|
527
|
+
if (outcome === "migrated") migrated += 1;
|
|
528
|
+
if (outcome === "quarantined") quarantined.push(candidate.id);
|
|
529
|
+
}
|
|
530
|
+
const last = candidates.rows.length > limit ? candidates.rows[limit - 1] : void 0;
|
|
531
|
+
return {
|
|
532
|
+
migrated,
|
|
533
|
+
quarantined,
|
|
534
|
+
...last ? { cursor: Buffer.from(JSON.stringify({ at: last.at, id: last.id }), "utf8").toString("base64url") } : {}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
389
537
|
async putConfiguration(configuration) {
|
|
390
538
|
return this.transaction(
|
|
391
539
|
async (client) => {
|
|
@@ -442,11 +590,20 @@ class PostgresReportingLedgerStore {
|
|
|
442
590
|
return result.rows.map((row) => clone(row.data));
|
|
443
591
|
}
|
|
444
592
|
async putObligation(obligation) {
|
|
593
|
+
const retired = await this.query(
|
|
594
|
+
`SELECT 1 FROM adcp_reporting_obligation_tombstones WHERE configuration_id = $1 AND period_ordinal = $2`,
|
|
595
|
+
[obligation.configurationId, obligation.periodOrdinal]
|
|
596
|
+
);
|
|
597
|
+
if (retired.rows.length > 0) throw new ReportingLedgerPeriodRetiredError();
|
|
445
598
|
return this.putImmutable(
|
|
446
599
|
`INSERT INTO adcp_reporting_obligations
|
|
447
600
|
(obligation_id, configuration_id, account_id, period_ordinal, period_start, period_end,
|
|
448
601
|
next_attempt_at, state, semantic_fingerprint, data, created_at)
|
|
449
|
-
|
|
602
|
+
SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, clock_timestamp()
|
|
603
|
+
WHERE NOT EXISTS (
|
|
604
|
+
SELECT 1 FROM adcp_reporting_obligation_tombstones
|
|
605
|
+
WHERE configuration_id = $2 AND period_ordinal = $4
|
|
606
|
+
)
|
|
450
607
|
ON CONFLICT DO NOTHING RETURNING data`,
|
|
451
608
|
[
|
|
452
609
|
obligation.reporting_obligation_id,
|
|
@@ -465,7 +622,16 @@ class PostgresReportingLedgerStore {
|
|
|
465
622
|
[obligation.configurationId, obligation.periodOrdinal],
|
|
466
623
|
obligation,
|
|
467
624
|
(value) => value.semanticFingerprint,
|
|
468
|
-
accountLock(obligation.account.account_id)
|
|
625
|
+
accountLock(obligation.account.account_id),
|
|
626
|
+
void 0,
|
|
627
|
+
void 0,
|
|
628
|
+
this.changeFeed ? (client) => this.appendChange(
|
|
629
|
+
client,
|
|
630
|
+
"obligation",
|
|
631
|
+
obligation.account.account_id,
|
|
632
|
+
obligation.reporting_obligation_id,
|
|
633
|
+
obligation.reporting_obligation_id
|
|
634
|
+
) : void 0
|
|
469
635
|
);
|
|
470
636
|
}
|
|
471
637
|
async getObligation(id, account_id) {
|
|
@@ -475,6 +641,18 @@ class PostgresReportingLedgerStore {
|
|
|
475
641
|
[id, account_id ?? null]
|
|
476
642
|
);
|
|
477
643
|
}
|
|
644
|
+
/** Periods retired by retention; planners treat them as already covered. */
|
|
645
|
+
async listRetiredObligationOrdinals(account_id) {
|
|
646
|
+
const result = await this.query(
|
|
647
|
+
`SELECT configuration_id, period_ordinal FROM adcp_reporting_obligation_tombstones
|
|
648
|
+
WHERE ($1::text IS NULL OR account_id = $1) ORDER BY configuration_id, period_ordinal`,
|
|
649
|
+
[account_id ?? null]
|
|
650
|
+
);
|
|
651
|
+
return result.rows.map((row) => ({
|
|
652
|
+
configurationId: row.configuration_id,
|
|
653
|
+
periodOrdinal: Number(row.period_ordinal)
|
|
654
|
+
}));
|
|
655
|
+
}
|
|
478
656
|
async listObligations(account_id) {
|
|
479
657
|
const result = await this.query(
|
|
480
658
|
`SELECT data FROM adcp_reporting_obligations
|
|
@@ -553,6 +731,13 @@ ${managedDueArm} )
|
|
|
553
731
|
]
|
|
554
732
|
);
|
|
555
733
|
if (result.rowCount !== 1) throw new ReportingLedgerLeaseLostError();
|
|
734
|
+
await this.appendChange(
|
|
735
|
+
client,
|
|
736
|
+
"obligation",
|
|
737
|
+
lease.obligation.account.account_id,
|
|
738
|
+
obligation.reporting_obligation_id,
|
|
739
|
+
obligation.reporting_obligation_id
|
|
740
|
+
);
|
|
556
741
|
if (issue) {
|
|
557
742
|
if (issue.reporting_obligation_id !== obligation.reporting_obligation_id) {
|
|
558
743
|
throw new Error("Reporting issue must belong to the leased obligation");
|
|
@@ -621,7 +806,20 @@ ${managedDueArm} )
|
|
|
621
806
|
async commitRevision(revision, lease) {
|
|
622
807
|
assertLeaseTarget(revision.reporting_obligation_id, lease);
|
|
623
808
|
validateRevisionBinding(revision);
|
|
624
|
-
|
|
809
|
+
const prepared = this.rowStorageWrites ? await this.rowStorage.prepare({
|
|
810
|
+
rowSetId: revision.reporting_revision_id,
|
|
811
|
+
rowSetKind: "revision",
|
|
812
|
+
accountId: lease.obligation.account.account_id,
|
|
813
|
+
obligationId: revision.reporting_obligation_id,
|
|
814
|
+
rows: revision.rows,
|
|
815
|
+
finality: revision.finality,
|
|
816
|
+
periodStart: lease.obligation.period.start,
|
|
817
|
+
binding: revision.binding,
|
|
818
|
+
controlTotals: revision.wireRevision.control_totals
|
|
819
|
+
}) : void 0;
|
|
820
|
+
const upload = prepared?.binding.kind === "object" ? await this.uploadRowSetUnlessCommitted(prepared, lease, "adcp_reporting_revisions", "revision_id") : void 0;
|
|
821
|
+
const port = this.notificationActivityPort?.recordLedgerChanged ? this.notificationActivityPort : void 0;
|
|
822
|
+
const result = await this.putImmutable(
|
|
625
823
|
`WITH leased_obligation AS (
|
|
626
824
|
SELECT * FROM adcp_reporting_obligations obligation
|
|
627
825
|
WHERE obligation.obligation_id = $2
|
|
@@ -658,7 +856,7 @@ ${managedDueArm} )
|
|
|
658
856
|
revision.kind,
|
|
659
857
|
revision.supersedes_reporting_revision_id ?? null,
|
|
660
858
|
revision.binding.sha256,
|
|
661
|
-
JSON.stringify(revision),
|
|
859
|
+
JSON.stringify(prepared ? withoutRows(revision) : revision),
|
|
662
860
|
lease.owner,
|
|
663
861
|
lease.generation
|
|
664
862
|
],
|
|
@@ -673,19 +871,31 @@ ${managedDueArm} )
|
|
|
673
871
|
generation: lease.generation
|
|
674
872
|
},
|
|
675
873
|
revisionLegacyCanonicalDigestReplay,
|
|
676
|
-
this.
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
874
|
+
prepared || port || this.changeFeed ? async (client, committed) => {
|
|
875
|
+
if (prepared) await this.writePreparedRowSet(client, prepared, upload);
|
|
876
|
+
await this.appendChange(
|
|
877
|
+
client,
|
|
878
|
+
"revision",
|
|
879
|
+
lease.obligation.account.account_id,
|
|
880
|
+
revision.reporting_revision_id,
|
|
881
|
+
revision.reporting_obligation_id
|
|
882
|
+
);
|
|
883
|
+
await port?.recordLedgerChanged(
|
|
884
|
+
{ obligation: lease.obligation, revision: withRows(committed, revision.rows) },
|
|
885
|
+
client
|
|
886
|
+
);
|
|
887
|
+
} : void 0
|
|
680
888
|
);
|
|
889
|
+
return { inserted: result.inserted, value: withRows(result.value, revision.rows) };
|
|
681
890
|
}
|
|
682
891
|
async getRevision(id, accountId) {
|
|
683
|
-
|
|
892
|
+
const revision = await this.one(
|
|
684
893
|
`SELECT revision.data FROM adcp_reporting_revisions revision
|
|
685
894
|
JOIN adcp_reporting_obligations obligation ON obligation.obligation_id = revision.obligation_id
|
|
686
895
|
WHERE revision.revision_id = $1 AND obligation.account_id = $2`,
|
|
687
896
|
[id, accountId]
|
|
688
897
|
);
|
|
898
|
+
return revision ? this.hydrateRevision(revision, accountId) : null;
|
|
689
899
|
}
|
|
690
900
|
async getRevisionMetadata(id, accountId) {
|
|
691
901
|
return this.one(
|
|
@@ -700,12 +910,13 @@ ${managedDueArm} )
|
|
|
700
910
|
throw new RangeError("Reporting row page bounds are invalid");
|
|
701
911
|
}
|
|
702
912
|
const result = await this.query(
|
|
703
|
-
`SELECT
|
|
704
|
-
|
|
913
|
+
`SELECT revision.data ? 'rows' AS inline,
|
|
914
|
+
CASE WHEN revision.data ? 'rows' THEN jsonb_array_length(revision.data->'rows') END AS total,
|
|
915
|
+
CASE WHEN revision.data ? 'rows' THEN jsonb_path_query_array(
|
|
705
916
|
revision.data->'rows',
|
|
706
917
|
'$[$from to $to]',
|
|
707
918
|
jsonb_build_object('from', $3::bigint, 'to', $3::bigint + $4::bigint - 1)
|
|
708
|
-
) AS rows
|
|
919
|
+
) END AS rows
|
|
709
920
|
FROM adcp_reporting_revisions revision
|
|
710
921
|
JOIN adcp_reporting_obligations obligation ON obligation.obligation_id = revision.obligation_id
|
|
711
922
|
WHERE revision.revision_id = $1 AND obligation.account_id = $2`,
|
|
@@ -713,15 +924,26 @@ ${managedDueArm} )
|
|
|
713
924
|
);
|
|
714
925
|
const row = result.rows[0];
|
|
715
926
|
if (!row) return null;
|
|
716
|
-
return { rows: clone(row.rows), total: Number(row.total) };
|
|
927
|
+
if (row.inline) return { rows: clone(row.rows ?? []), total: Number(row.total) };
|
|
928
|
+
const handle = await this.rowStorage.find(input.reporting_revision_id, input.account_id);
|
|
929
|
+
if (!handle) throw new import_row_storage.ReportingRowStoreError("ROWS_INTEGRITY_FAILED", "revision rows are not recorded");
|
|
930
|
+
if (input.offset >= handle.rowCount) return { rows: [], total: handle.rowCount };
|
|
931
|
+
return {
|
|
932
|
+
rows: await this.rowStorage.readPage(handle, input.offset, input.limit, input.signal),
|
|
933
|
+
total: handle.rowCount
|
|
934
|
+
};
|
|
717
935
|
}
|
|
718
936
|
async listRevisions(obligationId) {
|
|
719
937
|
const result = await this.query(
|
|
720
|
-
`SELECT data FROM adcp_reporting_revisions
|
|
721
|
-
|
|
938
|
+
`SELECT revision.data, obligation.account_id FROM adcp_reporting_revisions revision
|
|
939
|
+
JOIN adcp_reporting_obligations obligation ON obligation.obligation_id = revision.obligation_id
|
|
940
|
+
WHERE revision.obligation_id = $1
|
|
941
|
+
ORDER BY revision.revision_number, revision.revision_id`,
|
|
722
942
|
[obligationId]
|
|
723
943
|
);
|
|
724
|
-
|
|
944
|
+
const revisions = [];
|
|
945
|
+
for (const row of result.rows) revisions.push(await this.hydrateRevision(clone(row.data), row.account_id));
|
|
946
|
+
return revisions;
|
|
725
947
|
}
|
|
726
948
|
async listRevisionMetadata(obligationId, accountId) {
|
|
727
949
|
const result = await this.query(
|
|
@@ -736,7 +958,18 @@ ${managedDueArm} )
|
|
|
736
958
|
async commitAdjustment(adjustment, lease) {
|
|
737
959
|
assertLeaseTarget(adjustment.reporting_obligation_id, lease);
|
|
738
960
|
validateBoundRows(adjustment);
|
|
739
|
-
|
|
961
|
+
const prepared = this.rowStorageWrites ? await this.rowStorage.prepare({
|
|
962
|
+
rowSetId: adjustment.reporting_adjustment_id,
|
|
963
|
+
rowSetKind: "adjustment",
|
|
964
|
+
accountId: lease.obligation.account.account_id,
|
|
965
|
+
obligationId: adjustment.reporting_obligation_id,
|
|
966
|
+
rows: adjustment.rows,
|
|
967
|
+
periodStart: lease.obligation.period.start,
|
|
968
|
+
binding: adjustment.binding
|
|
969
|
+
}) : void 0;
|
|
970
|
+
const upload = prepared?.binding.kind === "object" ? await this.uploadRowSetUnlessCommitted(prepared, lease, "adcp_reporting_adjustments", "adjustment_id") : void 0;
|
|
971
|
+
const port = this.notificationActivityPort?.recordLedgerChanged ? this.notificationActivityPort : void 0;
|
|
972
|
+
const result = await this.putImmutable(
|
|
740
973
|
`WITH leased_obligation AS (
|
|
741
974
|
SELECT * FROM adcp_reporting_obligations obligation
|
|
742
975
|
WHERE obligation.obligation_id = $2
|
|
@@ -767,7 +1000,7 @@ ${managedDueArm} )
|
|
|
767
1000
|
adjustment.adjusts_reporting_revision_id,
|
|
768
1001
|
adjustment.adjustmentNumber,
|
|
769
1002
|
adjustment.binding.sha256,
|
|
770
|
-
JSON.stringify(adjustment),
|
|
1003
|
+
JSON.stringify(prepared ? withoutRows(adjustment) : adjustment),
|
|
771
1004
|
lease.owner,
|
|
772
1005
|
lease.generation
|
|
773
1006
|
],
|
|
@@ -782,19 +1015,624 @@ ${managedDueArm} )
|
|
|
782
1015
|
generation: lease.generation
|
|
783
1016
|
},
|
|
784
1017
|
adjustmentLegacyCanonicalDigestReplay,
|
|
785
|
-
this.
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1018
|
+
prepared || port || this.changeFeed ? async (client, committed) => {
|
|
1019
|
+
if (prepared) await this.writePreparedRowSet(client, prepared, upload);
|
|
1020
|
+
await this.appendChange(
|
|
1021
|
+
client,
|
|
1022
|
+
"adjustment",
|
|
1023
|
+
lease.obligation.account.account_id,
|
|
1024
|
+
adjustment.reporting_adjustment_id,
|
|
1025
|
+
adjustment.reporting_obligation_id
|
|
1026
|
+
);
|
|
1027
|
+
await port?.recordLedgerChanged(
|
|
1028
|
+
{ obligation: lease.obligation, adjustment: withRows(committed, adjustment.rows) },
|
|
1029
|
+
client
|
|
1030
|
+
);
|
|
1031
|
+
} : void 0
|
|
789
1032
|
);
|
|
1033
|
+
return { inserted: result.inserted, value: withRows(result.value, adjustment.rows) };
|
|
790
1034
|
}
|
|
791
1035
|
async listAdjustments(obligationId) {
|
|
792
1036
|
const result = await this.query(
|
|
793
|
-
`SELECT data FROM adcp_reporting_adjustments
|
|
794
|
-
|
|
1037
|
+
`SELECT adjustment.data, obligation.account_id FROM adcp_reporting_adjustments adjustment
|
|
1038
|
+
JOIN adcp_reporting_obligations obligation ON obligation.obligation_id = adjustment.obligation_id
|
|
1039
|
+
WHERE adjustment.obligation_id = $1
|
|
1040
|
+
ORDER BY adjustment.adjustment_number, adjustment.adjustment_id`,
|
|
795
1041
|
[obligationId]
|
|
796
1042
|
);
|
|
797
|
-
|
|
1043
|
+
const adjustments = [];
|
|
1044
|
+
for (const row of result.rows) {
|
|
1045
|
+
const adjustment = clone(row.data);
|
|
1046
|
+
if (Array.isArray(adjustment.rows)) {
|
|
1047
|
+
adjustments.push(adjustment);
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
const handle = await this.rowStorage.find(adjustment.reporting_adjustment_id, row.account_id);
|
|
1051
|
+
if (!handle) throw new import_row_storage.ReportingRowStoreError("ROWS_INTEGRITY_FAILED", "adjustment rows are not recorded");
|
|
1052
|
+
adjustments.push({ ...adjustment, rows: await this.rowStorage.readAll(handle) });
|
|
1053
|
+
}
|
|
1054
|
+
return adjustments;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* The obligation's current revision header: its official revision if one
|
|
1058
|
+
* exists, otherwise its highest-numbered snapshot. An official revision is
|
|
1059
|
+
* terminal, so this is always the highest revision number — one index
|
|
1060
|
+
* lookup. Rows are read separately through `readRevisionRows`.
|
|
1061
|
+
*/
|
|
1062
|
+
async getCurrentRevision(input) {
|
|
1063
|
+
return this.one(
|
|
1064
|
+
`SELECT revision.data - 'rows' AS data
|
|
1065
|
+
FROM adcp_reporting_obligations obligation
|
|
1066
|
+
JOIN LATERAL (
|
|
1067
|
+
SELECT data FROM adcp_reporting_revisions current_revision
|
|
1068
|
+
WHERE current_revision.obligation_id = obligation.obligation_id
|
|
1069
|
+
ORDER BY current_revision.revision_number DESC LIMIT 1
|
|
1070
|
+
) revision ON TRUE
|
|
1071
|
+
WHERE obligation.obligation_id = $1 AND obligation.account_id = $2`,
|
|
1072
|
+
[input.reporting_obligation_id, input.account_id]
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Current revision headers for an account's obligations whose period starts
|
|
1077
|
+
* in `[period_start_from, period_start_to)`, ordered by period start.
|
|
1078
|
+
* Obligations without a revision yet are omitted. Page with `cursor`.
|
|
1079
|
+
*/
|
|
1080
|
+
async listCurrentRevisions(input) {
|
|
1081
|
+
const limit = input.limit ?? 100;
|
|
1082
|
+
positiveInteger(limit, "limit");
|
|
1083
|
+
if (limit > 1e3) throw new RangeError("listCurrentRevisions limit must not exceed 1000");
|
|
1084
|
+
let after;
|
|
1085
|
+
if (input.cursor !== void 0) {
|
|
1086
|
+
try {
|
|
1087
|
+
const parsed = JSON.parse(Buffer.from(input.cursor, "base64url").toString("utf8"));
|
|
1088
|
+
if (typeof parsed.start !== "string" || typeof parsed.id !== "string") throw new Error("invalid");
|
|
1089
|
+
after = { start: parsed.start, id: parsed.id };
|
|
1090
|
+
} catch {
|
|
1091
|
+
throw new RangeError("listCurrentRevisions cursor is invalid");
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
const result = await this.query(
|
|
1095
|
+
`SELECT revision.data - 'rows' AS data,
|
|
1096
|
+
to_char(obligation.period_start AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS start,
|
|
1097
|
+
obligation.obligation_id AS id
|
|
1098
|
+
FROM adcp_reporting_obligations obligation
|
|
1099
|
+
JOIN LATERAL (
|
|
1100
|
+
SELECT data FROM adcp_reporting_revisions current_revision
|
|
1101
|
+
WHERE current_revision.obligation_id = obligation.obligation_id
|
|
1102
|
+
ORDER BY current_revision.revision_number DESC LIMIT 1
|
|
1103
|
+
) revision ON TRUE
|
|
1104
|
+
WHERE obligation.account_id = $1
|
|
1105
|
+
AND obligation.period_start >= $2::timestamptz AND obligation.period_start < $3::timestamptz
|
|
1106
|
+
AND ($4::text IS NULL OR obligation.data->>'delivery_config_id' = $4)
|
|
1107
|
+
AND ($5::timestamptz IS NULL
|
|
1108
|
+
OR (obligation.period_start, obligation.obligation_id) > ($5::timestamptz, $6::text))
|
|
1109
|
+
ORDER BY obligation.period_start, obligation.obligation_id
|
|
1110
|
+
LIMIT $7`,
|
|
1111
|
+
[
|
|
1112
|
+
input.account_id,
|
|
1113
|
+
input.period_start_from,
|
|
1114
|
+
input.period_start_to,
|
|
1115
|
+
input.delivery_config_id ?? null,
|
|
1116
|
+
after?.start ?? null,
|
|
1117
|
+
after?.id ?? null,
|
|
1118
|
+
limit + 1
|
|
1119
|
+
]
|
|
1120
|
+
);
|
|
1121
|
+
const page = result.rows.slice(0, limit);
|
|
1122
|
+
const last = result.rows.length > limit ? page.at(-1) : void 0;
|
|
1123
|
+
return {
|
|
1124
|
+
revisions: page.map((row) => clone(row.data)),
|
|
1125
|
+
...last ? { cursor: Buffer.from(JSON.stringify({ start: last.start, id: last.id }), "utf8").toString("base64url") } : {}
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Host change feed. Omit `cursor` to start from the oldest retained change.
|
|
1130
|
+
* Account-scoped feeds order by sequence; the deployment-wide feed orders by
|
|
1131
|
+
* `(xid, seq)` and returns only changes whose transaction precedes every
|
|
1132
|
+
* transaction still in flight, so no commit is ever skipped. A cursor older
|
|
1133
|
+
* than the retained history throws `ReportingChangeCursorExpiredError`;
|
|
1134
|
+
* resynchronize from `listCurrentRevisions`.
|
|
1135
|
+
*/
|
|
1136
|
+
async changesAfter(input = {}) {
|
|
1137
|
+
if (!this.changeFeed) throw new Error("changesAfter requires the store changeFeed option");
|
|
1138
|
+
const limit = input.limit ?? 500;
|
|
1139
|
+
positiveInteger(limit, "limit");
|
|
1140
|
+
if (limit > 1e4) throw new RangeError("changesAfter limit must not exceed 10000");
|
|
1141
|
+
const account = input.account_id ?? null;
|
|
1142
|
+
const cursor = input.cursor === void 0 ? void 0 : (0, import_change_feed.decodeReportingChangeCursorV1)(input.cursor, account);
|
|
1143
|
+
const horizon = account !== null ? {
|
|
1144
|
+
seq: (await this.query(
|
|
1145
|
+
`SELECT pruned_through_seq::text AS seq FROM adcp_reporting_change_account_horizon
|
|
1146
|
+
WHERE account_id = $1`,
|
|
1147
|
+
[account]
|
|
1148
|
+
)).rows[0]?.seq ?? "0"
|
|
1149
|
+
} : (await this.query(
|
|
1150
|
+
`SELECT pruned_through_xid::text AS xid, pruned_through_seq::text AS seq
|
|
1151
|
+
FROM adcp_reporting_change_horizon WHERE singleton`
|
|
1152
|
+
)).rows[0] ?? { xid: "0", seq: "0" };
|
|
1153
|
+
if (cursor) {
|
|
1154
|
+
const behind = account !== null ? BigInt(horizon.seq) > BigInt(cursor.seq) : BigInt(horizon.xid) > BigInt(cursor.xid) || BigInt(horizon.xid) === BigInt(cursor.xid) && BigInt(horizon.seq) > BigInt(cursor.seq);
|
|
1155
|
+
if (behind) throw new import_change_feed.ReportingChangeCursorExpiredError();
|
|
1156
|
+
}
|
|
1157
|
+
const position = cursor ?? {
|
|
1158
|
+
v: 1,
|
|
1159
|
+
account,
|
|
1160
|
+
seq: horizon.seq,
|
|
1161
|
+
...account === null ? { xid: horizon.xid } : {}
|
|
1162
|
+
};
|
|
1163
|
+
const kinds = input.kinds?.length ? [...input.kinds] : null;
|
|
1164
|
+
const columns = `seq::text AS seq, xid::text AS xid, account_id, record_kind, record_id, obligation_id,
|
|
1165
|
+
to_char(recorded_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS recorded_at`;
|
|
1166
|
+
const result = account !== null ? await this.query(
|
|
1167
|
+
`SELECT ${columns} FROM adcp_reporting_changes
|
|
1168
|
+
WHERE account_id = $1 AND seq > $2::bigint AND ($3::text[] IS NULL OR record_kind = ANY($3))
|
|
1169
|
+
ORDER BY seq LIMIT $4`,
|
|
1170
|
+
[account, position.seq, kinds, limit]
|
|
1171
|
+
) : await this.query(
|
|
1172
|
+
`SELECT ${columns} FROM adcp_reporting_changes
|
|
1173
|
+
WHERE (xid, seq) > ($1::text::xid8, $2::bigint)
|
|
1174
|
+
AND xid < pg_snapshot_xmin(pg_current_snapshot())
|
|
1175
|
+
AND ($3::text[] IS NULL OR record_kind = ANY($3))
|
|
1176
|
+
ORDER BY xid, seq LIMIT $4`,
|
|
1177
|
+
[position.xid ?? "0", position.seq, kinds, limit]
|
|
1178
|
+
);
|
|
1179
|
+
const last = result.rows.at(-1);
|
|
1180
|
+
const next = last ? (0, import_change_feed.encodeReportingChangeCursorV1)({ v: 1, account, seq: last.seq, ...account === null ? { xid: last.xid } : {} }) : input.cursor ?? (0, import_change_feed.encodeReportingChangeCursorV1)(position);
|
|
1181
|
+
return {
|
|
1182
|
+
records: result.rows.map((row) => ({
|
|
1183
|
+
kind: row.record_kind,
|
|
1184
|
+
record_id: row.record_id,
|
|
1185
|
+
reporting_obligation_id: row.obligation_id,
|
|
1186
|
+
account_id: row.account_id,
|
|
1187
|
+
recorded_at: row.recorded_at
|
|
1188
|
+
})),
|
|
1189
|
+
cursor: next
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Record a named durable consumer's position. Registered consumers hold
|
|
1194
|
+
* back change-table pruning until `maxFeedHoldDays` after their last update.
|
|
1195
|
+
*/
|
|
1196
|
+
async saveFeedConsumerCursor(name, cursor) {
|
|
1197
|
+
if (!this.changeFeed) throw new Error("saveFeedConsumerCursor requires the store changeFeed option");
|
|
1198
|
+
let account;
|
|
1199
|
+
try {
|
|
1200
|
+
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
1201
|
+
account = typeof parsed.account === "string" ? parsed.account : null;
|
|
1202
|
+
} catch {
|
|
1203
|
+
throw new RangeError("feed consumer cursor is invalid");
|
|
1204
|
+
}
|
|
1205
|
+
const decoded = (0, import_change_feed.decodeReportingChangeCursorV1)(cursor, account);
|
|
1206
|
+
await this.query(
|
|
1207
|
+
`INSERT INTO adcp_reporting_feed_consumers (consumer_name, account_id, cursor_xid, cursor_seq)
|
|
1208
|
+
VALUES ($1, $2, $3::text::xid8, $4::bigint)
|
|
1209
|
+
ON CONFLICT (consumer_name) DO UPDATE
|
|
1210
|
+
SET account_id = EXCLUDED.account_id, cursor_xid = EXCLUDED.cursor_xid,
|
|
1211
|
+
cursor_seq = EXCLUDED.cursor_seq, updated_at = clock_timestamp()`,
|
|
1212
|
+
[name, account, account === null ? decoded.xid : null, decoded.seq]
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Delete change rows older than `changeRetentionDays` that every consumer
|
|
1217
|
+
* updated within `maxFeedHoldDays` has passed, and advance the expiry
|
|
1218
|
+
* horizon so older cursors fail closed.
|
|
1219
|
+
*/
|
|
1220
|
+
async pruneChanges(input = {}) {
|
|
1221
|
+
if (!this.changeFeed) return { deleted: 0 };
|
|
1222
|
+
const retentionDays = input.changeRetentionDays ?? 30;
|
|
1223
|
+
const holdDays = input.maxFeedHoldDays ?? 7;
|
|
1224
|
+
positiveInteger(retentionDays, "changeRetentionDays");
|
|
1225
|
+
positiveInteger(holdDays, "maxFeedHoldDays");
|
|
1226
|
+
const limit = input.limit ?? 1e4;
|
|
1227
|
+
positiveInteger(limit, "limit");
|
|
1228
|
+
const deleted = await this.transaction(async (client) => {
|
|
1229
|
+
const victims = await client.query(
|
|
1230
|
+
`WITH victims AS (
|
|
1231
|
+
SELECT change.seq FROM adcp_reporting_changes change
|
|
1232
|
+
WHERE change.recorded_at < clock_timestamp() - ($1::integer * INTERVAL '1 day')
|
|
1233
|
+
AND NOT EXISTS (
|
|
1234
|
+
SELECT 1 FROM adcp_reporting_feed_consumers consumer
|
|
1235
|
+
WHERE consumer.updated_at > clock_timestamp() - ($2::integer * INTERVAL '1 day')
|
|
1236
|
+
AND ((consumer.account_id IS NULL
|
|
1237
|
+
AND (change.xid, change.seq) > (consumer.cursor_xid, consumer.cursor_seq))
|
|
1238
|
+
OR (consumer.account_id = change.account_id AND change.seq > consumer.cursor_seq))
|
|
1239
|
+
)
|
|
1240
|
+
ORDER BY change.seq LIMIT $3
|
|
1241
|
+
)
|
|
1242
|
+
DELETE FROM adcp_reporting_changes change USING victims
|
|
1243
|
+
WHERE change.seq = victims.seq
|
|
1244
|
+
RETURNING change.seq::text AS seq, change.xid::text AS xid, change.account_id`,
|
|
1245
|
+
[retentionDays, holdDays, limit]
|
|
1246
|
+
);
|
|
1247
|
+
if (victims.rows.length === 0) return 0;
|
|
1248
|
+
const highest = victims.rows.reduce(
|
|
1249
|
+
(max, row) => BigInt(row.xid) > BigInt(max.xid) || row.xid === max.xid && BigInt(row.seq) > BigInt(max.seq) ? row : max
|
|
1250
|
+
);
|
|
1251
|
+
await client.query(
|
|
1252
|
+
`UPDATE adcp_reporting_change_horizon
|
|
1253
|
+
SET (pruned_through_xid, pruned_through_seq) = ($1::text::xid8, $2::bigint)
|
|
1254
|
+
WHERE singleton AND ($1::text::xid8, $2::bigint) > (pruned_through_xid, pruned_through_seq)`,
|
|
1255
|
+
[highest.xid, highest.seq]
|
|
1256
|
+
);
|
|
1257
|
+
const perAccount = /* @__PURE__ */ new Map();
|
|
1258
|
+
for (const row of victims.rows) {
|
|
1259
|
+
const seq = BigInt(row.seq);
|
|
1260
|
+
if (seq > (perAccount.get(row.account_id) ?? 0n)) perAccount.set(row.account_id, seq);
|
|
1261
|
+
}
|
|
1262
|
+
for (const [accountId, seq] of perAccount) {
|
|
1263
|
+
await client.query(
|
|
1264
|
+
`INSERT INTO adcp_reporting_change_account_horizon (account_id, pruned_through_seq) VALUES ($1, $2::bigint)
|
|
1265
|
+
ON CONFLICT (account_id) DO UPDATE
|
|
1266
|
+
SET pruned_through_seq = GREATEST(adcp_reporting_change_account_horizon.pruned_through_seq,
|
|
1267
|
+
EXCLUDED.pruned_through_seq)`,
|
|
1268
|
+
[accountId, seq.toString()]
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
return victims.rows.length;
|
|
1272
|
+
});
|
|
1273
|
+
return { deleted };
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Retire whole reporting periods whose retention has elapsed (shared SDK
|
|
1277
|
+
* persistence spec §4). A period is eligible once both its end and its
|
|
1278
|
+
* latest publication are older than `max(statusRetentionDays,
|
|
1279
|
+
* recordRetentionDays)`, no worker holds its lease, and no Managed Delivery
|
|
1280
|
+
* materialization still references it. Retirement is period-aligned and
|
|
1281
|
+
* resumable:
|
|
1282
|
+
*
|
|
1283
|
+
* 1. under the account lock, record a `retiring` tombstone and mark the
|
|
1284
|
+
* period's row sets `pruning`, so exact reads answer as expired;
|
|
1285
|
+
* 2. delete the period's external row objects at their recorded versions;
|
|
1286
|
+
* 3. under the account lock, delete the period's ledger records (rows,
|
|
1287
|
+
* revisions, adjustments, consumer statuses, issues, transitions,
|
|
1288
|
+
* lifecycle state, obligation) and mark the tombstone `retired`.
|
|
1289
|
+
*
|
|
1290
|
+
* Planners skip retired periods, and `ledger_retained_from` advances past
|
|
1291
|
+
* them because their obligations no longer exist.
|
|
1292
|
+
*
|
|
1293
|
+
* When the change-feed tables exist, a period that is not yet retiring is
|
|
1294
|
+
* also held while any live registered feed consumer (updated within
|
|
1295
|
+
* `maxFeedHoldDays`, default 7) has not passed that period's change rows in
|
|
1296
|
+
* the consumer's own feed order, the same rule `pruneChanges` applies. A
|
|
1297
|
+
* held period is simply not a candidate this pass. A consumer that stays
|
|
1298
|
+
* silent past `maxFeedHoldDays` stops holding, so retention is never
|
|
1299
|
+
* blocked forever; it then falls behind retention and a sink sees the
|
|
1300
|
+
* retired revisions as missing.
|
|
1301
|
+
*/
|
|
1302
|
+
async retireExpiredPeriods(input) {
|
|
1303
|
+
if (!Number.isSafeInteger(input.statusRetentionDays) || input.statusRetentionDays < 1) {
|
|
1304
|
+
throw new RangeError("statusRetentionDays must be a positive integer");
|
|
1305
|
+
}
|
|
1306
|
+
const recordDays = input.recordRetentionDays ?? input.statusRetentionDays;
|
|
1307
|
+
if (!Number.isSafeInteger(recordDays) || recordDays < input.statusRetentionDays) {
|
|
1308
|
+
throw new RangeError("recordRetentionDays must be an integer no shorter than statusRetentionDays");
|
|
1309
|
+
}
|
|
1310
|
+
const limit = input.limit ?? 100;
|
|
1311
|
+
positiveInteger(limit, "limit");
|
|
1312
|
+
const holdDays = input.maxFeedHoldDays ?? 7;
|
|
1313
|
+
positiveInteger(holdDays, "maxFeedHoldDays");
|
|
1314
|
+
const tables = await this.optionalTables();
|
|
1315
|
+
const materializationHold = tables.has("adcp_reporting_materializations") ? `AND NOT EXISTS (SELECT 1 FROM adcp_reporting_materializations hold WHERE hold.obligation_id = obligation.obligation_id)` : "";
|
|
1316
|
+
const feedHold = tables.has("adcp_reporting_changes") && tables.has("adcp_reporting_feed_consumers") ? `AND NOT EXISTS (
|
|
1317
|
+
SELECT 1 FROM adcp_reporting_changes change
|
|
1318
|
+
JOIN adcp_reporting_feed_consumers consumer
|
|
1319
|
+
ON consumer.updated_at > clock_timestamp() - ($4::integer * INTERVAL '1 day')
|
|
1320
|
+
WHERE change.obligation_id = obligation.obligation_id
|
|
1321
|
+
AND ((consumer.account_id IS NULL
|
|
1322
|
+
AND (change.xid, change.seq) > (consumer.cursor_xid, consumer.cursor_seq))
|
|
1323
|
+
OR (consumer.account_id = change.account_id AND change.seq > consumer.cursor_seq)))` : "";
|
|
1324
|
+
const candidates = await this.query(
|
|
1325
|
+
`SELECT obligation_id, account_id FROM (
|
|
1326
|
+
SELECT tombstone.obligation_id, tombstone.account_id, 0 AS resume, tombstone.period_end
|
|
1327
|
+
FROM adcp_reporting_obligation_tombstones tombstone
|
|
1328
|
+
WHERE tombstone.state = 'retiring' AND ($2::text IS NULL OR tombstone.account_id = $2)
|
|
1329
|
+
UNION ALL
|
|
1330
|
+
SELECT obligation.obligation_id, obligation.account_id, 1, obligation.period_end
|
|
1331
|
+
FROM adcp_reporting_obligations obligation
|
|
1332
|
+
WHERE ($2::text IS NULL OR obligation.account_id = $2)
|
|
1333
|
+
AND obligation.period_end + ($3::integer * INTERVAL '1 day') <= clock_timestamp()
|
|
1334
|
+
AND (obligation.lease_expires_at IS NULL OR obligation.lease_expires_at <= clock_timestamp())
|
|
1335
|
+
AND COALESCE((SELECT MAX(revision.recorded_at) FROM adcp_reporting_revisions revision
|
|
1336
|
+
WHERE revision.obligation_id = obligation.obligation_id), obligation.created_at)
|
|
1337
|
+
+ ($3::integer * INTERVAL '1 day') <= clock_timestamp()
|
|
1338
|
+
AND COALESCE((SELECT MAX(adjustment.recorded_at) FROM adcp_reporting_adjustments adjustment
|
|
1339
|
+
WHERE adjustment.obligation_id = obligation.obligation_id), '-infinity'::timestamptz)
|
|
1340
|
+
+ ($3::integer * INTERVAL '1 day') <= clock_timestamp()
|
|
1341
|
+
AND NOT EXISTS (SELECT 1 FROM adcp_reporting_obligation_tombstones tombstone
|
|
1342
|
+
WHERE tombstone.obligation_id = obligation.obligation_id)
|
|
1343
|
+
${materializationHold}
|
|
1344
|
+
${feedHold}
|
|
1345
|
+
) candidate
|
|
1346
|
+
ORDER BY resume, period_end, obligation_id
|
|
1347
|
+
LIMIT $1`,
|
|
1348
|
+
feedHold ? [limit, input.account_id ?? null, recordDays, holdDays] : [limit, input.account_id ?? null, recordDays]
|
|
1349
|
+
);
|
|
1350
|
+
let retired = 0;
|
|
1351
|
+
const failed = [];
|
|
1352
|
+
const failures = [];
|
|
1353
|
+
for (const candidate of candidates.rows) {
|
|
1354
|
+
input.signal?.throwIfAborted();
|
|
1355
|
+
try {
|
|
1356
|
+
await this.retirePeriod(candidate.obligation_id, candidate.account_id, tables, input.signal);
|
|
1357
|
+
retired += 1;
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
if (error instanceof ReportingLedgerPeriodRetiredError) continue;
|
|
1360
|
+
failed.push(candidate.obligation_id);
|
|
1361
|
+
failures.push({
|
|
1362
|
+
reporting_obligation_id: candidate.obligation_id,
|
|
1363
|
+
cause: (0, import_row_storage.isReportingRowStoreError)(error) ? error.code : error instanceof Error ? error.name : "Error"
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return { retired, failed, failures };
|
|
1368
|
+
}
|
|
1369
|
+
async retirePeriod(obligationId, accountId, tables, signal) {
|
|
1370
|
+
const rowStorage = tables.has("adcp_reporting_row_sets");
|
|
1371
|
+
await this.transaction(
|
|
1372
|
+
async (client) => {
|
|
1373
|
+
await client.query(
|
|
1374
|
+
`INSERT INTO adcp_reporting_obligation_tombstones
|
|
1375
|
+
(obligation_id, account_id, configuration_id, period_ordinal, period_start, period_end, state,
|
|
1376
|
+
final_revision_id, final_content_sha256, revision_count, adjustment_count)
|
|
1377
|
+
SELECT obligation.obligation_id, obligation.account_id, obligation.configuration_id,
|
|
1378
|
+
obligation.period_ordinal, obligation.period_start, obligation.period_end, 'retiring',
|
|
1379
|
+
latest.revision_id, latest.content_sha256,
|
|
1380
|
+
(SELECT count(*) FROM adcp_reporting_revisions revision WHERE revision.obligation_id = obligation.obligation_id),
|
|
1381
|
+
(SELECT count(*) FROM adcp_reporting_adjustments adjustment WHERE adjustment.obligation_id = obligation.obligation_id)
|
|
1382
|
+
FROM adcp_reporting_obligations obligation
|
|
1383
|
+
LEFT JOIN LATERAL (
|
|
1384
|
+
SELECT revision.revision_id, revision.content_sha256 FROM adcp_reporting_revisions revision
|
|
1385
|
+
WHERE revision.obligation_id = obligation.obligation_id
|
|
1386
|
+
ORDER BY revision.revision_number DESC LIMIT 1
|
|
1387
|
+
) latest ON TRUE
|
|
1388
|
+
WHERE obligation.obligation_id = $1
|
|
1389
|
+
ON CONFLICT (obligation_id) DO NOTHING`,
|
|
1390
|
+
[obligationId]
|
|
1391
|
+
);
|
|
1392
|
+
if (rowStorage) {
|
|
1393
|
+
await client.query(
|
|
1394
|
+
`UPDATE adcp_reporting_row_sets SET rows_state = 'pruning', rows_state_changed_at = clock_timestamp()
|
|
1395
|
+
WHERE obligation_id = $1 AND rows_state IN ('live', 'unavailable')`,
|
|
1396
|
+
[obligationId]
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
{ preBeginAdvisoryLock: accountLock(accountId) }
|
|
1401
|
+
);
|
|
1402
|
+
if (rowStorage) {
|
|
1403
|
+
const objects = await this.query(
|
|
1404
|
+
`SELECT DISTINCT row_set.row_binding_id, chunk.object_key, chunk.native_version
|
|
1405
|
+
FROM adcp_reporting_row_chunks chunk
|
|
1406
|
+
JOIN adcp_reporting_row_sets row_set ON row_set.row_set_id = chunk.row_set_id
|
|
1407
|
+
WHERE chunk.obligation_id = $1 AND chunk.object_key IS NOT NULL`,
|
|
1408
|
+
[obligationId]
|
|
1409
|
+
);
|
|
1410
|
+
const byBinding = /* @__PURE__ */ new Map();
|
|
1411
|
+
for (const row of objects.rows) {
|
|
1412
|
+
const list = byBinding.get(row.row_binding_id) ?? [];
|
|
1413
|
+
list.push({ objectKey: row.object_key, nativeVersion: row.native_version });
|
|
1414
|
+
byBinding.set(row.row_binding_id, list);
|
|
1415
|
+
}
|
|
1416
|
+
for (const [bindingId, list] of byBinding) await this.rowStorage.deleteObjects(bindingId, list, signal);
|
|
1417
|
+
}
|
|
1418
|
+
await this.transaction(
|
|
1419
|
+
async (client) => {
|
|
1420
|
+
const revisionIds = `SELECT revision_id FROM adcp_reporting_revisions WHERE obligation_id = $1`;
|
|
1421
|
+
if (rowStorage) {
|
|
1422
|
+
await client.query(`DELETE FROM adcp_reporting_row_chunks WHERE obligation_id = $1`, [obligationId]);
|
|
1423
|
+
await client.query(`DELETE FROM adcp_reporting_row_sets WHERE obligation_id = $1`, [obligationId]);
|
|
1424
|
+
await client.query(`DELETE FROM adcp_reporting_chunk_bodies WHERE account_id = $2 AND obligation_id = $1`, [
|
|
1425
|
+
obligationId,
|
|
1426
|
+
accountId
|
|
1427
|
+
]);
|
|
1428
|
+
}
|
|
1429
|
+
await client.query(
|
|
1430
|
+
`DELETE FROM adcp_reporting_consumer_statuses
|
|
1431
|
+
WHERE obligation_id = $1 OR revision_id IN (${revisionIds})`,
|
|
1432
|
+
[obligationId]
|
|
1433
|
+
);
|
|
1434
|
+
await client.query(`DELETE FROM adcp_reporting_adjustments WHERE obligation_id = $1`, [obligationId]);
|
|
1435
|
+
await client.query(`DELETE FROM adcp_reporting_revisions WHERE obligation_id = $1`, [obligationId]);
|
|
1436
|
+
await client.query(`DELETE FROM adcp_reporting_issues WHERE obligation_id = $1`, [obligationId]);
|
|
1437
|
+
await client.query(`DELETE FROM adcp_reporting_transitions WHERE obligation_id = $1`, [obligationId]);
|
|
1438
|
+
await client.query(`DELETE FROM adcp_reporting_lifecycle_state WHERE obligation_id = $1`, [obligationId]);
|
|
1439
|
+
await client.query(`DELETE FROM adcp_reporting_obligations WHERE obligation_id = $1`, [obligationId]);
|
|
1440
|
+
const retiredNow = await client.query(
|
|
1441
|
+
`UPDATE adcp_reporting_obligation_tombstones SET state = 'retired', retired_at = clock_timestamp()
|
|
1442
|
+
WHERE obligation_id = $1 AND state = 'retiring'`,
|
|
1443
|
+
[obligationId]
|
|
1444
|
+
);
|
|
1445
|
+
if (retiredNow.rowCount === 1) {
|
|
1446
|
+
await this.appendChange(client, "retirement", accountId, obligationId, obligationId);
|
|
1447
|
+
}
|
|
1448
|
+
},
|
|
1449
|
+
{ preBeginAdvisoryLock: accountLock(accountId) }
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Resolve abandoned object uploads (spec §4.3). An expired `open` write
|
|
1454
|
+
* intent whose ledger commit never happened is moved to `sweeping` under
|
|
1455
|
+
* the account lock, so no writer can commit against it, then only the
|
|
1456
|
+
* objects that upload created — never adopted ones, never any object a
|
|
1457
|
+
* committed chunk references — are deleted at their recorded versions.
|
|
1458
|
+
* Interrupted sweeps resume.
|
|
1459
|
+
*/
|
|
1460
|
+
async sweepRowWriteIntents(input = {}) {
|
|
1461
|
+
const limit = input.limit ?? 100;
|
|
1462
|
+
positiveInteger(limit, "limit");
|
|
1463
|
+
const tables = await this.optionalTables();
|
|
1464
|
+
if (!tables.has("adcp_reporting_row_write_intents")) return { swept: 0, objectsDeleted: 0, failed: 0 };
|
|
1465
|
+
const expired = await this.query(
|
|
1466
|
+
`SELECT row_binding_id, account_id, row_set_id, content_sha256 FROM adcp_reporting_row_write_intents
|
|
1467
|
+
WHERE ($2::text IS NULL OR account_id = $2)
|
|
1468
|
+
AND (state = 'sweeping' OR expires_at <= clock_timestamp())
|
|
1469
|
+
ORDER BY expires_at LIMIT $1`,
|
|
1470
|
+
[limit, input.account_id ?? null]
|
|
1471
|
+
);
|
|
1472
|
+
let swept = 0;
|
|
1473
|
+
let objectsDeleted = 0;
|
|
1474
|
+
let failed = 0;
|
|
1475
|
+
for (const intent of expired.rows) {
|
|
1476
|
+
input.signal?.throwIfAborted();
|
|
1477
|
+
const key = [intent.row_binding_id, intent.row_set_id, intent.content_sha256];
|
|
1478
|
+
try {
|
|
1479
|
+
const claimed = await this.transaction(
|
|
1480
|
+
async (client) => {
|
|
1481
|
+
const updated = await client.query(
|
|
1482
|
+
`UPDATE adcp_reporting_row_write_intents SET state = 'sweeping'
|
|
1483
|
+
WHERE row_binding_id = $1 AND row_set_id = $2 AND content_sha256 = $3
|
|
1484
|
+
AND (state = 'sweeping' OR expires_at <= clock_timestamp())
|
|
1485
|
+
RETURNING created_objects`,
|
|
1486
|
+
key
|
|
1487
|
+
);
|
|
1488
|
+
return updated.rows[0]?.created_objects;
|
|
1489
|
+
},
|
|
1490
|
+
{ preBeginAdvisoryLock: accountLock(intent.account_id) }
|
|
1491
|
+
);
|
|
1492
|
+
if (!claimed) continue;
|
|
1493
|
+
const orphaned = [];
|
|
1494
|
+
for (const object of claimed) {
|
|
1495
|
+
const referenced = await this.query(
|
|
1496
|
+
`SELECT 1 FROM adcp_reporting_row_chunks WHERE object_key = $1 AND native_version = $2 LIMIT 1`,
|
|
1497
|
+
[object.objectKey, object.nativeVersion]
|
|
1498
|
+
);
|
|
1499
|
+
if (referenced.rows.length === 0) orphaned.push(object);
|
|
1500
|
+
}
|
|
1501
|
+
if (orphaned.length > 0) await this.rowStorage.deleteObjects(intent.row_binding_id, orphaned, input.signal);
|
|
1502
|
+
await this.query(
|
|
1503
|
+
`DELETE FROM adcp_reporting_row_write_intents
|
|
1504
|
+
WHERE row_binding_id = $1 AND row_set_id = $2 AND content_sha256 = $3 AND state = 'sweeping'`,
|
|
1505
|
+
key
|
|
1506
|
+
);
|
|
1507
|
+
swept += 1;
|
|
1508
|
+
objectsDeleted += orphaned.length;
|
|
1509
|
+
} catch {
|
|
1510
|
+
failed += 1;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return { swept, objectsDeleted, failed };
|
|
1514
|
+
}
|
|
1515
|
+
async optionalTables() {
|
|
1516
|
+
const result = await this.query(
|
|
1517
|
+
`SELECT table_name AS name FROM information_schema.tables
|
|
1518
|
+
WHERE table_schema = current_schema()
|
|
1519
|
+
AND table_name IN ('adcp_reporting_row_sets', 'adcp_reporting_row_write_intents',
|
|
1520
|
+
'adcp_reporting_materializations', 'adcp_reporting_changes',
|
|
1521
|
+
'adcp_reporting_feed_consumers')`
|
|
1522
|
+
);
|
|
1523
|
+
return new Set(result.rows.map((row) => row.name));
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Upload an object-kind row set before its ledger commit (spec §3.5):
|
|
1527
|
+
* skip when the document already exists (replay), refuse unless the lease
|
|
1528
|
+
* still covers the upload deadline, open or take over an expired write
|
|
1529
|
+
* intent under the account lock, then write every chunk create-only,
|
|
1530
|
+
* recording each object this upload created in the intent first.
|
|
1531
|
+
*/
|
|
1532
|
+
async uploadRowSetUnlessCommitted(prepared, lease, table, idColumn) {
|
|
1533
|
+
const { input } = prepared;
|
|
1534
|
+
const committed = await this.query(`SELECT 1 FROM ${table} WHERE ${idColumn} = $1`, [input.rowSetId]);
|
|
1535
|
+
if (committed.rows.length > 0) return void 0;
|
|
1536
|
+
const shared = await this.rowStorage.findShareable(this.pool, prepared);
|
|
1537
|
+
if (shared) return { locators: shared.locators, sharedFrom: shared.rowSetId };
|
|
1538
|
+
const ownerToken = (0, import_row_storage.reportingRowWriteIntentIdV1)();
|
|
1539
|
+
const budget = this.rowStorage.writeDeadlineMilliseconds;
|
|
1540
|
+
const intentKey = [prepared.bindingId, input.rowSetId, input.binding.sha256];
|
|
1541
|
+
await this.transaction(
|
|
1542
|
+
async (client) => {
|
|
1543
|
+
const opened = await client.query(
|
|
1544
|
+
`INSERT INTO adcp_reporting_row_write_intents
|
|
1545
|
+
(row_binding_id, account_id, row_set_id, content_sha256, state, owner_token, expires_at)
|
|
1546
|
+
SELECT $1, $2, $3, $4, 'open', $5, obligation.lease_expires_at + ($6::bigint * INTERVAL '1 millisecond')
|
|
1547
|
+
FROM adcp_reporting_obligations obligation
|
|
1548
|
+
WHERE obligation.obligation_id = $7 AND obligation.lease_owner = $8
|
|
1549
|
+
AND obligation.lease_generation = $9
|
|
1550
|
+
AND obligation.lease_expires_at - clock_timestamp() >= (($6::bigint + 5000) * INTERVAL '1 millisecond')
|
|
1551
|
+
ON CONFLICT (row_binding_id, row_set_id, content_sha256) DO UPDATE
|
|
1552
|
+
SET owner_token = EXCLUDED.owner_token, expires_at = EXCLUDED.expires_at
|
|
1553
|
+
WHERE adcp_reporting_row_write_intents.state = 'open'
|
|
1554
|
+
AND adcp_reporting_row_write_intents.expires_at <= clock_timestamp()
|
|
1555
|
+
RETURNING owner_token`,
|
|
1556
|
+
[
|
|
1557
|
+
prepared.bindingId,
|
|
1558
|
+
input.accountId,
|
|
1559
|
+
input.rowSetId,
|
|
1560
|
+
input.binding.sha256,
|
|
1561
|
+
ownerToken,
|
|
1562
|
+
budget,
|
|
1563
|
+
input.obligationId,
|
|
1564
|
+
lease.owner,
|
|
1565
|
+
lease.generation
|
|
1566
|
+
]
|
|
1567
|
+
);
|
|
1568
|
+
if (opened.rows.length === 1) return;
|
|
1569
|
+
const held = await client.query(
|
|
1570
|
+
`SELECT lease_expires_at - clock_timestamp() >= (($4::bigint + 5000) * INTERVAL '1 millisecond') AS budget
|
|
1571
|
+
FROM adcp_reporting_obligations
|
|
1572
|
+
WHERE obligation_id = $1 AND lease_owner = $2 AND lease_generation = $3
|
|
1573
|
+
AND lease_expires_at > clock_timestamp()`,
|
|
1574
|
+
[input.obligationId, lease.owner, lease.generation, budget]
|
|
1575
|
+
);
|
|
1576
|
+
if (held.rows.length === 0) throw new ReportingLedgerLeaseLostError();
|
|
1577
|
+
if (!held.rows[0].budget) {
|
|
1578
|
+
throw new import_row_storage.ReportingRowStoreError("DEADLINE_EXCEEDED", "the lease does not cover the row upload deadline");
|
|
1579
|
+
}
|
|
1580
|
+
throw new import_row_storage.ReportingRowStoreError("STATE_UNAVAILABLE", "another writer holds this row upload");
|
|
1581
|
+
},
|
|
1582
|
+
{ preBeginAdvisoryLock: accountLock(input.accountId) }
|
|
1583
|
+
);
|
|
1584
|
+
const locators = await this.rowStorage.upload(prepared, ownerToken, async (object) => {
|
|
1585
|
+
const recorded = await this.query(
|
|
1586
|
+
`UPDATE adcp_reporting_row_write_intents
|
|
1587
|
+
SET created_objects = created_objects || $5::jsonb
|
|
1588
|
+
WHERE row_binding_id = $1 AND row_set_id = $2 AND content_sha256 = $3
|
|
1589
|
+
AND owner_token = $4 AND state = 'open'`,
|
|
1590
|
+
[...intentKey, ownerToken, JSON.stringify([object])]
|
|
1591
|
+
);
|
|
1592
|
+
if (recorded.rowCount !== 1) {
|
|
1593
|
+
throw new import_row_storage.ReportingRowStoreError("STATE_UNAVAILABLE", "the row upload intent was taken over or swept");
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
return { locators, ownerToken };
|
|
1597
|
+
}
|
|
1598
|
+
/** Persist a prepared row set and close its upload intent in the ledger commit transaction. */
|
|
1599
|
+
async writePreparedRowSet(client, prepared, upload) {
|
|
1600
|
+
if (prepared.binding.kind === "postgres") {
|
|
1601
|
+
const shared = await this.rowStorage.findShareable(client, prepared);
|
|
1602
|
+
await this.rowStorage.writeInTransaction(client, prepared, void 0, shared?.rowSetId);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (!upload) throw new import_row_storage.ReportingRowStoreError("STATE_UNAVAILABLE", "object rows were not uploaded");
|
|
1606
|
+
if (upload.sharedFrom) {
|
|
1607
|
+
const live = await client.query(
|
|
1608
|
+
`SELECT 1 FROM adcp_reporting_row_sets WHERE row_set_id = $1 AND rows_state = 'live' FOR SHARE`,
|
|
1609
|
+
[upload.sharedFrom]
|
|
1610
|
+
);
|
|
1611
|
+
if (live.rows.length !== 1) {
|
|
1612
|
+
throw new import_row_storage.ReportingRowStoreError("STATE_UNAVAILABLE", "the shared row set is no longer live");
|
|
1613
|
+
}
|
|
1614
|
+
} else {
|
|
1615
|
+
const closed = await client.query(
|
|
1616
|
+
`DELETE FROM adcp_reporting_row_write_intents
|
|
1617
|
+
WHERE row_binding_id = $1 AND row_set_id = $2 AND content_sha256 = $3
|
|
1618
|
+
AND owner_token = $4 AND state = 'open'`,
|
|
1619
|
+
[prepared.bindingId, prepared.input.rowSetId, prepared.input.binding.sha256, upload.ownerToken]
|
|
1620
|
+
);
|
|
1621
|
+
if (closed.rowCount !== 1) {
|
|
1622
|
+
throw new import_row_storage.ReportingRowStoreError("STATE_UNAVAILABLE", "the row upload intent was taken over or swept");
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
await this.rowStorage.writeInTransaction(client, prepared, upload.locators, upload.sharedFrom);
|
|
1626
|
+
}
|
|
1627
|
+
/** Attach verified rows to a revision document whose rows live in row storage. */
|
|
1628
|
+
async hydrateRevision(revision, accountId) {
|
|
1629
|
+
if (Array.isArray(revision.rows)) return revision;
|
|
1630
|
+
const handle = await this.rowStorage.find(revision.reporting_revision_id, accountId);
|
|
1631
|
+
if (!handle) throw new import_row_storage.ReportingRowStoreError("ROWS_INTEGRITY_FAILED", "revision rows are not recorded");
|
|
1632
|
+
if (handle.contentSha256 !== revision.binding.sha256 || handle.rowCount !== revision.binding.rowCount) {
|
|
1633
|
+
throw new import_row_storage.ReportingRowStoreError("ROWS_INTEGRITY_FAILED", "row set does not match the revision binding");
|
|
1634
|
+
}
|
|
1635
|
+
return { ...revision, rows: await this.rowStorage.readAll(handle, revision.wireRevision.control_totals) };
|
|
798
1636
|
}
|
|
799
1637
|
async listAdjustmentMetadata(obligationId) {
|
|
800
1638
|
const result = await this.query(
|
|
@@ -2650,7 +3488,7 @@ ${managedDueArm} )
|
|
|
2650
3488
|
releaseError = rollbackCause instanceof Error ? rollbackCause : new Error("Reporting ledger rollback failed");
|
|
2651
3489
|
}
|
|
2652
3490
|
}
|
|
2653
|
-
if (error instanceof ReportingLedgerLeaseLostError || error instanceof ReportingLedgerContinuityError || error instanceof import_types.ReportingLedgerSnapshotUnavailableError || error instanceof import_types2.ReportingConsumerStatusConflictError) {
|
|
3491
|
+
if (error instanceof ReportingLedgerLeaseLostError || error instanceof ReportingLedgerContinuityError || error instanceof ReportingLedgerPeriodRetiredError || error instanceof import_types.ReportingLedgerSnapshotUnavailableError || error instanceof import_types2.ReportingConsumerStatusConflictError || (0, import_row_storage.isReportingRowStoreError)(error)) {
|
|
2654
3492
|
throw error;
|
|
2655
3493
|
}
|
|
2656
3494
|
throw new Error("PostgresReportingLedgerStore transaction failed", { cause: error });
|
|
@@ -3101,6 +3939,13 @@ function clone(value) {
|
|
|
3101
3939
|
function positiveInteger(value, name) {
|
|
3102
3940
|
if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError(`${name} must be a positive safe integer`);
|
|
3103
3941
|
}
|
|
3942
|
+
function withoutRows(value) {
|
|
3943
|
+
const { rows: _rows, ...rest } = value;
|
|
3944
|
+
return rest;
|
|
3945
|
+
}
|
|
3946
|
+
function withRows(value, rows) {
|
|
3947
|
+
return Array.isArray(value.rows) ? value : { ...value, rows: clone(rows) };
|
|
3948
|
+
}
|
|
3104
3949
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3105
3950
|
0 && (module.exports = {
|
|
3106
3951
|
PostgresReportingLedgerStore,
|
|
@@ -3109,5 +3954,6 @@ function positiveInteger(value, name) {
|
|
|
3109
3954
|
REPORTING_LEDGER_MIGRATION,
|
|
3110
3955
|
ReportingLedgerContinuityError,
|
|
3111
3956
|
ReportingLedgerLeaseLostError,
|
|
3957
|
+
ReportingLedgerPeriodRetiredError,
|
|
3112
3958
|
sweepExpiredReportingLedgerState
|
|
3113
3959
|
});
|