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