@adcp/sdk 15.6.0 → 15.7.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.
@@ -410,6 +410,12 @@ function createReportingStatusHandler(store, options) {
410
410
  };
411
411
  }
412
412
  const REPORTING_STATUS_REVISION_VIEW_MAX_ROWS = 1e4;
413
+ function reportingRevisionNotFound() {
414
+ return new import_async_outcome.AdcpError("REFERENCE_NOT_FOUND", {
415
+ message: "Reporting revision is unavailable",
416
+ field: "reporting_revision_id"
417
+ });
418
+ }
413
419
  function createReportingDeliveryHandler(store) {
414
420
  const activeReadsByAccount = /* @__PURE__ */ new Map();
415
421
  return async (request, context) => {
@@ -428,14 +434,15 @@ function createReportingDeliveryHandler(store) {
428
434
  try {
429
435
  const raw = request;
430
436
  if (typeof raw.reporting_revision_id !== "string" || !raw.reporting_revision_id) {
431
- throw new Error("Ledger delivery reads require reporting_revision_id");
437
+ throw new import_async_outcome.AdcpError("VALIDATION_ERROR", {
438
+ message: "Ledger delivery reads require reporting_revision_id",
439
+ field: "reporting_revision_id"
440
+ });
432
441
  }
433
442
  const revision = await (0, import_revision_metadata.getRevisionMetadataFromStore)(store, raw.reporting_revision_id, accountId);
434
- if (!revision) throw new Error("Reporting revision is unavailable");
443
+ if (!revision) throw reportingRevisionNotFound();
435
444
  const obligation = await store.getObligation(revision.reporting_obligation_id);
436
- if (!obligation || obligation.account.account_id !== accountId) {
437
- throw new Error("Reporting revision is unavailable");
438
- }
445
+ if (!obligation || obligation.account.account_id !== accountId) throw reportingRevisionNotFound();
439
446
  const pagination = isRecord(raw.pagination) ? raw.pagination : {};
440
447
  const maxResults = typeof pagination.max_results === "number" && Number.isFinite(pagination.max_results) ? Math.max(1, Math.min(500, Math.trunc(pagination.max_results))) : 100;
441
448
  const totalCount = revision.binding.rowCount;
@@ -448,7 +455,7 @@ function createReportingDeliveryHandler(store) {
448
455
  }
449
456
  offset = cursor.offset;
450
457
  } catch {
451
- throw new Error("Reporting delivery cursor is invalid");
458
+ throw reportingRevisionNotFound();
452
459
  }
453
460
  }
454
461
  let reportingRows = [];
@@ -464,14 +471,19 @@ function createReportingDeliveryHandler(store) {
464
471
  });
465
472
  } catch (error) {
466
473
  if ((0, import_row_storage.isReportingRowStoreError)(error)) {
467
- if (error.code === "ROWS_EXPIRED") throw new Error("Reporting revision is unavailable");
474
+ if (error.code === "ROWS_EXPIRED") throw reportingRevisionNotFound();
468
475
  throw new import_async_outcome.AdcpError("SERVICE_UNAVAILABLE", {
469
476
  message: "Reporting revision rows are temporarily unavailable"
470
477
  });
471
478
  }
472
479
  throw error;
473
480
  }
474
- if (!page || page.total !== totalCount) throw new Error("Reporting revision is unavailable");
481
+ if (!page) throw reportingRevisionNotFound();
482
+ if (page.total !== totalCount) {
483
+ throw new import_async_outcome.AdcpError("SERVICE_UNAVAILABLE", {
484
+ message: "Reporting revision rows are temporarily unavailable"
485
+ });
486
+ }
475
487
  reportingRows = page.rows;
476
488
  }
477
489
  const nextOffset = offset + reportingRows.length;
@@ -393,6 +393,12 @@ function createReportingStatusHandler(store, options) {
393
393
  };
394
394
  }
395
395
  const REPORTING_STATUS_REVISION_VIEW_MAX_ROWS = 1e4;
