deepline 0.3.141 → 0.3.142

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.
Files changed (25) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/sdk/src/types.ts +65 -2
  3. package/dist/bundling-sources/shared_libs/observability/dlq.ts +11 -0
  4. package/dist/bundling-sources/shared_libs/observability/queue-health.ts +111 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/bettercontact-batching.ts +34 -8
  6. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +271 -44
  7. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +25 -6
  8. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +132 -8
  9. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +3 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +49 -54
  11. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +14 -7
  12. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +118 -29
  13. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-queue-health.ts +183 -36
  14. package/dist/bundling-sources/shared_libs/product-notifications/events.ts +4 -1
  15. package/dist/cli/index.js +242 -12
  16. package/dist/cli/index.mjs +242 -12
  17. package/dist/index.d.mts +54 -2
  18. package/dist/index.d.ts +54 -2
  19. package/dist/index.js +1 -1
  20. package/dist/index.mjs +1 -1
  21. package/dist/release.d.mts +1 -1
  22. package/dist/release.d.ts +1 -1
  23. package/dist/release.js +1 -1
  24. package/dist/release.mjs +1 -1
  25. package/package.json +1 -1
@@ -8,6 +8,7 @@ import {
8
8
  type QueueDefinition,
9
9
  type QueueHealth,
10
10
  type QueueObservation,
11
+ type QueueRetirementCandidate,
11
12
  } from '../../observability/queue-health';
