@adcp/sdk 15.7.1 → 15.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/core/SingleAgentClient.d.mts +14 -0
- package/dist/lib/core/SingleAgentClient.d.ts +14 -0
- package/dist/lib/core/SingleAgentClient.js +138 -54
- package/dist/lib/core/SingleAgentClient.mjs +141 -54
- package/dist/lib/core/agent-discovery-cache.d.mts +2 -0
- package/dist/lib/core/agent-discovery-cache.d.ts +2 -0
- package/dist/lib/core/creative-schema-support.d.mts +3 -0
- package/dist/lib/core/creative-schema-support.d.ts +3 -0
- package/dist/lib/core/creative-schema-support.js +113 -0
- package/dist/lib/core/creative-schema-support.mjs +89 -0
- package/dist/lib/core/product-cache.js +4 -3
- package/dist/lib/core/product-cache.mjs +4 -3
- package/dist/lib/reporting/ledger/handler.d.mts +10 -3
- package/dist/lib/reporting/ledger/handler.d.ts +10 -3
- package/dist/lib/reporting/ledger/handler.js +17 -4
- package/dist/lib/reporting/ledger/handler.mjs +17 -4
- package/dist/lib/reporting/ledger/managed.d.mts +3 -1
- package/dist/lib/reporting/ledger/managed.d.ts +3 -1
- package/dist/lib/reporting/ledger/managed.js +4 -1
- package/dist/lib/reporting/ledger/managed.mjs +4 -1
- package/dist/lib/reporting/ledger/postgres.d.mts +87 -0
- package/dist/lib/reporting/ledger/postgres.d.ts +87 -0
- package/dist/lib/reporting/ledger/postgres.js +316 -15
- package/dist/lib/reporting/ledger/postgres.mjs +316 -15
- package/dist/lib/reporting/ledger/row-storage.d.mts +23 -0
- package/dist/lib/reporting/ledger/row-storage.d.ts +23 -0
- package/dist/lib/reporting/ledger/row-storage.js +56 -0
- package/dist/lib/reporting/ledger/row-storage.mjs +56 -0
- package/dist/lib/reporting/ledger/types.d.mts +26 -0
- package/dist/lib/reporting/ledger/types.d.ts +26 -0
- package/dist/lib/reporting/service/index.d.mts +33 -3
- package/dist/lib/reporting/service/index.d.ts +33 -3
- package/dist/lib/reporting/service/index.js +20 -2
- package/dist/lib/reporting/service/index.mjs +20 -2
- package/dist/lib/reporting/service/maintenance.d.mts +20 -1
- package/dist/lib/reporting/service/maintenance.d.ts +20 -1
- package/dist/lib/reporting/service/maintenance.js +25 -2
- package/dist/lib/reporting/service/maintenance.mjs +25 -2
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/testing/storyboard/request-builder.js +14 -16
- package/dist/lib/testing/storyboard/request-builder.mjs +14 -16
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/v2/projection/creative-delivery.d.mts +22 -0
- package/dist/lib/v2/projection/creative-delivery.d.ts +22 -0
- package/dist/lib/v2/projection/creative-delivery.js +92 -0
- package/dist/lib/v2/projection/creative-delivery.mjs +91 -0
- package/dist/lib/v2/projection/legacy-metadata.d.mts +6 -3
- package/dist/lib/v2/projection/legacy-metadata.d.ts +6 -3
- package/dist/lib/v2/projection/legacy-metadata.js +17 -0
- package/dist/lib/v2/projection/legacy-metadata.mjs +15 -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/TYPE-SUMMARY.md +2 -2
- package/docs/guides/CREATIVE-DELIVERY.md +16 -0
- package/docs/guides/REPORTING-LEDGER-CUTOVER.md +405 -0
- package/docs/guides/REPORTING-LEDGER.md +130 -0
- package/docs/guides/REPORTING-OPERATIONS.md +12 -0
- package/docs/llms.txt +3 -2
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/package.json +1 -1
|
@@ -443,6 +443,14 @@ class PostgresReportingLedgerStore {
|
|
|
443
443
|
[accountId, kind, recordId, obligationId]
|
|
444
444
|
);
|
|
445
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Throws `INVALID_INPUT` when any configured object row binding's key template omits
|
|
448
|
+
* `{revision_key}`, which makes snapshot compaction unsafe (see `compactSupersededSnapshots`).
|
|
449
|
+
* Called by `compactSupersededSnapshots` and by service construction when compaction is enabled.
|
|
450
|
+
*/
|
|
451
|
+
assertCompactionSafeRowBindings() {
|
|
452
|
+
this.rowStorage.assertCompactionSafeKeyTemplates();
|
|
453
|
+
}
|
|
446
454
|
/** Probe and register row storage when enabled. Safe to call repeatedly. */
|
|
447
455
|
async readyRowStorage() {
|
|
448
456
|
if (this.rowStorageWrites) await this.rowStorage.ready();
|
|
@@ -953,7 +961,14 @@ ${managedDueArm} )
|
|
|
953
961
|
[obligationId]
|
|
954
962
|
);
|
|
955
963
|
const revisions = [];
|
|
956
|
-
for (const row of result.rows)
|
|
964
|
+
for (const row of result.rows) {
|
|
965
|
+
try {
|
|
966
|
+
revisions.push(await this.hydrateRevision(clone(row.data), row.account_id));
|
|
967
|
+
} catch (error) {
|
|
968
|
+
if (isReportingRowStoreError(error) && error.code === "ROWS_EXPIRED") continue;
|
|
969
|
+
throw error;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
957
972
|
return revisions;
|
|
958
973
|
}
|
|
959
974
|
async listRevisionMetadata(obligationId, accountId) {
|
|
@@ -1177,14 +1192,14 @@ ${managedDueArm} )
|
|
|
1177
1192
|
const result = account !== null ? await this.query(
|
|
1178
1193
|
`SELECT ${columns} FROM adcp_reporting_changes
|
|
1179
1194
|
WHERE account_id = $1 AND seq > $2::bigint AND ($3::text[] IS NULL OR record_kind = ANY($3))
|
|
1180
|
-
ORDER BY seq LIMIT $4`,
|
|
1195
|
+
ORDER BY adcp_reporting_changes.seq LIMIT $4`,
|
|
1181
1196
|
[account, position.seq, kinds, limit]
|
|
1182
1197
|
) : await this.query(
|
|
1183
1198
|
`SELECT ${columns} FROM adcp_reporting_changes
|
|
1184
1199
|
WHERE (xid, seq) > ($1::text::xid8, $2::bigint)
|
|
1185
1200
|
AND xid < pg_snapshot_xmin(pg_current_snapshot())
|
|
1186
1201
|
AND ($3::text[] IS NULL OR record_kind = ANY($3))
|
|
1187
|
-
ORDER BY xid, seq LIMIT $4`,
|
|
1202
|
+
ORDER BY adcp_reporting_changes.xid, adcp_reporting_changes.seq LIMIT $4`,
|
|
1188
1203
|
[position.xid ?? "0", position.seq, kinds, limit]
|
|
1189
1204
|
);
|
|
1190
1205
|
const last = result.rows.at(-1);
|
|
@@ -1424,7 +1439,9 @@ ${managedDueArm} )
|
|
|
1424
1439
|
list.push({ objectKey: row.object_key, nativeVersion: row.native_version });
|
|
1425
1440
|
byBinding.set(row.row_binding_id, list);
|
|
1426
1441
|
}
|
|
1427
|
-
for (const [bindingId, list] of byBinding)
|
|
1442
|
+
for (const [bindingId, list] of byBinding) {
|
|
1443
|
+
await this.deleteUnreferencedObjects(bindingId, list, { obligationId }, signal);
|
|
1444
|
+
}
|
|
1428
1445
|
}
|
|
1429
1446
|
await this.transaction(
|
|
1430
1447
|
async (client) => {
|
|
@@ -1460,6 +1477,228 @@ ${managedDueArm} )
|
|
|
1460
1477
|
{ preBeginAdvisoryLock: accountLock(accountId) }
|
|
1461
1478
|
);
|
|
1462
1479
|
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Compact superseded snapshot revisions to metadata only, as adcontextprotocol/adcp#8100
|
|
1482
|
+
* permits (`reporting-revision.json` `content_retention`). Opt-in: nothing calls this
|
|
1483
|
+
* unless the host enables `compaction` in maintenance, and it must not be enabled in
|
|
1484
|
+
* production before the AdCP release that takes compaction into effect (the next
|
|
1485
|
+
* eligible minor after November 20, 2026).
|
|
1486
|
+
*
|
|
1487
|
+
* A snapshot revision is compacted only when every one of these holds:
|
|
1488
|
+
*
|
|
1489
|
+
* - it is superseded: a later revision names it in `supersedes_reporting_revision_id`
|
|
1490
|
+
* (so a current revision, and every official revision, is never compacted);
|
|
1491
|
+
* - it is not the revision the current, unsuperseded revision names;
|
|
1492
|
+
* - its recovery grace has passed: `max(automatedRecoveryWindowSeconds, 3600)` seconds
|
|
1493
|
+
* since its successor's `created_at`;
|
|
1494
|
+
* - while its window is open (`statusRetentionDays` since its successor's `created_at`),
|
|
1495
|
+
* no caller's reporting receipt (any status) names it and no caller's current consumer
|
|
1496
|
+
* status statement of `received`, `unreadable` or `content_mismatch` names it. The
|
|
1497
|
+
* check spans every caller, so content any caller is entitled to is kept;
|
|
1498
|
+
* - it has rows (nothing to reclaim otherwise, and an empty exact read is still correct);
|
|
1499
|
+
* - no live obligation lease, no pending materialization of it, and no live change-feed
|
|
1500
|
+
* consumer that has not yet passed its change row (the holds retirement applies,
|
|
1501
|
+
* narrowed to this revision).
|
|
1502
|
+
*
|
|
1503
|
+
* Only row content is dropped: the revision header, its manifest and every metadata
|
|
1504
|
+
* listing stay. The row set is marked `pruned`, so an exact read answers
|
|
1505
|
+
* `ROWS_EXPIRED`, which the handlers surface as the same nondisclosing
|
|
1506
|
+
* `REFERENCE_NOT_FOUND` as an unknown ID. No change-feed row is recorded: the revision's
|
|
1507
|
+
* metadata is unchanged, and a sink still catching up holds compaction back.
|
|
1508
|
+
*
|
|
1509
|
+
* Compaction is resumable and runs in the same three phases as retirement, under the
|
|
1510
|
+
* account lock where eligibility is decided:
|
|
1511
|
+
*
|
|
1512
|
+
* 1. re-check eligibility and mark the row set `pruning`, so reads answer as expired and
|
|
1513
|
+
* no new revision can share its chunks;
|
|
1514
|
+
* 2. delete external objects at their recorded versions, skipping any object another
|
|
1515
|
+
* row set that is not being pruned still references (chunk sharing);
|
|
1516
|
+
* 3. delete the chunk manifest rows and any PostgreSQL chunk body nothing else
|
|
1517
|
+
* references, and mark the row set `pruned`.
|
|
1518
|
+
*
|
|
1519
|
+
* A crash after phase 1 leaves a `pruning` row set that the next pass resumes.
|
|
1520
|
+
*/
|
|
1521
|
+
async compactSupersededSnapshots(input) {
|
|
1522
|
+
this.assertCompactionSafeRowBindings();
|
|
1523
|
+
positiveInteger(input.statusRetentionDays, "statusRetentionDays");
|
|
1524
|
+
positiveInteger(input.automatedRecoveryWindowSeconds, "automatedRecoveryWindowSeconds");
|
|
1525
|
+
const limit = input.limit ?? 100;
|
|
1526
|
+
positiveInteger(limit, "limit");
|
|
1527
|
+
const holdDays = input.maxFeedHoldDays ?? 7;
|
|
1528
|
+
positiveInteger(holdDays, "maxFeedHoldDays");
|
|
1529
|
+
const tables = await this.optionalTables();
|
|
1530
|
+
if (!tables.has("adcp_reporting_row_sets")) return { compacted: 0, failed: [], failures: [] };
|
|
1531
|
+
const graceSeconds = Math.max(input.automatedRecoveryWindowSeconds, 3600);
|
|
1532
|
+
const eligible = `
|
|
1533
|
+
FROM adcp_reporting_revisions revision
|
|
1534
|
+
JOIN adcp_reporting_obligations obligation ON obligation.obligation_id = revision.obligation_id
|
|
1535
|
+
JOIN adcp_reporting_row_sets row_set
|
|
1536
|
+
ON row_set.row_set_id = revision.revision_id AND row_set.row_set_kind = 'revision'
|
|
1537
|
+
JOIN LATERAL (
|
|
1538
|
+
SELECT MAX(later.created_at) AS created_at FROM adcp_reporting_revisions later
|
|
1539
|
+
WHERE later.supersedes_revision_id = revision.revision_id
|
|
1540
|
+
) successor ON successor.created_at IS NOT NULL
|
|
1541
|
+
WHERE revision.finality = 'snapshot' AND revision.kind = 'snapshot'
|
|
1542
|
+
AND ($4::text IS NULL OR obligation.account_id = $4)
|
|
1543
|
+
AND ($5::text IS NULL OR revision.revision_id = $5)
|
|
1544
|
+
AND $3::integer > 0
|
|
1545
|
+
AND row_set.row_count > 0
|
|
1546
|
+
AND row_set.rows_state IN ('live', 'unavailable')
|
|
1547
|
+
AND successor.created_at + ($2::bigint * INTERVAL '1 second') <= clock_timestamp()
|
|
1548
|
+
AND NOT EXISTS (
|
|
1549
|
+
SELECT 1 FROM adcp_reporting_revisions current_revision
|
|
1550
|
+
WHERE current_revision.supersedes_revision_id = revision.revision_id
|
|
1551
|
+
AND NOT EXISTS (
|
|
1552
|
+
SELECT 1 FROM adcp_reporting_revisions next_revision
|
|
1553
|
+
WHERE next_revision.supersedes_revision_id = current_revision.revision_id
|
|
1554
|
+
)
|
|
1555
|
+
)
|
|
1556
|
+
AND (obligation.lease_expires_at IS NULL OR obligation.lease_expires_at <= clock_timestamp())
|
|
1557
|
+
AND NOT EXISTS (SELECT 1 FROM adcp_reporting_obligation_tombstones tombstone
|
|
1558
|
+
WHERE tombstone.obligation_id = obligation.obligation_id)
|
|
1559
|
+
AND (
|
|
1560
|
+
successor.created_at + ($1::integer * INTERVAL '1 day') <= clock_timestamp()
|
|
1561
|
+
OR (
|
|
1562
|
+
NOT EXISTS (
|
|
1563
|
+
SELECT 1 FROM adcp_reporting_consumer_statuses statement
|
|
1564
|
+
WHERE statement.revision_id = revision.revision_id AND statement.is_current
|
|
1565
|
+
AND COALESCE(statement.data->>'consumer_status', statement.data->'status'->>'consumer_status')
|
|
1566
|
+
IN ('received', 'unreadable', 'content_mismatch')
|
|
1567
|
+
)
|
|
1568
|
+
${tables.has("adcp_reporting_receipts") ? `AND NOT EXISTS (
|
|
1569
|
+
SELECT 1 FROM adcp_reporting_receipts receipt
|
|
1570
|
+
WHERE receipt.receipt_kind = 'revision' AND receipt.subject_id = revision.revision_id
|
|
1571
|
+
)` : ""}
|
|
1572
|
+
)
|
|
1573
|
+
)
|
|
1574
|
+
${tables.has("adcp_reporting_materializations") ? `AND NOT EXISTS (
|
|
1575
|
+
SELECT 1 FROM adcp_reporting_materializations hold
|
|
1576
|
+
WHERE hold.revision_id = revision.revision_id AND hold.status = 'pending'
|
|
1577
|
+
)` : ""}
|
|
1578
|
+
${tables.has("adcp_reporting_changes") && tables.has("adcp_reporting_feed_consumers") ? `AND NOT EXISTS (
|
|
1579
|
+
SELECT 1 FROM adcp_reporting_changes change
|
|
1580
|
+
JOIN adcp_reporting_feed_consumers consumer
|
|
1581
|
+
ON consumer.updated_at > clock_timestamp() - ($3::integer * INTERVAL '1 day')
|
|
1582
|
+
WHERE change.record_kind = 'revision' AND change.record_id = revision.revision_id
|
|
1583
|
+
AND ((consumer.account_id IS NULL
|
|
1584
|
+
AND (change.xid, change.seq) > (consumer.cursor_xid, consumer.cursor_seq))
|
|
1585
|
+
OR (consumer.account_id = change.account_id AND change.seq > consumer.cursor_seq)))` : ""}`;
|
|
1586
|
+
const baseValues = [input.statusRetentionDays, graceSeconds, holdDays, input.account_id ?? null];
|
|
1587
|
+
const candidates = await this.query(
|
|
1588
|
+
`SELECT revision_id, obligation_id, account_id FROM (
|
|
1589
|
+
SELECT row_set.row_set_id AS revision_id, row_set.obligation_id, row_set.account_id, 0 AS resume,
|
|
1590
|
+
row_set.recorded_at
|
|
1591
|
+
FROM adcp_reporting_row_sets row_set
|
|
1592
|
+
WHERE row_set.row_set_kind = 'revision' AND row_set.rows_state = 'pruning'
|
|
1593
|
+
AND ($4::text IS NULL OR row_set.account_id = $4)
|
|
1594
|
+
AND EXISTS (SELECT 1 FROM adcp_reporting_revisions revision
|
|
1595
|
+
WHERE revision.revision_id = row_set.row_set_id)
|
|
1596
|
+
AND NOT EXISTS (SELECT 1 FROM adcp_reporting_obligation_tombstones tombstone
|
|
1597
|
+
WHERE tombstone.obligation_id = row_set.obligation_id)
|
|
1598
|
+
UNION ALL
|
|
1599
|
+
SELECT revision.revision_id, revision.obligation_id, obligation.account_id, 1, row_set.recorded_at
|
|
1600
|
+
${eligible}
|
|
1601
|
+
) candidate
|
|
1602
|
+
ORDER BY resume, recorded_at, revision_id
|
|
1603
|
+
LIMIT $6`,
|
|
1604
|
+
[...baseValues, null, limit]
|
|
1605
|
+
);
|
|
1606
|
+
let compacted = 0;
|
|
1607
|
+
const failed = [];
|
|
1608
|
+
const failures = [];
|
|
1609
|
+
for (const candidate of candidates.rows) {
|
|
1610
|
+
input.signal?.throwIfAborted();
|
|
1611
|
+
try {
|
|
1612
|
+
const done = await this.compactSnapshot(
|
|
1613
|
+
candidate.revision_id,
|
|
1614
|
+
candidate.obligation_id,
|
|
1615
|
+
candidate.account_id,
|
|
1616
|
+
eligible,
|
|
1617
|
+
baseValues,
|
|
1618
|
+
input.signal
|
|
1619
|
+
);
|
|
1620
|
+
if (done) compacted += 1;
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
failed.push(candidate.revision_id);
|
|
1623
|
+
failures.push({
|
|
1624
|
+
reporting_obligation_id: candidate.obligation_id,
|
|
1625
|
+
reporting_revision_id: candidate.revision_id,
|
|
1626
|
+
cause: isReportingRowStoreError(error) ? error.code : error instanceof Error ? error.name : "Error"
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return { compacted, failed, failures };
|
|
1631
|
+
}
|
|
1632
|
+
async compactSnapshot(revisionId, obligationId, accountId, eligible, baseValues, signal) {
|
|
1633
|
+
const claimed = await this.transaction(
|
|
1634
|
+
async (client) => {
|
|
1635
|
+
const marked = await client.query(
|
|
1636
|
+
`UPDATE adcp_reporting_row_sets SET rows_state = 'pruning', rows_state_changed_at = clock_timestamp()
|
|
1637
|
+
WHERE row_set_id = $5 AND rows_state IN ('live', 'unavailable')
|
|
1638
|
+
AND row_set_id IN (SELECT revision.revision_id ${eligible})`,
|
|
1639
|
+
[...baseValues, revisionId]
|
|
1640
|
+
);
|
|
1641
|
+
if (marked.rowCount === 1) return true;
|
|
1642
|
+
const resumed = await client.query(
|
|
1643
|
+
`SELECT 1 FROM adcp_reporting_row_sets row_set
|
|
1644
|
+
WHERE row_set.row_set_id = $1 AND row_set.rows_state = 'pruning'
|
|
1645
|
+
AND NOT EXISTS (SELECT 1 FROM adcp_reporting_obligation_tombstones tombstone
|
|
1646
|
+
WHERE tombstone.obligation_id = row_set.obligation_id)`,
|
|
1647
|
+
[revisionId]
|
|
1648
|
+
);
|
|
1649
|
+
return resumed.rows.length === 1;
|
|
1650
|
+
},
|
|
1651
|
+
{ preBeginAdvisoryLock: accountLock(accountId) }
|
|
1652
|
+
);
|
|
1653
|
+
if (!claimed) return false;
|
|
1654
|
+
const binding = await this.query(
|
|
1655
|
+
`SELECT row_binding_id FROM adcp_reporting_row_sets WHERE row_set_id = $1`,
|
|
1656
|
+
[revisionId]
|
|
1657
|
+
);
|
|
1658
|
+
if (binding.rows.length === 0) return false;
|
|
1659
|
+
const objects = await this.query(
|
|
1660
|
+
`SELECT DISTINCT chunk.object_key, chunk.native_version
|
|
1661
|
+
FROM adcp_reporting_row_chunks chunk
|
|
1662
|
+
WHERE chunk.row_set_id = $1 AND chunk.object_key IS NOT NULL`,
|
|
1663
|
+
[revisionId]
|
|
1664
|
+
);
|
|
1665
|
+
if (objects.rows.length > 0) {
|
|
1666
|
+
await this.deleteUnreferencedObjects(
|
|
1667
|
+
binding.rows[0].row_binding_id,
|
|
1668
|
+
objects.rows.map((row) => ({ objectKey: row.object_key, nativeVersion: row.native_version })),
|
|
1669
|
+
{ rowSetId: revisionId },
|
|
1670
|
+
signal
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
await this.transaction(
|
|
1674
|
+
async (client) => {
|
|
1675
|
+
const dropped = await client.query(
|
|
1676
|
+
`DELETE FROM adcp_reporting_row_chunks WHERE row_set_id = $1 RETURNING sha256, object_key`,
|
|
1677
|
+
[revisionId]
|
|
1678
|
+
);
|
|
1679
|
+
const bodies = [...new Set(dropped.rows.filter((row) => row.object_key === null).map((row) => row.sha256))];
|
|
1680
|
+
if (bodies.length > 0) {
|
|
1681
|
+
await client.query(
|
|
1682
|
+
`DELETE FROM adcp_reporting_chunk_bodies body
|
|
1683
|
+
WHERE body.account_id = $1 AND body.obligation_id = $2 AND body.sha256 = ANY($3::text[])
|
|
1684
|
+
AND NOT EXISTS (
|
|
1685
|
+
SELECT 1 FROM adcp_reporting_row_chunks other
|
|
1686
|
+
WHERE other.account_id = body.account_id AND other.obligation_id = body.obligation_id
|
|
1687
|
+
AND other.sha256 = body.sha256 AND other.object_key IS NULL
|
|
1688
|
+
)`,
|
|
1689
|
+
[accountId, obligationId, bodies]
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
await client.query(
|
|
1693
|
+
`UPDATE adcp_reporting_row_sets SET rows_state = 'pruned', rows_state_changed_at = clock_timestamp()
|
|
1694
|
+
WHERE row_set_id = $1 AND rows_state = 'pruning'`,
|
|
1695
|
+
[revisionId]
|
|
1696
|
+
);
|
|
1697
|
+
},
|
|
1698
|
+
{ preBeginAdvisoryLock: accountLock(accountId) }
|
|
1699
|
+
);
|
|
1700
|
+
return true;
|
|
1701
|
+
}
|
|
1463
1702
|
/**
|
|
1464
1703
|
* Resolve abandoned object uploads (spec §4.3). An expired `open` write
|
|
1465
1704
|
* intent whose ledger commit never happened is moved to `sweeping` under
|
|
@@ -1501,35 +1740,85 @@ ${managedDueArm} )
|
|
|
1501
1740
|
{ preBeginAdvisoryLock: accountLock(intent.account_id) }
|
|
1502
1741
|
);
|
|
1503
1742
|
if (!claimed) continue;
|
|
1504
|
-
const
|
|
1505
|
-
for (const object of claimed) {
|
|
1506
|
-
const referenced = await this.query(
|
|
1507
|
-
`SELECT 1 FROM adcp_reporting_row_chunks WHERE object_key = $1 AND native_version = $2 LIMIT 1`,
|
|
1508
|
-
[object.objectKey, object.nativeVersion]
|
|
1509
|
-
);
|
|
1510
|
-
if (referenced.rows.length === 0) orphaned.push(object);
|
|
1511
|
-
}
|
|
1512
|
-
if (orphaned.length > 0) await this.rowStorage.deleteObjects(intent.row_binding_id, orphaned, input.signal);
|
|
1743
|
+
const deleted = await this.deleteUnreferencedObjects(intent.row_binding_id, claimed, {}, input.signal);
|
|
1513
1744
|
await this.query(
|
|
1514
1745
|
`DELETE FROM adcp_reporting_row_write_intents
|
|
1515
1746
|
WHERE row_binding_id = $1 AND row_set_id = $2 AND content_sha256 = $3 AND state = 'sweeping'`,
|
|
1516
1747
|
key
|
|
1517
1748
|
);
|
|
1518
1749
|
swept += 1;
|
|
1519
|
-
objectsDeleted +=
|
|
1750
|
+
objectsDeleted += deleted;
|
|
1520
1751
|
} catch {
|
|
1521
1752
|
failed += 1;
|
|
1522
1753
|
}
|
|
1523
1754
|
}
|
|
1524
1755
|
return { swept, objectsDeleted, failed };
|
|
1525
1756
|
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Delete external row objects, skipping any object another row set still references.
|
|
1759
|
+
*
|
|
1760
|
+
* A custom `keyTemplate` need only include `{content_sha256}` and `{chunk_index}`, so row sets
|
|
1761
|
+
* from different obligations or accounts with identical chunk content can share one object (a
|
|
1762
|
+
* create-only write adopts an existing object). An object is therefore deleted only when no
|
|
1763
|
+
* chunk of a live (or unavailable) row set outside this operation references the same binding,
|
|
1764
|
+
* key and native version, and no other open write intent created it. Row sets already `pruning`
|
|
1765
|
+
* or `pruned` do not count: their own operation deletes the object, and counting them would let
|
|
1766
|
+
* two concurrent retirements each skip the object and leak it. `scope` names what this
|
|
1767
|
+
* operation is deleting: an obligation (retirement) or a row set (compaction).
|
|
1768
|
+
*
|
|
1769
|
+
* The check and the provider delete run in one transaction under a per-object advisory lock.
|
|
1770
|
+
* A commit that adopts an existing object takes the same lock before it inserts its chunk rows
|
|
1771
|
+
* and re-verifies the object under it (`writePreparedRowSet`), and holds it until commit. So
|
|
1772
|
+
* either the delete wins the lock and the adopting commit then fails cleanly, or the commit wins
|
|
1773
|
+
* and the delete sees its chunk rows and skips. A skipped object stays for the row set that owns
|
|
1774
|
+
* it: that row set's own retirement, compaction or sweep deletes it, because by then nothing
|
|
1775
|
+
* else references it. Locks are taken one object at a time, never while waiting on the account
|
|
1776
|
+
* lock, so commits (account lock, then object locks) cannot deadlock with these deletes.
|
|
1777
|
+
*/
|
|
1778
|
+
async deleteUnreferencedObjects(bindingId, objects, scope, signal) {
|
|
1779
|
+
let deleted = 0;
|
|
1780
|
+
for (const object of objects) {
|
|
1781
|
+
signal?.throwIfAborted();
|
|
1782
|
+
const removed = await this.transaction(async (client) => {
|
|
1783
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [
|
|
1784
|
+
objectLock(bindingId, object.objectKey)
|
|
1785
|
+
]);
|
|
1786
|
+
const referenced = await client.query(
|
|
1787
|
+
`(SELECT 1 FROM adcp_reporting_row_chunks chunk
|
|
1788
|
+
JOIN adcp_reporting_row_sets row_set ON row_set.row_set_id = chunk.row_set_id
|
|
1789
|
+
WHERE row_set.row_binding_id = $1 AND chunk.object_key = $2 AND chunk.native_version = $3
|
|
1790
|
+
AND row_set.rows_state IN ('live', 'unavailable')
|
|
1791
|
+
AND ($4::text IS NULL OR chunk.obligation_id <> $4)
|
|
1792
|
+
AND ($5::text IS NULL OR chunk.row_set_id <> $5)
|
|
1793
|
+
LIMIT 1)
|
|
1794
|
+
UNION ALL
|
|
1795
|
+
(SELECT 1 FROM adcp_reporting_row_write_intents intent
|
|
1796
|
+
WHERE intent.row_binding_id = $1 AND intent.state = 'open' AND intent.created_objects @> $6::jsonb
|
|
1797
|
+
LIMIT 1)`,
|
|
1798
|
+
[
|
|
1799
|
+
bindingId,
|
|
1800
|
+
object.objectKey,
|
|
1801
|
+
object.nativeVersion,
|
|
1802
|
+
scope.obligationId ?? null,
|
|
1803
|
+
scope.rowSetId ?? null,
|
|
1804
|
+
JSON.stringify([{ objectKey: object.objectKey, nativeVersion: object.nativeVersion }])
|
|
1805
|
+
]
|
|
1806
|
+
);
|
|
1807
|
+
if (referenced.rows.length > 0) return false;
|
|
1808
|
+
await this.rowStorage.deleteObjects(bindingId, [object], signal);
|
|
1809
|
+
return true;
|
|
1810
|
+
});
|
|
1811
|
+
if (removed) deleted += 1;
|
|
1812
|
+
}
|
|
1813
|
+
return deleted;
|
|
1814
|
+
}
|
|
1526
1815
|
async optionalTables() {
|
|
1527
1816
|
const result = await this.query(
|
|
1528
1817
|
`SELECT table_name AS name FROM information_schema.tables
|
|
1529
1818
|
WHERE table_schema = current_schema()
|
|
1530
1819
|
AND table_name IN ('adcp_reporting_row_sets', 'adcp_reporting_row_write_intents',
|
|
1531
1820
|
'adcp_reporting_materializations', 'adcp_reporting_changes',
|
|
1532
|
-
'adcp_reporting_feed_consumers')`
|
|
1821
|
+
'adcp_reporting_feed_consumers', 'adcp_reporting_receipts')`
|
|
1533
1822
|
);
|
|
1534
1823
|
return new Set(result.rows.map((row) => row.name));
|
|
1535
1824
|
}
|
|
@@ -1633,6 +1922,15 @@ ${managedDueArm} )
|
|
|
1633
1922
|
throw new ReportingRowStoreError("STATE_UNAVAILABLE", "the row upload intent was taken over or swept");
|
|
1634
1923
|
}
|
|
1635
1924
|
}
|
|
1925
|
+
if (!upload.sharedFrom) {
|
|
1926
|
+
const adopted = upload.locators.filter((locator) => !locator.created).sort((left, right) => left.objectKey < right.objectKey ? -1 : left.objectKey > right.objectKey ? 1 : 0);
|
|
1927
|
+
for (const locator of adopted) {
|
|
1928
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [
|
|
1929
|
+
objectLock(prepared.bindingId, locator.objectKey)
|
|
1930
|
+
]);
|
|
1931
|
+
}
|
|
1932
|
+
if (adopted.length > 0) await this.rowStorage.assertObjectsPresent(prepared.bindingId, adopted);
|
|
1933
|
+
}
|
|
1636
1934
|
await this.rowStorage.writeInTransaction(client, prepared, upload.locators, upload.sharedFrom);
|
|
1637
1935
|
}
|
|
1638
1936
|
/** Attach verified rows to a revision document whose rows live in row storage. */
|
|
@@ -3821,6 +4119,9 @@ function rfc3339Microseconds(expression) {
|
|
|
3821
4119
|
function accountLock(accountId) {
|
|
3822
4120
|
return `adcp-reporting-account:${accountId}`;
|
|
3823
4121
|
}
|
|
4122
|
+
function objectLock(bindingId, objectKey) {
|
|
4123
|
+
return `adcp-reporting-object:${bindingId}:${objectKey}`;
|
|
4124
|
+
}
|
|
3824
4125
|
async function resolveStoredFinalityBaseline(transaction, obligationId) {
|
|
3825
4126
|
const latest = await transaction.query(
|
|
3826
4127
|
`SELECT transition_id, data->>'finality' AS finality
|
|
@@ -152,6 +152,18 @@ export declare class ReportingRowStorageV1 {
|
|
|
152
152
|
constructor(pool: QueryableV1, options?: ReportingRowStorageOptionsV1);
|
|
153
153
|
/** Deadline budget a writer must still hold on its lease before uploading. */
|
|
154
154
|
get writeDeadlineMilliseconds(): number;
|
|
155
|
+
/**
|
|
156
|
+
* Refuse a configuration in which compaction could delete an object a concurrent commit is
|
|
157
|
+
* adopting. An object binding whose key template omits `{revision_key}` can map two revisions
|
|
158
|
+
* with identical chunk content to one object key, and a create-only write adopts an existing
|
|
159
|
+
* object with identical bytes. Compaction deleting that object at the same moment would leave
|
|
160
|
+
* the newer revision without its rows. The default template includes `{revision_key}`.
|
|
161
|
+
*
|
|
162
|
+
* Since the reference-checked, per-object-locked delete this is no longer the only guard: every
|
|
163
|
+
* delete path now skips an object another row set still references. It is kept as defense in
|
|
164
|
+
* depth for compaction, the one path that deletes content while the obligation stays live.
|
|
165
|
+
*/
|
|
166
|
+
assertCompactionSafeKeyTemplates(): void;
|
|
155
167
|
/** Probe the schema and every provider, and register (or verify) every configured binding. Idempotent. */
|
|
156
168
|
ready(): Promise<void>;
|
|
157
169
|
/** Encode rows, prove the chunks reproduce the committed binding, select a binding and plan objects. */
|
|
@@ -199,6 +211,17 @@ export declare class ReportingRowStorageV1 {
|
|
|
199
211
|
objectKey: string;
|
|
200
212
|
nativeVersion: string;
|
|
201
213
|
}[], signal?: AbortSignal): Promise<void>;
|
|
214
|
+
/**
|
|
215
|
+
* Prove each object is still present at its recorded version with the recorded bytes.
|
|
216
|
+
* A commit that adopted an object another row set created calls this under the object's
|
|
217
|
+
* lock, so a concurrent delete either finished first (this throws, the commit fails and
|
|
218
|
+
* the writer re-uploads) or sees the commit's chunk reference and skips the delete.
|
|
219
|
+
*/
|
|
220
|
+
assertObjectsPresent(bindingId: string, objects: readonly {
|
|
221
|
+
objectKey: string;
|
|
222
|
+
nativeVersion: string;
|
|
223
|
+
physicalSha256: string;
|
|
224
|
+
}[], signal?: AbortSignal): Promise<void>;
|
|
202
225
|
/**
|
|
203
226
|
* A live row set in the same obligation and binding whose rows are
|
|
204
227
|
* byte-identical (same manifest digest), with its chunk locations. Sharing
|
|
@@ -152,6 +152,18 @@ export declare class ReportingRowStorageV1 {
|
|
|
152
152
|
constructor(pool: QueryableV1, options?: ReportingRowStorageOptionsV1);
|
|
153
153
|
/** Deadline budget a writer must still hold on its lease before uploading. */
|
|
154
154
|
get writeDeadlineMilliseconds(): number;
|
|
155
|
+
/**
|
|
156
|
+
* Refuse a configuration in which compaction could delete an object a concurrent commit is
|
|
157
|
+
* adopting. An object binding whose key template omits `{revision_key}` can map two revisions
|
|
158
|
+
* with identical chunk content to one object key, and a create-only write adopts an existing
|
|
159
|
+
* object with identical bytes. Compaction deleting that object at the same moment would leave
|
|
160
|
+
* the newer revision without its rows. The default template includes `{revision_key}`.
|
|
161
|
+
*
|
|
162
|
+
* Since the reference-checked, per-object-locked delete this is no longer the only guard: every
|
|
163
|
+
* delete path now skips an object another row set still references. It is kept as defense in
|
|
164
|
+
* depth for compaction, the one path that deletes content while the obligation stays live.
|
|
165
|
+
*/
|
|
166
|
+
assertCompactionSafeKeyTemplates(): void;
|
|
155
167
|
/** Probe the schema and every provider, and register (or verify) every configured binding. Idempotent. */
|
|
156
168
|
ready(): Promise<void>;
|
|
157
169
|
/** Encode rows, prove the chunks reproduce the committed binding, select a binding and plan objects. */
|
|
@@ -199,6 +211,17 @@ export declare class ReportingRowStorageV1 {
|
|
|
199
211
|
objectKey: string;
|
|
200
212
|
nativeVersion: string;
|
|
201
213
|
}[], signal?: AbortSignal): Promise<void>;
|
|
214
|
+
/**
|
|
215
|
+
* Prove each object is still present at its recorded version with the recorded bytes.
|
|
216
|
+
* A commit that adopted an object another row set created calls this under the object's
|
|
217
|
+
* lock, so a concurrent delete either finished first (this throws, the commit fails and
|
|
218
|
+
* the writer re-uploads) or sees the commit's chunk reference and skips the delete.
|
|
219
|
+
*/
|
|
220
|
+
assertObjectsPresent(bindingId: string, objects: readonly {
|
|
221
|
+
objectKey: string;
|
|
222
|
+
nativeVersion: string;
|
|
223
|
+
physicalSha256: string;
|
|
224
|
+
}[], signal?: AbortSignal): Promise<void>;
|
|
202
225
|
/**
|
|
203
226
|
* A live row set in the same obligation and binding whose rows are
|
|
204
227
|
* byte-identical (same manifest digest), with its chunk locations. Sharing
|
|
@@ -81,6 +81,28 @@ class ReportingRowStorageV1 {
|
|
|
81
81
|
get writeDeadlineMilliseconds() {
|
|
82
82
|
return this.writeDeadline;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Refuse a configuration in which compaction could delete an object a concurrent commit is
|
|
86
|
+
* adopting. An object binding whose key template omits `{revision_key}` can map two revisions
|
|
87
|
+
* with identical chunk content to one object key, and a create-only write adopts an existing
|
|
88
|
+
* object with identical bytes. Compaction deleting that object at the same moment would leave
|
|
89
|
+
* the newer revision without its rows. The default template includes `{revision_key}`.
|
|
90
|
+
*
|
|
91
|
+
* Since the reference-checked, per-object-locked delete this is no longer the only guard: every
|
|
92
|
+
* delete path now skips an object another row set still references. It is kept as defense in
|
|
93
|
+
* depth for compaction, the one path that deletes content while the obligation stays live.
|
|
94
|
+
*/
|
|
95
|
+
assertCompactionSafeKeyTemplates() {
|
|
96
|
+
const unsafe = Object.entries(this.bindings).filter(
|
|
97
|
+
([, binding]) => binding.kind === "object" && !(binding.keyTemplate ?? import_row_storage_object.REPORTING_ROW_DEFAULT_KEY_TEMPLATE).includes("{revision_key}")
|
|
98
|
+
).map(([id]) => id);
|
|
99
|
+
if (unsafe.length > 0) {
|
|
100
|
+
throw new import_row_storage_errors.ReportingRowStoreError(
|
|
101
|
+
"INVALID_INPUT",
|
|
102
|
+
`snapshot compaction requires every object row binding's keyTemplate to include {revision_key}; binding(s) ${unsafe.join(", ")} omit it, so two revisions with identical content could share an object that compaction deletes while a concurrent commit adopts it`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
84
106
|
/** Probe the schema and every provider, and register (or verify) every configured binding. Idempotent. */
|
|
85
107
|
ready() {
|
|
86
108
|
this.readied ??= this.register().catch((error) => {
|
|
@@ -461,6 +483,40 @@ class ReportingRowStorageV1 {
|
|
|
461
483
|
);
|
|
462
484
|
}
|
|
463
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Prove each object is still present at its recorded version with the recorded bytes.
|
|
488
|
+
* A commit that adopted an object another row set created calls this under the object's
|
|
489
|
+
* lock, so a concurrent delete either finished first (this throws, the commit fails and
|
|
490
|
+
* the writer re-uploads) or sees the commit's chunk reference and skips the delete.
|
|
491
|
+
*/
|
|
492
|
+
async assertObjectsPresent(bindingId, objects, signal) {
|
|
493
|
+
await this.ready();
|
|
494
|
+
const binding = this.bindings[bindingId];
|
|
495
|
+
if (!binding || binding.kind !== "object") {
|
|
496
|
+
throw new import_row_storage_errors.ReportingRowStoreError("STATE_UNAVAILABLE", `row binding ${bindingId} is not configured for objects`);
|
|
497
|
+
}
|
|
498
|
+
const provider = this.providers[binding.provider];
|
|
499
|
+
const context = { signal: deadlineSignal(this.writeDeadline, signal) };
|
|
500
|
+
for (const object of objects) {
|
|
501
|
+
const bytes = await this.provider(
|
|
502
|
+
() => provider.get(
|
|
503
|
+
{
|
|
504
|
+
...providerLocation(binding),
|
|
505
|
+
key: object.objectKey,
|
|
506
|
+
nativeVersion: object.nativeVersion,
|
|
507
|
+
maxBytes: MAX_ADOPTED_OBJECT_BYTES
|
|
508
|
+
},
|
|
509
|
+
context
|
|
510
|
+
)
|
|
511
|
+
);
|
|
512
|
+
if (!bytes || sha256Hex(bytes) !== object.physicalSha256) {
|
|
513
|
+
throw new import_row_storage_errors.ReportingRowStoreError(
|
|
514
|
+
"STATE_UNAVAILABLE",
|
|
515
|
+
"an adopted row object was deleted or replaced before its commit; retry the upload"
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
464
520
|
/**
|
|
465
521
|
* A live row set in the same obligation and binding whose rows are
|
|
466
522
|
* byte-identical (same manifest digest), with its chunk locations. Sharing
|
|
@@ -70,6 +70,28 @@ class ReportingRowStorageV1 {
|
|
|
70
70
|
get writeDeadlineMilliseconds() {
|
|
71
71
|
return this.writeDeadline;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Refuse a configuration in which compaction could delete an object a concurrent commit is
|
|
75
|
+
* adopting. An object binding whose key template omits `{revision_key}` can map two revisions
|
|
76
|
+
* with identical chunk content to one object key, and a create-only write adopts an existing
|
|
77
|
+
* object with identical bytes. Compaction deleting that object at the same moment would leave
|
|
78
|
+
* the newer revision without its rows. The default template includes `{revision_key}`.
|
|
79
|
+
*
|
|
80
|
+
* Since the reference-checked, per-object-locked delete this is no longer the only guard: every
|
|
81
|
+
* delete path now skips an object another row set still references. It is kept as defense in
|
|
82
|
+
* depth for compaction, the one path that deletes content while the obligation stays live.
|
|
83
|
+
*/
|
|
84
|
+
assertCompactionSafeKeyTemplates() {
|
|
85
|
+
const unsafe = Object.entries(this.bindings).filter(
|
|
86
|
+
([, binding]) => binding.kind === "object" && !(binding.keyTemplate ?? REPORTING_ROW_DEFAULT_KEY_TEMPLATE).includes("{revision_key}")
|
|
87
|
+
).map(([id]) => id);
|
|
88
|
+
if (unsafe.length > 0) {
|
|
89
|
+
throw new ReportingRowStoreError(
|
|
90
|
+
"INVALID_INPUT",
|
|
91
|
+
`snapshot compaction requires every object row binding's keyTemplate to include {revision_key}; binding(s) ${unsafe.join(", ")} omit it, so two revisions with identical content could share an object that compaction deletes while a concurrent commit adopts it`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
73
95
|
/** Probe the schema and every provider, and register (or verify) every configured binding. Idempotent. */
|
|
74
96
|
ready() {
|
|
75
97
|
this.readied ??= this.register().catch((error) => {
|
|
@@ -450,6 +472,40 @@ class ReportingRowStorageV1 {
|
|
|
450
472
|
);
|
|
451
473
|
}
|
|
452
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* Prove each object is still present at its recorded version with the recorded bytes.
|
|
477
|
+
* A commit that adopted an object another row set created calls this under the object's
|
|
478
|
+
* lock, so a concurrent delete either finished first (this throws, the commit fails and
|
|
479
|
+
* the writer re-uploads) or sees the commit's chunk reference and skips the delete.
|
|
480
|
+
*/
|
|
481
|
+
async assertObjectsPresent(bindingId, objects, signal) {
|
|
482
|
+
await this.ready();
|
|
483
|
+
const binding = this.bindings[bindingId];
|
|
484
|
+
if (!binding || binding.kind !== "object") {
|
|
485
|
+
throw new ReportingRowStoreError("STATE_UNAVAILABLE", `row binding ${bindingId} is not configured for objects`);
|
|
486
|
+
}
|
|
487
|
+
const provider = this.providers[binding.provider];
|
|
488
|
+
const context = { signal: deadlineSignal(this.writeDeadline, signal) };
|
|
489
|
+
for (const object of objects) {
|
|
490
|
+
const bytes = await this.provider(
|
|
491
|
+
() => provider.get(
|
|
492
|
+
{
|
|
493
|
+
...providerLocation(binding),
|
|
494
|
+
key: object.objectKey,
|
|
495
|
+
nativeVersion: object.nativeVersion,
|
|
496
|
+
maxBytes: MAX_ADOPTED_OBJECT_BYTES
|
|
497
|
+
},
|
|
498
|
+
context
|
|
499
|
+
)
|
|
500
|
+
);
|
|
501
|
+
if (!bytes || sha256Hex(bytes) !== object.physicalSha256) {
|
|
502
|
+
throw new ReportingRowStoreError(
|
|
503
|
+
"STATE_UNAVAILABLE",
|
|
504
|
+
"an adopted row object was deleted or replaced before its commit; retry the upload"
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
453
509
|
/**
|
|
454
510
|
* A live row set in the same obligation and binding whose rows are
|
|
455
511
|
* byte-identical (same manifest digest), with its chunk locations. Sharing
|
|
@@ -1009,6 +1009,32 @@ export interface ReportingProducerV1 {
|
|
|
1009
1009
|
export type ReportingSourceWithReaderV1 = ReportingSourceExecutorV1 & ReportingSourceStagedObjectReaderV1;
|
|
1010
1010
|
export type ReportingStatusHandlerV1 = (request: AdcpToolMap['get_reporting_status']['params'], context: HandlerContext<unknown>) => Promise<GetReportingStatusResponse>;
|
|
1011
1011
|
export type ReportingDeliveryHandlerV1 = (request: AdcpToolMap['get_media_buy_delivery']['params'], context: HandlerContext<unknown>) => Promise<AdcpToolMap['get_media_buy_delivery']['result']>;
|
|
1012
|
+
/**
|
|
1013
|
+
* Exact-revision reader for revisions the SDK ledger does not own, typically a
|
|
1014
|
+
* seller's pre-cutover ledger tables. It receives the same request and context
|
|
1015
|
+
* as `get_media_buy_delivery` and returns the protocol response, or
|
|
1016
|
+
* `null`/`undefined` when it has no such revision.
|
|
1017
|
+
*
|
|
1018
|
+
* Authorization is the reader's responsibility. The SDK has already resolved
|
|
1019
|
+
* the account (`context.account`) and cannot tell whether a legacy revision
|
|
1020
|
+
* belongs to it, so the reader MUST scope the lookup by that account and return
|
|
1021
|
+
* `null` for a revision that belongs to another one. Never return a response
|
|
1022
|
+
* for a revision the resolved account cannot read.
|
|
1023
|
+
*/
|
|
1024
|
+
export type ReportingLegacyRevisionReaderV1 = (request: AdcpToolMap['get_media_buy_delivery']['params'], context: HandlerContext<unknown>) => Promise<AdcpToolMap['get_media_buy_delivery']['result'] | null | undefined>;
|
|
1025
|
+
export interface CreateReportingDeliveryHandlerOptionsV1 {
|
|
1026
|
+
/**
|
|
1027
|
+
* Consulted only when the SDK ledger has no such revision for the resolved
|
|
1028
|
+
* account (unknown id, or an id owned by a different account). Never
|
|
1029
|
+
* consulted for an SDK-owned revision that is expired, has unavailable or
|
|
1030
|
+
* corrupt rows, or carries an unusable cursor, nor for capacity or other
|
|
1031
|
+
* errors: falling back there could serve different content under an id the
|
|
1032
|
+
* SDK owns. A `null`/`undefined` result is the same nondisclosing
|
|
1033
|
+
* `REFERENCE_NOT_FOUND` the SDK returns itself; an error the reader throws
|
|
1034
|
+
* propagates unchanged.
|
|
1035
|
+
*/
|
|
1036
|
+
legacyRevisionReader?: ReportingLegacyRevisionReaderV1;
|
|
1037
|
+
}
|
|
1012
1038
|
export interface CreateReportingProducerOptionsV1 {
|
|
1013
1039
|
store: ReportingLedgerStore;
|
|
1014
1040
|
source: ReportingSourceWithReaderV1;
|