396
+ function reportingRevisionNotFound() {
397
+ return new AdcpError("REFERENCE_NOT_FOUND", {
398
+ message: "Reporting revision is unavailable",
399
+ field: "reporting_revision_id"
400
+ });
401
+ }
396
402
  function createReportingDeliveryHandler(store) {
397
403
  const activeReadsByAccount = /* @__PURE__ */ new Map();
398
404
  return async (request, context) => {
@@ -411,14 +417,15 @@ function createReportingDeliveryHandler(store) {
411
417
  try {
412
418
  const raw = request;
413
419
  if (typeof raw.reporting_revision_id !== "string" || !raw.reporting_revision_id) {
414
- throw new Error("Ledger delivery reads require reporting_revision_id");
420
+ throw new AdcpError("VALIDATION_ERROR", {
421
+ message: "Ledger delivery reads require reporting_revision_id",
422
+ field: "reporting_revision_id"
423
+ });
415
424
  }
416
425
  const revision = await getRevisionMetadataFromStore(store, raw.reporting_revision_id, accountId);
417
- if (!revision) throw new Error("Reporting revision is unavailable");
426
+ if (!revision) throw reportingRevisionNotFound();
418
427
  const obligation = await store.getObligation(revision.reporting_obligation_id);
419
- if (!obligation || obligation.account.account_id !== accountId) {
420
- throw new Error("Reporting revision is unavailable");
421
- }
428
+ if (!obligation || obligation.account.account_id !== accountId) throw reportingRevisionNotFound();
422
429
  const pagination = isRecord(raw.pagination) ? raw.pagination : {};
423
430
  const maxResults = typeof pagination.max_results === "number" && Number.isFinite(pagination.max_results) ? Math.max(1, Math.min(500, Math.trunc(pagination.max_results))) : 100;
424
431
  const totalCount = revision.binding.rowCount;
@@ -431,7 +438,7 @@ function createReportingDeliveryHandler(store) {
431
438
  }
432
439
  offset = cursor.offset;
433
440
  } catch {
434
- throw new Error("Reporting delivery cursor is invalid");
441
+ throw reportingRevisionNotFound();
435
442
  }
436
443
  }
437
444
  let reportingRows = [];
@@ -447,14 +454,19 @@ function createReportingDeliveryHandler(store) {
447
454
  });
448
455
  } catch (error) {
449
456
  if (isReportingRowStoreError(error)) {
450
- if (error.code === "ROWS_EXPIRED") throw new Error("Reporting revision is unavailable");
457
+ if (error.code === "ROWS_EXPIRED") throw reportingRevisionNotFound();
451
458
  throw new AdcpError("SERVICE_UNAVAILABLE", {
452
459
  message: "Reporting revision rows are temporarily unavailable"
453
460
  });
454
461
  }
455
462
  throw error;
456
463
  }
457
- if (!page || page.total !== totalCount) throw new Error("Reporting revision is unavailable");
464
+ if (!page) throw reportingRevisionNotFound();
465
+ if (page.total !== totalCount) {
466
+ throw new AdcpError("SERVICE_UNAVAILABLE", {
467
+ message: "Reporting revision rows are temporarily unavailable"
468
+ });
469
+ }
458
470
  reportingRows = page.rows;
459
471
  }
460
472
  const nextOffset = offset + reportingRows.length;
@@ -137,6 +137,17 @@ export declare class PostgresReportingLedgerStore implements ReportingLedgerStor
137
137
  private readonly rowStorageWrites;
138
138
  private readonly changeFeed;
139
139
  constructor(pool: ReportingPgPool, options?: PostgresReportingLedgerStoreOptions);
140
+ /** Whether this store writes the host change feed (`changeFeed: true`). */
141
+ get changeFeedEnabled(): boolean;
142
+ /**
143
+ * Delete expired cursor snapshots and changes checkpoints, a bounded batch
144
+ * at a time. The store-bound form of `sweepExpiredReportingLedgerState`, for
145
+ * hosts that hold the store but not the pool.
146
+ */
147
+ sweepExpiredState(limit?: number): Promise<{
148
+ snapshotsDeleted: number;
149
+ checkpointsDeleted: number;
150
+ }>;
140
151
  /** Append one host change-feed row inside the caller's account-locked transaction. */
141
152
  private appendChange;
142
153
  /** Probe and register row storage when enabled. Safe to call repeatedly. */
@@ -137,6 +137,17 @@ export declare class PostgresReportingLedgerStore implements ReportingLedgerStor
137
137
  private readonly rowStorageWrites;
138
138
  private readonly changeFeed;
139
139
  constructor(pool: ReportingPgPool, options?: PostgresReportingLedgerStoreOptions);
140
+ /** Whether this store writes the host change feed (`changeFeed: true`). */
141
+ get changeFeedEnabled(): boolean;
142
+ /**
143
+ * Delete expired cursor snapshots and changes checkpoints, a bounded batch
144
+ * at a time. The store-bound form of `sweepExpiredReportingLedgerState`, for
145
+ * hosts that hold the store but not the pool.
146
+ */
147
+ sweepExpiredState(limit?: number): Promise<{
148
+ snapshotsDeleted: number;
149
+ checkpointsDeleted: number;
150
+ }>;
140
151
  /** Append one host change-feed row inside the caller's account-locked transaction. */
141
152
  private appendChange;
142
153
  /** Probe and register row storage when enabled. Safe to call repeatedly. */