12
13
  import {
13
14
  dlqInventoryFromQueueObservation,
@@ -73,6 +74,8 @@ export type RuntimeQueueHealthProjection = {
73
74
  observation: QueueObservation;
74
75
  /** Shared DLQ contract consumed by operators and the admin CLI. */
75
76
  dlq: DlqInventory;
77
+ /** Internal-only bounded sample used to join operator dispositions. */
78
+ operatorRetirementCandidates?: QueueRetirementCandidate[];
76
79
  health: QueueHealth;
77
80
  depth: RuntimeQueueDepth;
78
81
  oldestAgeMs: RuntimeQueueOldestAge;
@@ -117,6 +120,8 @@ export type RuntimeQueueHealthOptions = {
117
120
  /** The caller could not enumerate every live lane within the bounded cap. */
118
121
  absurdQueueInventoryTruncated?: boolean;
119
122
  maxAbsurdQueues?: number;
123
+ /** Include bounded attention-row identities for an app-side disposition join. */
124
+ includeRetirementCandidates?: boolean;
120
125
  };
121
126
 
122
127
  export type RuntimeQueueHealthThresholds = Required<
@@ -151,6 +156,8 @@ type CountRow = {
151
156
  dead_lettered_available?: boolean;
152
157
  flow_available?: boolean;
153
158
  flow_unavailable_reason?: string | null;
159
+ blocked_retirement_candidates?: unknown;
160
+ dead_lettered_retirement_candidates?: unknown;
154
161
  };
155
162
 
156
163
  function quoteIdentifier(value: string, label: string): string {
@@ -184,6 +191,59 @@ function ageValue(value: string | number | null | undefined): number | null {
184
191
  return Math.floor(parsed);
185
192
  }
186
193
 
194
+ function parseJsonArray(value: unknown): Record<string, unknown>[] {
195
+ let parsed = value;
196
+ if (typeof value === 'string') {
197
+ try {
198
+ parsed = JSON.parse(value) as unknown;
199
+ } catch {
200
+ return [];
201
+ }
202
+ }
203
+ return Array.isArray(parsed)
204
+ ? parsed.filter(
205
+ (row): row is Record<string, unknown> =>
206
+ typeof row === 'object' && row !== null && !Array.isArray(row),
207
+ )
208
+ : [];
209
+ }
210
+
211
+ function operatorRetirementCandidates(
212
+ row: CountRow,
213
+ kind: RuntimeQueueHealthProjection['kind'],
214
+ ): QueueRetirementCandidate[] {
215
+ const candidates: QueueRetirementCandidate[] = [];
216
+ const append = (
217
+ value: unknown,
218
+ bucket: QueueRetirementCandidate['bucket'],
219
+ ) => {
220
+ for (const candidate of parseJsonArray(value)) {
221
+ let itemId: string | undefined;
222
+ if (kind === 'sandbox_cleanup') {
223
+ const provider = candidate.provider;
224
+ const sandboxId = candidate.sandboxId;
225
+ const routingDomainKey = candidate.routingDomainKey;
226
+ if (
227
+ typeof provider === 'string' &&
228
+ typeof sandboxId === 'string' &&
229
+ typeof routingDomainKey === 'string'
230
+ ) {
231
+ itemId = encodeURIComponent(
232
+ JSON.stringify([provider, sandboxId, routingDomainKey]),
233
+ );
234
+ }
235
+ } else if (typeof candidate.itemId === 'string') {
236
+ itemId = candidate.itemId;
237
+ }
238
+ const ageMs = numberValue(candidate.ageMs as string | number | undefined);
239
+ if (itemId !== undefined) candidates.push({ itemId, bucket, ageMs });
240
+ }
241
+ };
242
+ append(row.blocked_retirement_candidates, 'blocked');
243
+ append(row.dead_lettered_retirement_candidates, 'deadLettered');
244
+ return candidates;
245
+ }
246
+
187
247
  function optionsWithDefaults(
188
248
  input: RuntimeQueueHealthOptions = {},
189
249
  ): RuntimeQueueHealthThresholds {
@@ -265,6 +325,7 @@ function projectionFromRow(input: {
265
325
  observedAt: Date;
266
326
  environment?: string;
267
327
  options: RuntimeQueueHealthThresholds;
328
+ includeRetirementCandidates?: boolean;
268
329
  }): RuntimeQueueHealthProjection {
269
330
  const depth: RuntimeQueueDepth = {
270
331
  ready: numberValue(input.row.ready),
@@ -351,7 +412,9 @@ function projectionFromRow(input: {
351
412
  blocked: measureDepth(depth.blocked, input.row.blocked_truncated),
352
413
  deadLettered: deadLetteredAvailable
353
414
  ? measureDepth(depth.deadLettered, input.row.dead_lettered_truncated)
354
- : notApplicable<number>('This workset has blocked quarantine, not a DLQ.'),
415
+ : notApplicable<number>(
416
+ 'This workset has blocked quarantine, not a DLQ.',
417
+ ),
355
418
  },
356
419
  oldestAgeMs: {
357
420
  ready: measureAge(oldestAgeMs.ready, input.row.ready_truncated),
@@ -363,7 +426,9 @@ function projectionFromRow(input: {
363
426
  oldestAgeMs.deadLettered,
364
427
  input.row.dead_lettered_truncated,
365
428
  )
366
- : notApplicable<number>('This workset has blocked quarantine, not a DLQ.'),
429
+ : notApplicable<number>(
430
+ 'This workset has blocked quarantine, not a DLQ.',
431
+ ),
367
432
  },
368
433
  flow: {
369
434
  admitted: flowAvailable
@@ -410,6 +475,14 @@ function projectionFromRow(input: {
410
475
  kind: input.kind,
411
476
  source: input.source,
412
477
  observedAt: input.observedAt.toISOString(),
478
+ ...(input.includeRetirementCandidates
479
+ ? {
480
+ operatorRetirementCandidates: operatorRetirementCandidates(
481
+ input.row,
482
+ input.kind,
483
+ ),
484
+ }
485
+ : {}),
413
486
  status: derived.status,
414
487
  statusReason: derived.statusReason,
415
488
  observation,
@@ -438,36 +511,51 @@ function intervalExpr(column: string, nowParameter = '$1'): string {
438
511
  return `GREATEST(0, floor(extract(epoch FROM (${nowParameter}::timestamptz - ${column})) * 1000))`;
439
512
  }
440
513
 
441
- function absurdProjectionQuery(queueTable: string): string {
514
+ function absurdProjectionQuery(
515
+ queueTable: string,
516
+ includeRetirementCandidates: boolean,
517
+ candidateSampleLimit: number,
518
+ ): string {
442
519
  const table = `absurd.${quoteIdentifier(queueTable, 'queue table')}`;
520
+ const retirementColumns = includeRetirementCandidates
521
+ ? `,
522
+ COALESCE((
523
+ SELECT json_agg(json_build_object('itemId', item_id, 'ageMs', ${intervalExpr('age_at')}))
524
+ FROM (SELECT item_id, age_at FROM sampled WHERE bucket = 'blocked' ORDER BY age_at ASC, item_id ASC LIMIT $4::int) candidates
525
+ ), '[]'::json) AS blocked_retirement_candidates,
526
+ COALESCE((
527
+ SELECT json_agg(json_build_object('itemId', item_id, 'ageMs', ${intervalExpr('age_at')}))
528
+ FROM (SELECT item_id, age_at FROM sampled WHERE bucket = 'dead_lettered' ORDER BY age_at ASC, item_id ASC LIMIT $4::int) candidates
529
+ ), '[]'::json) AS dead_lettered_retirement_candidates`
530
+ : '';
443
531
  return `
444
532
  WITH sampled AS (
445
- (SELECT 'ready'::text AS bucket, available_at AS age_at
533
+ (SELECT 'ready'::text AS bucket, run_id::text AS item_id, available_at AS age_at
446
534
  FROM ${table}
447
535
  WHERE state = 'pending' AND available_at <= $1::timestamptz
448
536
  ORDER BY available_at, run_id
449
537
  LIMIT $2::int)
450
538
  UNION ALL
451
- (SELECT 'delayed'::text, available_at
539
+ (SELECT 'delayed'::text, run_id::text, available_at
452
540
  FROM ${table}
453
541
  WHERE state = 'sleeping'
454
542
  ORDER BY available_at, run_id
455
543
  LIMIT $2::int)
456
544
  UNION ALL
457
- (SELECT 'delayed'::text, available_at
545
+ (SELECT 'delayed'::text, run_id::text, available_at
458
546
  FROM ${table}
459
547
  WHERE state = 'pending' AND available_at > $1::timestamptz
460
548
  ORDER BY available_at, run_id
461
549
  LIMIT $2::int)
462
550
  UNION ALL
463
- (SELECT 'in_flight'::text, coalesce(started_at, created_at)
551
+ (SELECT 'in_flight'::text, run_id::text, coalesce(started_at, created_at)
464
552
  FROM ${table}
465
553
  WHERE state = 'running'
466
554
  AND (claim_expires_at IS NULL OR claim_expires_at > $1::timestamptz)
467
555
  ORDER BY available_at, run_id
468
556
  LIMIT $2::int)
469
557
  UNION ALL
470
- (SELECT 'blocked'::text, coalesce(claim_expires_at, started_at, created_at)
558
+ (SELECT 'blocked'::text, run_id::text, coalesce(claim_expires_at, started_at, created_at)
471
559
  FROM ${table}
472
560
  WHERE state = 'running'
473
561
  AND claim_expires_at IS NOT NULL
@@ -475,7 +563,7 @@ function absurdProjectionQuery(queueTable: string): string {
475
563
  ORDER BY claim_expires_at, run_id
476
564
  LIMIT $2::int)
477
565
  UNION ALL
478
- (SELECT 'dead_lettered'::text, coalesce(failed_at, created_at)
566
+ (SELECT 'dead_lettered'::text, run_id::text, coalesce(failed_at, created_at)
479
567
  FROM ${table}
480
568
  WHERE state = 'failed'
481
569
  ORDER BY available_at, run_id
@@ -496,7 +584,7 @@ function absurdProjectionQuery(queueTable: string): string {
496
584
  count(*) FILTER (WHERE bucket = 'delayed') > $3 AS delayed_truncated,
497
585
  count(*) FILTER (WHERE bucket = 'in_flight') > $3 AS in_flight_truncated,
498
586
  count(*) FILTER (WHERE bucket = 'blocked') > $3 AS blocked_truncated,
499
- count(*) FILTER (WHERE bucket = 'dead_lettered') > $3 AS dead_lettered_truncated,
587
+ count(*) FILTER (WHERE bucket = 'dead_lettered') > $3 AS dead_lettered_truncated${retirementColumns},
500
588
  0::bigint AS admitted,
501
589
  0::bigint AS started,
502
590
  0::bigint AS completed,
@@ -509,8 +597,22 @@ function absurdProjectionQuery(queueTable: string): string {
509
597
  `;
510
598
  }
511
599
 
512
- function outboxProjectionQuery(schema: string): string {
600
+ function outboxProjectionQuery(
601
+ schema: string,
602
+ includeRetirementCandidates: boolean,
603
+ ): string {
513
604
  const table = `${schema}."outbox"`;
605
+ const retirementColumns = includeRetirementCandidates
606
+ ? `,
607
+ COALESCE((
608
+ SELECT json_agg(json_build_object('itemId', item_id, 'ageMs', ${intervalExpr('age_at')}))
609
+ FROM (SELECT item_id, age_at FROM blocked ORDER BY age_at ASC, item_id ASC LIMIT $3::int) candidates
610
+ ), '[]'::json) AS blocked_retirement_candidates,
611
+ COALESCE((
612
+ SELECT json_agg(json_build_object('itemId', item_id, 'ageMs', ${intervalExpr('age_at')}))
613
+ FROM (SELECT item_id, age_at FROM dead_lettered ORDER BY age_at ASC, item_id ASC LIMIT $3::int) candidates
614
+ ), '[]'::json) AS dead_lettered_retirement_candidates`
615
+ : '';
514
616
  return `
515
617
  WITH ready AS (
516
618
  SELECT coalesce(next_attempt_at, created_at) AS age_at
@@ -540,7 +642,7 @@ function outboxProjectionQuery(schema: string): string {
540
642
  ORDER BY lease_expires_at, created_at, event_id
541
643
  LIMIT $2::int
542
644
  ), blocked AS (
543
- SELECT coalesce(lease_expires_at, created_at) AS age_at
645
+ SELECT event_id::text AS item_id, coalesce(lease_expires_at, created_at) AS age_at
544
646
  FROM ${table}
545
647
  WHERE delivered_at IS NULL
546
648
  AND failed_at IS NULL
@@ -549,7 +651,7 @@ function outboxProjectionQuery(schema: string): string {
549
651
  ORDER BY coalesce(lease_expires_at, created_at), created_at, event_id
550
652
  LIMIT $2::int
551
653
  ), dead_lettered AS (
552
- SELECT coalesce(failed_at, created_at) AS age_at
654
+ SELECT event_id::text AS item_id, coalesce(failed_at, created_at) AS age_at
553
655
  FROM ${table}
554
656
  WHERE delivered_at IS NULL AND failed_at IS NOT NULL
555
657
  ORDER BY failed_at, event_id
@@ -570,7 +672,7 @@ function outboxProjectionQuery(schema: string): string {
570
672
  (SELECT count(*) FROM delayed) > $3 AS delayed_truncated,
571
673
  (SELECT count(*) FROM in_flight) > $3 AS in_flight_truncated,
572
674
  (SELECT count(*) FROM blocked) > $3 AS blocked_truncated,
573
- (SELECT count(*) FROM dead_lettered) > $3 AS dead_lettered_truncated,
675
+ (SELECT count(*) FROM dead_lettered) > $3 AS dead_lettered_truncated${retirementColumns},
574
676
  0::bigint AS admitted,
575
677
  0::bigint AS started,
576
678
  0::bigint AS completed,
@@ -582,8 +684,19 @@ function outboxProjectionQuery(schema: string): string {
582
684
  `;
583
685
  }
584
686
 
585
- function cleanupProjectionQuery(schema: string): string {
687
+ function cleanupProjectionQuery(
688
+ schema: string,
689
+ includeRetirementCandidates: boolean,
690
+ ): string {
586
691
  const table = `${schema}."sandbox_cleanup_jobs_v2"`;
692
+ const retirementColumns = includeRetirementCandidates
693
+ ? `,
694
+ COALESCE((
695
+ SELECT json_agg(json_build_object('provider', provider, 'sandboxId', sandbox_id, 'routingDomainKey', routing_domain_key, 'ageMs', ${intervalExpr('age_at')}))
696
+ FROM (SELECT provider, sandbox_id, routing_domain_key, created_at, age_at FROM blocked ORDER BY age_at ASC, created_at ASC, provider ASC, sandbox_id ASC LIMIT $3::int) candidates
697
+ ), '[]'::json) AS blocked_retirement_candidates,
698
+ '[]'::json AS dead_lettered_retirement_candidates`
699
+ : '';
587
700
  return `
588
701
  WITH ready AS (
589
702
  SELECT created_at AS age_at
@@ -616,6 +729,10 @@ function cleanupProjectionQuery(schema: string): string {
616
729
  LIMIT $2::int
617
730
  ), blocked AS (
618
731
  SELECT
732
+ provider,
733
+ sandbox_id,
734
+ routing_domain_key,
735
+ created_at,
619
736
  CASE
620
737
  WHEN blocked_at IS NOT NULL THEN blocked_at
621
738
  ELSE coalesce(lease_expires_at, created_at)
@@ -645,7 +762,7 @@ function cleanupProjectionQuery(schema: string): string {
645
762
  (SELECT count(*) FROM delayed) > $3 AS delayed_truncated,
646
763
  (SELECT count(*) FROM in_flight) > $3 AS in_flight_truncated,
647
764
  (SELECT count(*) FROM blocked) > $3 AS blocked_truncated,
648
- false AS dead_lettered_truncated,
765
+ false AS dead_lettered_truncated${retirementColumns},
649
766
  0::bigint AS admitted,
650
767
  0::bigint AS started,
651
768
  0::bigint AS completed,
@@ -657,8 +774,19 @@ function cleanupProjectionQuery(schema: string): string {
657
774
  `;
658
775
  }
659
776
 
660
- function computeBillingProjectionQuery(schema: string): string {
777
+ function computeBillingProjectionQuery(
778
+ schema: string,
779
+ includeRetirementCandidates: boolean,
780
+ ): string {
661
781
  const table = `${schema}."compute_billing_jobs_v2"`;
782
+ const retirementColumns = includeRetirementCandidates
783
+ ? `,
784
+ COALESCE((
785
+ SELECT json_agg(json_build_object('itemId', item_id, 'ageMs', ${intervalExpr('age_at')}))
786
+ FROM (SELECT item_id, age_at FROM blocked ORDER BY age_at ASC, item_id ASC LIMIT $3::int) candidates
787
+ ), '[]'::json) AS blocked_retirement_candidates,
788
+ '[]'::json AS dead_lettered_retirement_candidates`
789
+ : '';
662
790
  return `
663
791
  WITH ready AS (
664
792
  SELECT created_at AS age_at
@@ -688,7 +816,7 @@ function computeBillingProjectionQuery(schema: string): string {
688
816
  ORDER BY lease_expires_at, created_at, run_id
689
817
  LIMIT $2::int
690
818
  ), blocked AS (
691
- SELECT coalesce(blocked_at, lease_expires_at, created_at) AS age_at
819
+ SELECT run_id::text AS item_id, coalesce(blocked_at, lease_expires_at, created_at) AS age_at
692
820
  FROM ${table}
693
821
  WHERE status <> 'settled'
694
822
  AND (
@@ -713,7 +841,7 @@ function computeBillingProjectionQuery(schema: string): string {
713
841
  (SELECT count(*) FROM delayed) > $3 AS delayed_truncated,
714
842
  (SELECT count(*) FROM in_flight) > $3 AS in_flight_truncated,
715
843
  (SELECT count(*) FROM blocked) > $3 AS blocked_truncated,
716
- false AS dead_lettered_truncated,
844
+ false AS dead_lettered_truncated${retirementColumns},
717
845
  0::bigint AS admitted,
718
846
  0::bigint AS started,
719
847
  0::bigint AS completed,
@@ -796,6 +924,10 @@ export async function readRuntimeQueueHealthFromClient(
796
924
  const queueNames = registry.rows
797
925
  .slice(0, maxQueues)
798
926
  .map((row) => row.queue_name);
927
+ const candidateSampleLimit = Math.max(
928
+ 1,
929
+ Math.floor(sampleLimit / Math.max(queueNames.length, 1)),
930
+ );
799
931
  if (requestedNames) {
800
932
  const found = new Set(queueNames);
801
933
  const missing = requestedNames.filter((name) => !found.has(name));
@@ -807,9 +939,17 @@ export async function readRuntimeQueueHealthFromClient(
807
939
  }
808
940
  const projections: RuntimeQueueHealthProjection[] = [];
809
941
  for (const queueName of queueNames) {
942
+ const includeRetirementCandidates =
943
+ input.includeRetirementCandidates === true;
810
944
  const result = await client.query<CountRow>(
811
- absurdProjectionQuery(`r_${queueName}`),
812
- [now, sampleLimit + 1, sampleLimit],
945
+ absurdProjectionQuery(
946
+ `r_${queueName}`,
947
+ includeRetirementCandidates,
948
+ candidateSampleLimit,
949
+ ),
950
+ includeRetirementCandidates
951
+ ? [now, sampleLimit + 1, sampleLimit, candidateSampleLimit]
952
+ : [now, sampleLimit + 1, sampleLimit],
813
953
  );
814
954
  projections.push(
815
955
  projectionFromRow({
@@ -820,25 +960,29 @@ export async function readRuntimeQueueHealthFromClient(
820
960
  observedAt: now,
821
961
  environment: input.environment,
822
962
  options,
963
+ includeRetirementCandidates: input.includeRetirementCandidates,
823
964
  }),
824
965
  );
825
966
  }
826
967
  const [outbox, computeBilling, cleanup] = await Promise.all([
827
- client.query<CountRow>(outboxProjectionQuery(schema), [
828
- now,
829
- sampleLimit + 1,
830
- sampleLimit,
831
- ]),
832
- client.query<CountRow>(computeBillingProjectionQuery(schema), [
833
- now,
834
- sampleLimit + 1,
835
- sampleLimit,
836
- ]),
837
- client.query<CountRow>(cleanupProjectionQuery(schema), [
838
- now,
839
- sampleLimit + 1,
840
- sampleLimit,
841
- ]),
968
+ client.query<CountRow>(
969
+ outboxProjectionQuery(schema, input.includeRetirementCandidates === true),
970
+ [now, sampleLimit + 1, sampleLimit],
971
+ ),
972
+ client.query<CountRow>(
973
+ computeBillingProjectionQuery(
974
+ schema,
975
+ input.includeRetirementCandidates === true,
976
+ ),
977
+ [now, sampleLimit + 1, sampleLimit],
978
+ ),
979
+ client.query<CountRow>(
980
+ cleanupProjectionQuery(
981
+ schema,
982
+ input.includeRetirementCandidates === true,
983
+ ),
984
+ [now, sampleLimit + 1, sampleLimit],
985
+ ),
842
986
  ]);
843
987
  projections.push(
844
988
  projectionFromRow({
@@ -849,6 +993,7 @@ export async function readRuntimeQueueHealthFromClient(
849
993
  observedAt: now,
850
994
  environment: input.environment,
851
995
  options,
996
+ includeRetirementCandidates: input.includeRetirementCandidates,
852
997
  }),
853
998
  projectionFromRow({
854
999
  id: 'runtime:compute-billing-jobs-v2',
@@ -858,6 +1003,7 @@ export async function readRuntimeQueueHealthFromClient(
858
1003
  observedAt: now,
859
1004
  environment: input.environment,
860
1005
  options,
1006
+ includeRetirementCandidates: input.includeRetirementCandidates,
861
1007
  }),
862
1008
  projectionFromRow({
863
1009
  id: 'runtime:sandbox-cleanup-jobs-v2',
@@ -867,6 +1013,7 @@ export async function readRuntimeQueueHealthFromClient(
867
1013
  observedAt: now,
868
1014
  environment: input.environment,
869
1015
  options,
1016
+ includeRetirementCandidates: input.includeRetirementCandidates,
870
1017
  }),
871
1018
  );
872
1019
  const summary = {
@@ -11,6 +11,7 @@ export const PRODUCT_NOTIFICATION_EVENT_SCHEMA_VERSION = 1 as const;
11
11
  export const PRODUCT_NOTIFICATION_MAX_EVENT_BYTES = 16_000;
12
12
  export const PRODUCT_NOTIFICATION_MAX_EVENT_FACTS = 20;
13
13
  export const PRODUCT_NOTIFICATION_MAX_EVENT_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
14
+ export const PRODUCT_NOTIFICATION_MAX_IDENTIFIER_LENGTH = 512;
14
15
  export const PRODUCT_NOTIFICATION_MAX_SOURCE_CLOCK_SKEW_MS = 5 * 60 * 1_000;
15
16
 
16
17
  export const ProductNotificationEvent = {
@@ -255,7 +256,9 @@ function requiredString(value: unknown, field: string) {
255
256
  if (typeof value !== 'string' || !value.trim()) {
256
257
  throw new Error(`${field} must be a non-empty string.`);
257
258
  }
258
- if (value.length > 512) throw new Error(`${field} is too long.`);
259
+ if (value.length > PRODUCT_NOTIFICATION_MAX_IDENTIFIER_LENGTH) {
260
+ throw new Error(`${field} is too long.`);
261
+ }
259
262
  }
260
263
 
261
264
  function requireInternalTargetPath(value: unknown) {