@@ -424,6 +424,18 @@ class PostgresReportingLedgerStore {
424
424
  rowStorage;
425
425
  rowStorageWrites;
426
426
  changeFeed;
427
+ /** Whether this store writes the host change feed (`changeFeed: true`). */
428
+ get changeFeedEnabled() {
429
+ return this.changeFeed;
430
+ }
431
+ /**
432
+ * Delete expired cursor snapshots and changes checkpoints, a bounded batch
433
+ * at a time. The store-bound form of `sweepExpiredReportingLedgerState`, for
434
+ * hosts that hold the store but not the pool.
435
+ */
436
+ async sweepExpiredState(limit = 1e3) {
437
+ return sweepExpiredReportingLedgerState(this.pool, limit);
438
+ }
427
439
  /** Append one host change-feed row inside the caller's account-locked transaction. */
428
440
  async appendChange(client, kind, accountId, recordId, obligationId) {
429
441
  if (!this.changeFeed) return;
@@ -423,6 +423,18 @@ class PostgresReportingLedgerStore {
423
423
  rowStorage;
424
424
  rowStorageWrites;
425
425
  changeFeed;
426
+ /** Whether this store writes the host change feed (`changeFeed: true`). */
427
+ get changeFeedEnabled() {
428
+ return this.changeFeed;
429
+ }
430
+ /**
431
+ * Delete expired cursor snapshots and changes checkpoints, a bounded batch
432
+ * at a time. The store-bound form of `sweepExpiredReportingLedgerState`, for
433
+ * hosts that hold the store but not the pool.
434
+ */
435
+ async sweepExpiredState(limit = 1e3) {
436
+ return sweepExpiredReportingLedgerState(this.pool, limit);
437
+ }
426
438
  /** Append one host change-feed row inside the caller's account-locked transaction. */
427
439
  async appendChange(client, kind, accountId, recordId, obligationId) {
428
440
  if (!this.changeFeed) return;
@@ -6,6 +6,7 @@ import type { ReportingDeliveryCapabilities, ReportingDeliveryOffering } from '.
6
6
  import { type CreatePostgresPersistentNotificationRuntimeOptions, type PostgresPersistentNotificationRuntime } from '../../server/notification-subscriptions/index.mjs';
7
7
  import { REPORTING_LEDGER_MIGRATION, PostgresReportingLedgerStore, PostgresReportingManagedDeliveryStore, type CreateReportingProducerOptionsV1, type ReportingConsumerMismatchEscalationV1, type ReportingLedgerConfigurationV1, type ReportingLedgerStore, type ReportingProducerContactV1, type ReportingProducerV1, type ReportingManagedDeliveryAdapterV1, type ReportingManagedDeliveryRuntimeV1, type PostgresReportingManagedDeliveryStoreOptions, type PostgresReportingNotificationActivityOptions, type PostgresReportingNotificationActivityRuntime, type ReportingPgPool, type ReportingRowStorageOptionsV1, type ReportingSourceWithReaderV1 } from '../ledger/index.mjs';
8
8
  import { type PostgresReportingWebhookActivityV1 } from '../webhook-activity.mjs';
9
+ import { type ReportingChangeFeedMaintenanceOptionsV1, type ReportingLedgerMaintenanceTaskResultV1, type ReportingRetentionOptionsV1 } from './maintenance.mjs';
9
10
  import type { InlineReportingReplayRetentionV1 } from '../source/index.mjs';
10
11
  import { type InlineReportingDeliveryFetchV1, type ReportingSourceOfferingV1 } from '../source/index.mjs';
11
12
  /** The intentionally small provider boundary: one bounded slice fetch plus immutable metadata. */
@@ -149,6 +150,29 @@ export interface CreateReliableReportingServiceOptionsV1<TCtxMeta = Record<strin
149
150
  /** Ledger metadata retention commitment advertised to buyers. */
150
151
  statusRetentionDays: number;
151
152
  subscribers?: CreateReportingProducerOptionsV1['subscribers'];
153
+ /**
154
+ * Opt in to scheduled ledger maintenance for a Core-only deployment on a
155
+ * `PostgresReportingLedgerStore`. `start()` runs it at most once per
156
+ * `intervalMilliseconds`; `runMaintenance()` runs it on demand for a host
157
+ * with its own scheduler. The production service does this itself and does
158
+ * not take this option.
159
+ *
160
+ * Always swept: expired cursor snapshots and checkpoints, and abandoned row
161
+ * uploads. `retention` and `changeFeed` have the same shape and meaning as
162
+ * on `createPostgresReliableReportingProductionService`. Retention uses
163
+ * this service's `statusRetentionDays`, so it can never retire a period
164
+ * earlier than the retention advertised to buyers. `changeFeed` requires the
165
+ * store to be built with `changeFeed: true`.
166
+ */
167
+ maintenance?: ReliableReportingMaintenanceOptionsV1;
168
+ }
169
+ export interface ReliableReportingMaintenanceOptionsV1 {
170
+ /** Minimum time between scheduled maintenance passes. Defaults to 300000 (5 minutes). */
171
+ intervalMilliseconds?: number;
172
+ /** Period-aligned retention; see the production service's `retention`. */
173
+ retention?: ReportingRetentionOptionsV1;
174
+ /** Change-feed pruning and consumer hold; see the production service's `changeFeed`. */
175
+ changeFeed?: ReportingChangeFeedMaintenanceOptionsV1;
152
176
  }
153
177
  export interface ReliableReportingSetupV1 {
154
178
  readonly component: 'reliable-reporting-core';
@@ -179,6 +203,14 @@ export interface ReliableReportingServiceV1<TCtxMeta = Record<string, unknown>>
179
203
  notReady: number;
180
204
  failed: number;
181
205
  }>;
206
+ /**
207
+ * Run one ledger-maintenance pass now and return each task's outcome. For
208
+ * hosts that run their own scheduler instead of `start()`. A task failure is
209
+ * reported in its result, not thrown. Requires the `maintenance` option.
210
+ */
211
+ runMaintenance(options?: {
212
+ signal?: AbortSignal;
213
+ }): Promise<ReportingLedgerMaintenanceTaskResultV1[]>;
182
214
  start(options: ReliableReportingSchedulerOptionsV1): void;
183
215
  stop(): Promise<void>;
184
216
  }
@@ -188,7 +220,7 @@ export interface ReliableReportingServiceV1<TCtxMeta = Record<string, unknown>>
188
220
  */
189
221
  export declare function createReliableReportingService<TCtxMeta = Record<string, unknown>>(options: CreateReliableReportingServiceOptionsV1<TCtxMeta>): ReliableReportingServiceV1<TCtxMeta>;
190
222
  type ProductionNotificationOptions = Omit<CreatePostgresPersistentNotificationRuntimeOptions, 'db' | 'publisherScope' | 'checkpointDeliveryAttempt' | 'supportedAccountEventTypes'>;
191
- export type CreatePostgresReliableReportingProductionServiceOptionsV1<TCtxMeta = Record<string, unknown>> = Omit<CreateReliableReportingServiceOptionsV1<TCtxMeta>, 'store' | 'subscribers'> & {
223
+ export type CreatePostgresReliableReportingProductionServiceOptionsV1<TCtxMeta = Record<string, unknown>> = Omit<CreateReliableReportingServiceOptionsV1<TCtxMeta>, 'store' | 'subscribers' | 'maintenance'> & {
192
224
  db: ReportingPgPool;
193
225
  /** Stable, non-secret deployment namespace shared by reporting workers. */
194
226
  namespace: string;
@@ -211,11 +243,7 @@ export type CreatePostgresReliableReportingProductionServiceOptionsV1<TCtxMeta =
211
243
  * period is also held until every live registered feed consumer (updated
212
244
  * within `changeFeed.maxFeedHoldDays`, default 7) has read its changes.
213
245
  */
214
- retention?: {
215
- enabled: true;
216
- recordRetentionDays?: number;
217
- limit?: number;
218
- };
246
+ retention?: ReportingRetentionOptionsV1;
219
247
  /**
220
248
  * Maintain the host change feed (`changesAfter`, `getCurrentRevision`,
221
249
  * `listCurrentRevisions` on `stores.core`). Adds
@@ -223,10 +251,7 @@ export type CreatePostgresReliableReportingProductionServiceOptionsV1<TCtxMeta =
223
251
  * changes older than `changeRetentionDays` (default 30) unless a registered
224
252
  * consumer updated within `maxFeedHoldDays` (default 7) still needs them.
225
253
  */
226
- changeFeed?: boolean | {
227
- changeRetentionDays?: number;
228
- maxFeedHoldDays?: number;
229
- };
254
+ changeFeed?: ReportingChangeFeedMaintenanceOptionsV1;
230
255
  notifications: ProductionNotificationOptions;
231
256
  managedDelivery: {
232
257
  adapter: ReportingManagedDeliveryAdapterV1;
@@ -304,14 +329,4 @@ export interface PostgresReliableReportingProductionServiceV1<TCtxMeta = Record<
304
329
  */
305
330
  export declare function createPostgresReliableReportingProductionService<TCtxMeta = Record<string, unknown>>(options: CreatePostgresReliableReportingProductionServiceOptionsV1<TCtxMeta>): Promise<PostgresReliableReportingProductionServiceV1<TCtxMeta>>;
306
331
  export * from './conformance.mjs';
307
- /**
308
- * A scheduled maintenance pass completed but could not process some items
309
- * (for example, a retention period whose objects could not be deleted). The
310
- * pass resumes them next time; repeated reports need operator attention.
311
- */
312
- export declare class ReportingMaintenancePartialFailureError extends Error {
313
- readonly task: string;
314
- readonly failedCount: number;
315
- readonly failedIds: readonly string[];
316
- constructor(task: string, failedCount: number, failedIds: readonly string[]);
317
- }
332
+ export { ReportingMaintenancePartialFailureError, type ReportingChangeFeedMaintenanceOptionsV1, type ReportingLedgerMaintenanceStoreV1, type ReportingLedgerMaintenanceTaskNameV1, type ReportingLedgerMaintenanceTaskResultV1, type ReportingRetentionOptionsV1, } from './maintenance.mjs';
@@ -6,6 +6,7 @@ import type { ReportingDeliveryCapabilities, ReportingDeliveryOffering } from '.
6
6
  import { type CreatePostgresPersistentNotificationRuntimeOptions, type PostgresPersistentNotificationRuntime } from '../../server/notification-subscriptions';
7
7
  import { REPORTING_LEDGER_MIGRATION, PostgresReportingLedgerStore, PostgresReportingManagedDeliveryStore, type CreateReportingProducerOptionsV1, type ReportingConsumerMismatchEscalationV1, type ReportingLedgerConfigurationV1, type ReportingLedgerStore, type ReportingProducerContactV1, type ReportingProducerV1, type ReportingManagedDeliveryAdapterV1, type ReportingManagedDeliveryRuntimeV1, type PostgresReportingManagedDeliveryStoreOptions, type PostgresReportingNotificationActivityOptions, type PostgresReportingNotificationActivityRuntime, type ReportingPgPool, type ReportingRowStorageOptionsV1, type ReportingSourceWithReaderV1 } from '../ledger';
8
8
  import { type PostgresReportingWebhookActivityV1 } from '../webhook-activity';
9
+ import { type ReportingChangeFeedMaintenanceOptionsV1, type ReportingLedgerMaintenanceTaskResultV1, type ReportingRetentionOptionsV1 } from './maintenance';
9
10
  import type { InlineReportingReplayRetentionV1 } from '../source';
10
11
  import { type InlineReportingDeliveryFetchV1, type ReportingSourceOfferingV1 } from '../source';
11
12
  /** The intentionally small provider boundary: one bounded slice fetch plus immutable metadata. */
@@ -149,6 +150,29 @@ export interface CreateReliableReportingServiceOptionsV1<TCtxMeta = Record<strin
149
150
  /** Ledger metadata retention commitment advertised to buyers. */
150
151
  statusRetentionDays: number;
151
152
  subscribers?: CreateReportingProducerOptionsV1['subscribers'];
153
+ /**
154
+ * Opt in to scheduled ledger maintenance for a Core-only deployment on a
155
+ * `PostgresReportingLedgerStore`. `start()` runs it at most once per
156
+ * `intervalMilliseconds`; `runMaintenance()` runs it on demand for a host
157
+ * with its own scheduler. The production service does this itself and does
158
+ * not take this option.
159
+ *
160
+ * Always swept: expired cursor snapshots and checkpoints, and abandoned row
161
+ * uploads. `retention` and `changeFeed` have the same shape and meaning as
162
+ * on `createPostgresReliableReportingProductionService`. Retention uses
163
+ * this service's `statusRetentionDays`, so it can never retire a period
164
+ * earlier than the retention advertised to buyers. `changeFeed` requires the
165
+ * store to be built with `changeFeed: true`.
166
+ */
167
+ maintenance?: ReliableReportingMaintenanceOptionsV1;
168
+ }
169
+ export interface ReliableReportingMaintenanceOptionsV1 {
170
+ /** Minimum time between scheduled maintenance passes. Defaults to 300000 (5 minutes). */
171
+ intervalMilliseconds?: number;
172
+ /** Period-aligned retention; see the production service's `retention`. */
173
+ retention?: ReportingRetentionOptionsV1;
174
+ /** Change-feed pruning and consumer hold; see the production service's `changeFeed`. */
175
+ changeFeed?: ReportingChangeFeedMaintenanceOptionsV1;
152
176
  }
153
177
  export interface ReliableReportingSetupV1 {
154
178
  readonly component: 'reliable-reporting-core';
@@ -179,6 +203,14 @@ export interface ReliableReportingServiceV1<TCtxMeta = Record<string, unknown>>
179
203
  notReady: number;
180
204
  failed: number;
181
205
  }>;
206
+ /**
207
+ * Run one ledger-maintenance pass now and return each task's outcome. For
208
+ * hosts that run their own scheduler instead of `start()`. A task failure is
209
+ * reported in its result, not thrown. Requires the `maintenance` option.
210
+ */
211
+ runMaintenance(options?: {
212
+ signal?: AbortSignal;
213
+ }): Promise<ReportingLedgerMaintenanceTaskResultV1[]>;
182
214
  start(options: ReliableReportingSchedulerOptionsV1): void;
183
215
  stop(): Promise<void>;
184
216
  }
@@ -188,7 +220,7 @@ export interface ReliableReportingServiceV1<TCtxMeta = Record<string, unknown>>
188
220
  */
189
221
  export declare function createReliableReportingService<TCtxMeta = Record<string, unknown>>(options: CreateReliableReportingServiceOptionsV1<TCtxMeta>): ReliableReportingServiceV1<TCtxMeta>;
190
222
  type ProductionNotificationOptions = Omit<CreatePostgresPersistentNotificationRuntimeOptions, 'db' | 'publisherScope' | 'checkpointDeliveryAttempt' | 'supportedAccountEventTypes'>;
191
- export type CreatePostgresReliableReportingProductionServiceOptionsV1<TCtxMeta = Record<string, unknown>> = Omit<CreateReliableReportingServiceOptionsV1<TCtxMeta>, 'store' | 'subscribers'> & {
223
+ export type CreatePostgresReliableReportingProductionServiceOptionsV1<TCtxMeta = Record<string, unknown>> = Omit<CreateReliableReportingServiceOptionsV1<TCtxMeta>, 'store' | 'subscribers' | 'maintenance'> & {
192
224
  db: ReportingPgPool;
193
225
  /** Stable, non-secret deployment namespace shared by reporting workers. */
194
226
  namespace: string;
@@ -211,11 +243,7 @@ export type CreatePostgresReliableReportingProductionServiceOptionsV1<TCtxMeta =
211
243
  * period is also held until every live registered feed consumer (updated
212
244
  * within `changeFeed.maxFeedHoldDays`, default 7) has read its changes.
213
245
  */
214
- retention?: {
215
- enabled: true;
216
- recordRetentionDays?: number;
217
- limit?: number;
218
- };
246
+ retention?: ReportingRetentionOptionsV1;
219
247
  /**
220
248
  * Maintain the host change feed (`changesAfter`, `getCurrentRevision`,
221
249
  * `listCurrentRevisions` on `stores.core`). Adds
@@ -223,10 +251,7 @@ export type CreatePostgresReliableReportingProductionServiceOptionsV1<TCtxMeta =
223
251
  * changes older than `changeRetentionDays` (default 30) unless a registered
224
252
  * consumer updated within `maxFeedHoldDays` (default 7) still needs them.
225
253
  */
226
- changeFeed?: boolean | {
227
- changeRetentionDays?: number;
228
- maxFeedHoldDays?: number;
229
- };
254
+ changeFeed?: ReportingChangeFeedMaintenanceOptionsV1;
230
255
  notifications: ProductionNotificationOptions;
231
256
  managedDelivery: {
232
257
  adapter: ReportingManagedDeliveryAdapterV1;
@@ -304,14 +329,4 @@ export interface PostgresReliableReportingProductionServiceV1<TCtxMeta = Record<
304
329
  */
305
330
  export declare function createPostgresReliableReportingProductionService<TCtxMeta = Record<string, unknown>>(options: CreatePostgresReliableReportingProductionServiceOptionsV1<TCtxMeta>): Promise<PostgresReliableReportingProductionServiceV1<TCtxMeta>>;
306
331
  export * from './conformance';
307
- /**
308
- * A scheduled maintenance pass completed but could not process some items
309
- * (for example, a retention period whose objects could not be deleted). The
310
- * pass resumes them next time; repeated reports need operator attention.
311
- */
312
- export declare class ReportingMaintenancePartialFailureError extends Error {
313
- readonly task: string;
314
- readonly failedCount: number;
315
- readonly failedIds: readonly string[];
316
- constructor(task: string, failedCount: number, failedIds: readonly string[]);
317
- }
332
+ export { ReportingMaintenancePartialFailureError, type ReportingChangeFeedMaintenanceOptionsV1, type ReportingLedgerMaintenanceStoreV1, type ReportingLedgerMaintenanceTaskNameV1, type ReportingLedgerMaintenanceTaskResultV1, type ReportingRetentionOptionsV1, } from './maintenance';
@@ -19,7 +19,7 @@ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "defau
19
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
20
  var service_exports = {};
21
21
  __export(service_exports, {
22
- ReportingMaintenancePartialFailureError: () => ReportingMaintenancePartialFailureError,
22
+ ReportingMaintenancePartialFailureError: () => import_maintenance2.ReportingMaintenancePartialFailureError,
23
23
  createPostgresReliableReportingProductionService: () => createPostgresReliableReportingProductionService,
24
24
  createReliableReportingService: () => createReliableReportingService
25
25
  });
@@ -31,11 +31,14 @@ var import_schemas = require('../../types/schemas.generated.js');
31
31
  var import_notification_subscriptions = require('../../server/notification-subscriptions/index.js');
32
32
  var import_ledger = require('../ledger/index.js');
33
33
  var import_webhook_activity = require('../webhook-activity.js');
34
+ var import_maintenance = require('./maintenance.js');
34
35
  var import_schedule = require('../ledger/schedule.js');
35
36
  var import_source = require('../source/index.js');
36
37
  __reExport(service_exports, require('./conformance.js'), module.exports);
38
+ var import_maintenance2 = require('./maintenance.js');
37
39
  const ADAPTER_SCOPE_KEY = "_adcp_reporting_adapter";
38
40
  const PRODUCTION_MANAGED_COMPOSITION = /* @__PURE__ */ Symbol("production-managed-reporting-composition");
41
+ const DEFAULT_MAINTENANCE_INTERVAL_MILLISECONDS = 3e5;
39
42
  function createReliableReportingService(options) {
40
43
  const allowManagedDelivery = options[PRODUCTION_MANAGED_COMPOSITION] === true;
41
44
  const adapterEntries = Object.entries(options.adapters);
@@ -97,6 +100,42 @@ function createReliableReportingService(options) {
97
100
  if (hasConsumerStatus) assertConsumerStatusStore(options.store);
98
101
  const retentionDays = options.statusRetentionDays;
99
102
  positiveInteger(retentionDays, "statusRetentionDays");
103
+ const maintenance = options.maintenance;
104
+ let maintenanceStore;
105
+ let maintenanceIntervalMilliseconds = DEFAULT_MAINTENANCE_INTERVAL_MILLISECONDS;
106
+ if (maintenance !== void 0) {
107
+ if (maintenance === null || typeof maintenance !== "object") {
108
+ throw new TypeError("maintenance must be an options object");
109
+ }
110
+ const candidate = options.store;
111
+ for (const method of ["retireExpiredPeriods", "pruneChanges", "sweepRowWriteIntents"]) {
112
+ if (typeof candidate[method] !== "function") {
113
+ throw new TypeError(
114
+ `maintenance requires a PostgresReportingLedgerStore (or a store with ${method}); the configured store cannot run ledger maintenance`
115
+ );
116
+ }
117
+ }
118
+ maintenanceStore = candidate;
119
+ if (maintenance.changeFeed && maintenanceStore.changeFeedEnabled === false) {
120
+ throw new TypeError(
121
+ "maintenance.changeFeed requires the store to be built with changeFeed: true; otherwise pruning silently does nothing"
122
+ );
123
+ }
124
+ if (maintenance.intervalMilliseconds !== void 0) {
125
+ positiveInteger(maintenance.intervalMilliseconds, "maintenance.intervalMilliseconds");
126
+ maintenanceIntervalMilliseconds = maintenance.intervalMilliseconds;
127
+ }
128
+ (0, import_maintenance.assertReportingLedgerMaintenanceOptions)(maintenance, retentionDays);
129
+ }
130
+ const runLedgerMaintenance = (signal, report) => (0, import_maintenance.runReportingLedgerMaintenance)({
131
+ store: maintenanceStore,
132
+ ...typeof maintenanceStore.sweepExpiredState === "function" ? { sweepSnapshots: (limit) => maintenanceStore.sweepExpiredState(limit) } : {},
133
+ statusRetentionDays: retentionDays,
134
+ retention: maintenance.retention,
135
+ changeFeed: maintenance.changeFeed,
136
+ signal,
137
+ ...report ? { report } : {}
138
+ });
100
139
  const capabilities = deepFreeze({
101
140
  supported: true,
102
141
  reliable_reporting_version: "1.0",
@@ -269,12 +308,26 @@ function createReliableReportingService(options) {
269
308
  });
270
309
  return { planned: planned.length, ...result };
271
310
  },
311
+ async runMaintenance(maintenanceOptions) {
312
+ if (maintenance === void 0) {
313
+ throw new TypeError("runMaintenance requires the maintenance option");
314
+ }
315
+ maintenanceOptions?.signal?.throwIfAborted();
316
+ return runLedgerMaintenance(maintenanceOptions?.signal);
317
+ },
272
318
  start(scheduler) {
273
319
  if (schedulerPromise) throw new Error("Reliable reporting scheduler is already running");
274
320
  positiveInteger(scheduler.intervalMilliseconds, "intervalMilliseconds");
275
321
  schedulerAbort = new AbortController();
276
322
  const signal = schedulerAbort.signal;
277
- schedulerPromise = schedulerLoop(service, scheduler, signal, options.store).finally(() => {
323
+ const scheduledMaintenance = maintenance === void 0 ? void 0 : {
324
+ intervalMilliseconds: maintenanceIntervalMilliseconds,
325
+ run: () => runLedgerMaintenance(
326
+ signal,
327
+ (error) => reportSchedulerError(scheduler, signal, error, { phase: "maintenance" })
328
+ )
329
+ };
330
+ schedulerPromise = schedulerLoop(service, scheduler, signal, options.store, scheduledMaintenance).finally(() => {
278
331
  schedulerPromise = void 0;
279
332
  schedulerAbort = void 0;
280
333
  });
@@ -506,53 +559,15 @@ async function createPostgresReliableReportingProductionService(options) {
506
559
  for (const result of pruneResults) {
507
560
  if (result.status === "rejected" && !signal.aborted) await reportAuxiliaryError(scheduler, result.reason);
508
561
  }
509
- const ledgerMaintenance = [
510
- ["snapshot sweep", (0, import_ledger.sweepExpiredReportingLedgerState)(options.db, 1e3)],
511
- ["row upload sweep", coreStore.sweepRowWriteIntents({ limit: 100, signal })],
512
- ...options.changeFeed ? [
513
- [
514
- "change-feed pruning",
515
- coreStore.pruneChanges(
516
- typeof options.changeFeed === "object" ? {
517
- ...options.changeFeed.changeRetentionDays !== void 0 ? { changeRetentionDays: options.changeFeed.changeRetentionDays } : {},
518
- ...options.changeFeed.maxFeedHoldDays !== void 0 ? { maxFeedHoldDays: options.changeFeed.maxFeedHoldDays } : {}
519
- } : {}
520
- )
521
- ]
522
- ] : [],
523
- ...options.retention?.enabled ? [
524
- [
525
- "retention",
526
- coreStore.retireExpiredPeriods({
527
- statusRetentionDays: options.statusRetentionDays,
528
- ...options.retention.recordRetentionDays !== void 0 ? { recordRetentionDays: options.retention.recordRetentionDays } : {},
529
- ...typeof options.changeFeed === "object" && options.changeFeed.maxFeedHoldDays !== void 0 ? { maxFeedHoldDays: options.changeFeed.maxFeedHoldDays } : {},
530
- limit: options.retention.limit ?? 100,
531
- signal
532
- })
533
- ]
534
- ] : []
535
- ];
536
- const maintenanceResults = await Promise.allSettled(ledgerMaintenance.map(([, task]) => task));
537
- for (const [index, result] of maintenanceResults.entries()) {
538
- const name = ledgerMaintenance[index][0];
539
- if (result.status === "rejected") {
540
- if (!signal.aborted) await reportAuxiliaryError(scheduler, result.reason);
541
- continue;
542
- }
543
- const value = result.value;
544
- const failed = Array.isArray(value?.failed) ? value.failed.length : typeof value?.failed === "number" ? value.failed : 0;
545
- if (failed > 0 && !signal.aborted) {
546
- await reportAuxiliaryError(
547
- scheduler,
548
- new ReportingMaintenancePartialFailureError(
549
- name,
550
- failed,
551
- value?.failures?.length ? value.failures.map((entry) => `${entry.reporting_obligation_id} (${entry.cause})`) : Array.isArray(value?.failed) ? value.failed : []
552
- )
553
- );
554
- }
555
- }
562
+ await (0, import_maintenance.runReportingLedgerMaintenance)({
563
+ store: coreStore,
564
+ sweepSnapshots: (limit) => (0, import_ledger.sweepExpiredReportingLedgerState)(options.db, limit),
565
+ statusRetentionDays: options.statusRetentionDays,
566
+ retention: options.retention,
567
+ changeFeed: options.changeFeed,
568
+ signal,
569
+ report: (error) => reportAuxiliaryError(scheduler, error)
570
+ });
556
571
  const accountIds = rotate(await deploymentWideAccountIds(coreStore), auxiliaryRotation);
557
572
  auxiliaryRotation += 1;
558
573
  for (const accountId of accountIds) {
@@ -678,8 +693,9 @@ function routeSource(sources, sourceScope) {
678
693
  if (!source) throw new TypeError("Reporting sourceScope has no installed adapter route");
679
694
  return source;
680
695
  }
681
- async function schedulerLoop(service, options, signal, store) {
696
+ async function schedulerLoop(service, options, signal, store, maintenance) {
682
697
  let rotation = 0;
698
+ let lastMaintenanceAt;
683
699
  while (!signal.aborted) {
684
700
  let accountIds = [];
685
701
  try {
@@ -716,6 +732,14 @@ async function schedulerLoop(service, options, signal, store) {
716
732
  await reportSchedulerError(options, signal, error, { phase: "cycle", accountId });
717
733
  }
718
734
  }
735
+ if (maintenance && !signal.aborted && (lastMaintenanceAt === void 0 || Date.now() - lastMaintenanceAt >= maintenance.intervalMilliseconds)) {
736
+ lastMaintenanceAt = Date.now();
737
+ try {
738
+ await maintenance.run();
739
+ } catch (error) {
740
+ await reportSchedulerError(options, signal, error, { phase: "maintenance" });
741
+ }
742
+ }
719
743
  if (!signal.aborted) await abortableDelay(options.intervalMilliseconds, signal);
720
744
  }
721
745
  }
@@ -732,7 +756,7 @@ async function reportSchedulerError(options, signal, error, site) {
732
756
  warnSchedulerFailure(options, site, error);
733
757
  }
734
758
  function warnSchedulerFailure(options, site, error, observerError) {
735
- const where = site.phase === "roster" ? "could not resolve its account roster, so this pass ran no tenants" : site.accountId === void 0 ? "deployment-wide cycle failed; the scheduler continues" : `cycle failed for account ${site.accountId}; later tenants continue`;
759
+ const where = site.phase === "roster" ? "could not resolve its account roster, so this pass ran no tenants" : site.phase === "maintenance" ? "ledger maintenance failed; the scheduler continues" : site.accountId === void 0 ? "deployment-wide cycle failed; the scheduler continues" : `cycle failed for account ${site.accountId}; later tenants continue`;
736
760
  const observed = observerError === void 0 ? "" : ` (the configured onError also threw: ${safeMessage(observerError)})`;
737
761
  const message = `[adcp/reporting] scheduler ${where}: ${safeMessage(error)}${observed}`;
738
762
  const logger = options.logger;
@@ -1200,20 +1224,6 @@ function deepFreeze(value) {
1200
1224
  }
1201
1225
  return value;
1202
1226
  }
1203
- class ReportingMaintenancePartialFailureError extends Error {
1204
- constructor(task, failedCount, failedIds) {
1205
- super(
1206
- `Reporting ${task} could not process ${failedCount} item(s)${failedIds.length ? `: ${failedIds.slice(0, 20).join(", ")}` : ""}`
1207
- );
1208
- this.task = task;
1209
- this.failedCount = failedCount;
1210
- this.failedIds = failedIds;
1211
- this.name = "ReportingMaintenancePartialFailureError";
1212
- }
1213
- task;
1214
- failedCount;
1215
- failedIds;
1216
- }
1217
1227
  // Annotate the CommonJS export names for ESM import in node:
1218
1228
  0 && (module.exports = {
1219
1229
  ReportingMaintenancePartialFailureError,