@ttt-productions/ttt-core 0.79.55 → 0.79.57
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/constants/scheduled-jobs.d.ts +42 -0
- package/dist/constants/scheduled-jobs.d.ts.map +1 -1
- package/dist/constants/scheduled-jobs.js +43 -0
- package/dist/constants/scheduled-jobs.js.map +1 -1
- package/dist/doc-schemas/backend-state.d.ts +13 -0
- package/dist/doc-schemas/backend-state.d.ts.map +1 -1
- package/dist/doc-schemas/backend-state.js +46 -4
- package/dist/doc-schemas/backend-state.js.map +1 -1
- package/dist/doc-schemas/media-copy-intents.d.ts +2 -0
- package/dist/doc-schemas/media-copy-intents.d.ts.map +1 -1
- package/dist/doc-schemas/media-copy-intents.js +17 -1
- package/dist/doc-schemas/media-copy-intents.js.map +1 -1
- package/dist/doc-schemas/registry.d.ts +8 -0
- package/dist/doc-schemas/registry.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -4,4 +4,46 @@ export declare const ADMIN_TASK_CLEANUP_INTERVAL = 15;
|
|
|
4
4
|
export declare const ADMIN_TASK_EXPIRED_BATCH_SIZE = 500;
|
|
5
5
|
/** Safety cap on iterations within a single admin-task cleanup run. */
|
|
6
6
|
export declare const ADMIN_TASK_CLEANUP_MAX_ITERATIONS = 10;
|
|
7
|
+
/**
|
|
8
|
+
* How long Hall-media copies are left alone before the orphan reaper may reclaim them — both a
|
|
9
|
+
* Hall-owned asset (asset phase, measured from its `createdAt`) and the objects a copy intent
|
|
10
|
+
* names (copy-intent phase: recording sets the intent's `reapAfter` this far out). It must exceed
|
|
11
|
+
* the longest retry window of the publish trigger `onThresholdItemReviewed` (about a day), since
|
|
12
|
+
* a pending retry re-derives the same deterministic copy and attaches its bytes. It also leaves a
|
|
13
|
+
* publish that parked partway time to be unwound, resubmitted, and re-driven onto the same
|
|
14
|
+
* destination while its earlier objects are still there to reuse.
|
|
15
|
+
*/
|
|
16
|
+
export declare const HALL_MEDIA_ORPHAN_GRACE_MS: number;
|
|
17
|
+
/** Most candidates each reaper phase takes up per pass: assets in the asset phase, due intents in
|
|
18
|
+
* the copy-intent phase. */
|
|
19
|
+
export declare const HALL_MEDIA_REAPER_PAGE_SIZE = 100;
|
|
20
|
+
/**
|
|
21
|
+
* Deferral backoff for both reaper phases — a copy intent the sweep cannot finish yet
|
|
22
|
+
* (`reapAttemptCount`) and an asset-phase candidate it cannot positively clear (a cursor
|
|
23
|
+
* `deferred` entry's `attemptCount`). With `n` the count after the deferral that is being
|
|
24
|
+
* scheduled, the item is next due `min(BASE × 2^(n−1), MAX)` from now: 1, 2, 4, 8, then 14 days
|
|
25
|
+
* from there on. The base is the reaper's daily cadence, since any shorter delay is simply the
|
|
26
|
+
* next pass; the cap bounds how long a released hold or a recovered read waits to be noticed.
|
|
27
|
+
*/
|
|
28
|
+
export declare const HALL_MEDIA_REAPER_BACKOFF_BASE_MS: number;
|
|
29
|
+
export declare const HALL_MEDIA_REAPER_BACKOFF_MAX_MS: number;
|
|
30
|
+
/**
|
|
31
|
+
* Most asset-phase candidates the Hall-media orphan reaper holds in its cursor's `deferred`
|
|
32
|
+
* set. Every pass retries the entries that are due, each retry re-reading the candidate and its
|
|
33
|
+
* owning Hall item, so the bound keeps that work to half of a HALL_MEDIA_REAPER_PAGE_SIZE page on
|
|
34
|
+
* top of the pass's own page, and keeps the singleton cursor doc a few kilobytes. A candidate the
|
|
35
|
+
* reaper cannot positively clear is rare, so a full set means something systemic is failing — the
|
|
36
|
+
* signal to stop and report, not to keep growing the list.
|
|
37
|
+
*/
|
|
38
|
+
export declare const HALL_MEDIA_REAPER_MAX_DEFERRED = 50;
|
|
39
|
+
/**
|
|
40
|
+
* How long the copy-intent sweep holds a claim on an intent. Claiming moves the intent to
|
|
41
|
+
* `reaping` and stamps `reapClaimedAt`, and that stamp is the claim's fencing token. Once the claim
|
|
42
|
+
* is this old it has expired: a later sweep may take the intent over and re-stamp it, and the copy
|
|
43
|
+
* path may take the `reaping` intent back to `copying`. It must far exceed the timeout of the
|
|
44
|
+
* reaper function `reapOrphanedHallMediaCopies` (300 s), so a live pass never loses its claim, and
|
|
45
|
+
* stay far below the retry budget of the publish trigger `onThresholdItemReviewed` (about a day),
|
|
46
|
+
* so a publish stranded behind an abandoned claim always recovers inside it.
|
|
47
|
+
*/
|
|
48
|
+
export declare const HALL_MEDIA_REAPER_CLAIM_LEASE_MS: number;
|
|
7
49
|
//# sourceMappingURL=scheduled-jobs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduled-jobs.d.ts","sourceRoot":"","sources":["../../src/constants/scheduled-jobs.ts"],"names":[],"mappings":"AAIA,oEAAoE;AACpE,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,wEAAwE;AACxE,eAAO,MAAM,6BAA6B,MAAM,CAAC;AAEjD,uEAAuE;AACvE,eAAO,MAAM,iCAAiC,KAAK,CAAC"}
|
|
1
|
+
{"version":3,"file":"scheduled-jobs.d.ts","sourceRoot":"","sources":["../../src/constants/scheduled-jobs.ts"],"names":[],"mappings":"AAIA,oEAAoE;AACpE,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,wEAAwE;AACxE,eAAO,MAAM,6BAA6B,MAAM,CAAC;AAEjD,uEAAuE;AACvE,eAAO,MAAM,iCAAiC,KAAK,CAAC;AAIpD;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,QAA2B,CAAC;AAEnE;6BAC6B;AAC7B,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,QAAsB,CAAC;AACrE,eAAO,MAAM,gCAAgC,QAA2B,CAAC;AAEzE;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAEjD;;;;;;;;GAQG;AACH,eAAO,MAAM,gCAAgC,QAAiB,CAAC"}
|
|
@@ -6,4 +6,47 @@ export const ADMIN_TASK_CLEANUP_INTERVAL = 15;
|
|
|
6
6
|
export const ADMIN_TASK_EXPIRED_BATCH_SIZE = 500;
|
|
7
7
|
/** Safety cap on iterations within a single admin-task cleanup run. */
|
|
8
8
|
export const ADMIN_TASK_CLEANUP_MAX_ITERATIONS = 10;
|
|
9
|
+
// --- Hall-media orphan reaper ---
|
|
10
|
+
/**
|
|
11
|
+
* How long Hall-media copies are left alone before the orphan reaper may reclaim them — both a
|
|
12
|
+
* Hall-owned asset (asset phase, measured from its `createdAt`) and the objects a copy intent
|
|
13
|
+
* names (copy-intent phase: recording sets the intent's `reapAfter` this far out). It must exceed
|
|
14
|
+
* the longest retry window of the publish trigger `onThresholdItemReviewed` (about a day), since
|
|
15
|
+
* a pending retry re-derives the same deterministic copy and attaches its bytes. It also leaves a
|
|
16
|
+
* publish that parked partway time to be unwound, resubmitted, and re-driven onto the same
|
|
17
|
+
* destination while its earlier objects are still there to reuse.
|
|
18
|
+
*/
|
|
19
|
+
export const HALL_MEDIA_ORPHAN_GRACE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
20
|
+
/** Most candidates each reaper phase takes up per pass: assets in the asset phase, due intents in
|
|
21
|
+
* the copy-intent phase. */
|
|
22
|
+
export const HALL_MEDIA_REAPER_PAGE_SIZE = 100;
|
|
23
|
+
/**
|
|
24
|
+
* Deferral backoff for both reaper phases — a copy intent the sweep cannot finish yet
|
|
25
|
+
* (`reapAttemptCount`) and an asset-phase candidate it cannot positively clear (a cursor
|
|
26
|
+
* `deferred` entry's `attemptCount`). With `n` the count after the deferral that is being
|
|
27
|
+
* scheduled, the item is next due `min(BASE × 2^(n−1), MAX)` from now: 1, 2, 4, 8, then 14 days
|
|
28
|
+
* from there on. The base is the reaper's daily cadence, since any shorter delay is simply the
|
|
29
|
+
* next pass; the cap bounds how long a released hold or a recovered read waits to be noticed.
|
|
30
|
+
*/
|
|
31
|
+
export const HALL_MEDIA_REAPER_BACKOFF_BASE_MS = 24 * 60 * 60 * 1000;
|
|
32
|
+
export const HALL_MEDIA_REAPER_BACKOFF_MAX_MS = 14 * 24 * 60 * 60 * 1000;
|
|
33
|
+
/**
|
|
34
|
+
* Most asset-phase candidates the Hall-media orphan reaper holds in its cursor's `deferred`
|
|
35
|
+
* set. Every pass retries the entries that are due, each retry re-reading the candidate and its
|
|
36
|
+
* owning Hall item, so the bound keeps that work to half of a HALL_MEDIA_REAPER_PAGE_SIZE page on
|
|
37
|
+
* top of the pass's own page, and keeps the singleton cursor doc a few kilobytes. A candidate the
|
|
38
|
+
* reaper cannot positively clear is rare, so a full set means something systemic is failing — the
|
|
39
|
+
* signal to stop and report, not to keep growing the list.
|
|
40
|
+
*/
|
|
41
|
+
export const HALL_MEDIA_REAPER_MAX_DEFERRED = 50;
|
|
42
|
+
/**
|
|
43
|
+
* How long the copy-intent sweep holds a claim on an intent. Claiming moves the intent to
|
|
44
|
+
* `reaping` and stamps `reapClaimedAt`, and that stamp is the claim's fencing token. Once the claim
|
|
45
|
+
* is this old it has expired: a later sweep may take the intent over and re-stamp it, and the copy
|
|
46
|
+
* path may take the `reaping` intent back to `copying`. It must far exceed the timeout of the
|
|
47
|
+
* reaper function `reapOrphanedHallMediaCopies` (300 s), so a live pass never loses its claim, and
|
|
48
|
+
* stay far below the retry budget of the publish trigger `onThresholdItemReviewed` (about a day),
|
|
49
|
+
* so a publish stranded behind an abandoned claim always recovers inside it.
|
|
50
|
+
*/
|
|
51
|
+
export const HALL_MEDIA_REAPER_CLAIM_LEASE_MS = 60 * 60 * 1000;
|
|
9
52
|
//# sourceMappingURL=scheduled-jobs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduled-jobs.js","sourceRoot":"","sources":["../../src/constants/scheduled-jobs.ts"],"names":[],"mappings":"AAAA,yDAAyD;AAEzD,6BAA6B;AAE7B,oEAAoE;AACpE,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAE9C,wEAAwE;AACxE,MAAM,CAAC,MAAM,6BAA6B,GAAG,GAAG,CAAC;AAEjD,uEAAuE;AACvE,MAAM,CAAC,MAAM,iCAAiC,GAAG,EAAE,CAAC","sourcesContent":["// Scheduled-job intervals, batch sizes, and source URLs.\n\n// --- Admin task cleanup ---\n\n/** Interval in minutes for the admin-task cleanup scheduled job. */\nexport const ADMIN_TASK_CLEANUP_INTERVAL = 15;\n\n/** Batch size of expired task checkouts processed per scheduled run. */\nexport const ADMIN_TASK_EXPIRED_BATCH_SIZE = 500;\n\n/** Safety cap on iterations within a single admin-task cleanup run. */\nexport const ADMIN_TASK_CLEANUP_MAX_ITERATIONS = 10;\n"]}
|
|
1
|
+
{"version":3,"file":"scheduled-jobs.js","sourceRoot":"","sources":["../../src/constants/scheduled-jobs.ts"],"names":[],"mappings":"AAAA,yDAAyD;AAEzD,6BAA6B;AAE7B,oEAAoE;AACpE,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAE9C,wEAAwE;AACxE,MAAM,CAAC,MAAM,6BAA6B,GAAG,GAAG,CAAC;AAEjD,uEAAuE;AACvE,MAAM,CAAC,MAAM,iCAAiC,GAAG,EAAE,CAAC;AAEpD,mCAAmC;AAEnC;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEnE;6BAC6B;AAC7B,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAC;AAE/C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AACrE,MAAM,CAAC,MAAM,gCAAgC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEzE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,CAAC;AAEjD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC","sourcesContent":["// Scheduled-job intervals, batch sizes, and source URLs.\n\n// --- Admin task cleanup ---\n\n/** Interval in minutes for the admin-task cleanup scheduled job. */\nexport const ADMIN_TASK_CLEANUP_INTERVAL = 15;\n\n/** Batch size of expired task checkouts processed per scheduled run. */\nexport const ADMIN_TASK_EXPIRED_BATCH_SIZE = 500;\n\n/** Safety cap on iterations within a single admin-task cleanup run. */\nexport const ADMIN_TASK_CLEANUP_MAX_ITERATIONS = 10;\n\n// --- Hall-media orphan reaper ---\n\n/**\n * How long Hall-media copies are left alone before the orphan reaper may reclaim them — both a\n * Hall-owned asset (asset phase, measured from its `createdAt`) and the objects a copy intent\n * names (copy-intent phase: recording sets the intent's `reapAfter` this far out). It must exceed\n * the longest retry window of the publish trigger `onThresholdItemReviewed` (about a day), since\n * a pending retry re-derives the same deterministic copy and attaches its bytes. It also leaves a\n * publish that parked partway time to be unwound, resubmitted, and re-driven onto the same\n * destination while its earlier objects are still there to reuse.\n */\nexport const HALL_MEDIA_ORPHAN_GRACE_MS = 14 * 24 * 60 * 60 * 1000;\n\n/** Most candidates each reaper phase takes up per pass: assets in the asset phase, due intents in\n * the copy-intent phase. */\nexport const HALL_MEDIA_REAPER_PAGE_SIZE = 100;\n\n/**\n * Deferral backoff for both reaper phases — a copy intent the sweep cannot finish yet\n * (`reapAttemptCount`) and an asset-phase candidate it cannot positively clear (a cursor\n * `deferred` entry's `attemptCount`). With `n` the count after the deferral that is being\n * scheduled, the item is next due `min(BASE × 2^(n−1), MAX)` from now: 1, 2, 4, 8, then 14 days\n * from there on. The base is the reaper's daily cadence, since any shorter delay is simply the\n * next pass; the cap bounds how long a released hold or a recovered read waits to be noticed.\n */\nexport const HALL_MEDIA_REAPER_BACKOFF_BASE_MS = 24 * 60 * 60 * 1000;\nexport const HALL_MEDIA_REAPER_BACKOFF_MAX_MS = 14 * 24 * 60 * 60 * 1000;\n\n/**\n * Most asset-phase candidates the Hall-media orphan reaper holds in its cursor's `deferred`\n * set. Every pass retries the entries that are due, each retry re-reading the candidate and its\n * owning Hall item, so the bound keeps that work to half of a HALL_MEDIA_REAPER_PAGE_SIZE page on\n * top of the pass's own page, and keeps the singleton cursor doc a few kilobytes. A candidate the\n * reaper cannot positively clear is rare, so a full set means something systemic is failing — the\n * signal to stop and report, not to keep growing the list.\n */\nexport const HALL_MEDIA_REAPER_MAX_DEFERRED = 50;\n\n/**\n * How long the copy-intent sweep holds a claim on an intent. Claiming moves the intent to\n * `reaping` and stamps `reapClaimedAt`, and that stamp is the claim's fencing token. Once the claim\n * is this old it has expired: a later sweep may take the intent over and re-stamp it, and the copy\n * path may take the `reaping` intent back to `copying`. It must far exceed the timeout of the\n * reaper function `reapOrphanedHallMediaCopies` (300 s), so a live pass never loses its claim, and\n * stay far below the retry budget of the publish trigger `onThresholdItemReviewed` (about a day),\n * so a publish stranded behind an abandoned claim always recovers inside it.\n */\nexport const HALL_MEDIA_REAPER_CLAIM_LEASE_MS = 60 * 60 * 1000;\n"]}
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export declare const HallMediaReaperDeferredCandidateSchema: z.ZodObject<{
|
|
3
|
+
mediaAssetId: z.ZodString;
|
|
4
|
+
createdAt: z.ZodNumber;
|
|
5
|
+
nextAttemptAt: z.ZodNumber;
|
|
6
|
+
attemptCount: z.ZodNumber;
|
|
7
|
+
}, z.core.$strict>;
|
|
8
|
+
export type HallMediaReaperDeferredCandidate = z.infer<typeof HallMediaReaperDeferredCandidateSchema>;
|
|
2
9
|
export declare const HallMediaReaperCursorSchema: z.ZodObject<{
|
|
3
10
|
createdAtCursor: z.ZodNumber;
|
|
4
11
|
updatedAt: z.ZodNumber;
|
|
12
|
+
deferred: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
13
|
+
mediaAssetId: z.ZodString;
|
|
14
|
+
createdAt: z.ZodNumber;
|
|
15
|
+
nextAttemptAt: z.ZodNumber;
|
|
16
|
+
attemptCount: z.ZodNumber;
|
|
17
|
+
}, z.core.$strict>>>;
|
|
5
18
|
}, z.core.$strip>;
|
|
6
19
|
export type HallMediaReaperCursor = z.infer<typeof HallMediaReaperCursorSchema>;
|
|
7
20
|
export declare const PublicUsersReconcilerCursorSchema: z.ZodObject<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend-state.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/backend-state.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"backend-state.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/backend-state.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,eAAO,MAAM,sCAAsC;;;;;kBAWxC,CAAC;AACZ,MAAM,MAAM,gCAAgC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sCAAsC,CAAC,CAAC;AAWtG,eAAO,MAAM,2BAA2B;;;;;;;;;iBAyBpC,CAAC;AACL,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAOhF,eAAO,MAAM,iCAAiC;;;iBAG5C,CAAC;AACH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAS5F,eAAO,MAAM,oBAAoB;;;;;;;;;;;;iBAa/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAMlE,eAAO,MAAM,4BAA4B;;;iBAIvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AASlF,eAAO,MAAM,gBAAgB;;;;;iBAS3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAU1D,eAAO,MAAM,kCAAkC;;;;;;;;;;;;;;iBAc7C,CAAC;AACH,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kCAAkC,CAAC,CAAC;AAS9F,eAAO,MAAM,kCAAkC;;;;;;;;;;;;iBAgB7C,CAAC;AACH,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kCAAkC,CAAC,CAAC;AAQ9F,eAAO,MAAM,mCAAmC;;;;;;;;iBAU9C,CAAC;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAC"}
|
|
@@ -4,15 +4,57 @@
|
|
|
4
4
|
// the path ONE owner, and COLLECTION_SCHEMAS is the inventory the schema-doc generator and
|
|
5
5
|
// the drift-check read. Types are inferred via z.infer.
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { HALL_MEDIA_REAPER_MAX_DEFERRED } from '../constants/scheduled-jobs.js';
|
|
7
8
|
import { ChildSafetyNcmecCompletionChannelSchema, ChildSafetyNcmecCompletionProofTypeSchema, } from './safety/case.js';
|
|
9
|
+
// One asset-phase candidate the Hall-media orphan reaper could not positively clear (a read
|
|
10
|
+
// error, a missing owner). The cursor moves past it and the reaper retries it from here instead,
|
|
11
|
+
// so one uncertain candidate never pins the scan window.
|
|
12
|
+
export const HallMediaReaperDeferredCandidateSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
mediaAssetId: z.string().min(1),
|
|
15
|
+
/** The candidate's mediaAssets `createdAt`. */
|
|
16
|
+
createdAt: z.number().int().nonnegative(),
|
|
17
|
+
/** Epoch ms from which a pass may retry it. */
|
|
18
|
+
nextAttemptAt: z.number().int().nonnegative(),
|
|
19
|
+
/** Passes that examined it without clearing it; the pass that deferred it is the first. The
|
|
20
|
+
* HALL_MEDIA_REAPER_BACKOFF_BASE_MS / HALL_MEDIA_REAPER_BACKOFF_MAX_MS backoff grows from it. */
|
|
21
|
+
attemptCount: z.number().int().positive(),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
8
24
|
// _systemData/hallMediaReaperCursor — the scheduled Hall-media orphan reaper's scan cursor.
|
|
9
|
-
// `createdAtCursor` is the highest mediaAssets `createdAt` the reaper has
|
|
10
|
-
//
|
|
11
|
-
// permanently-live Hall assets instead of
|
|
25
|
+
// `createdAtCursor` is the highest mediaAssets `createdAt` the reaper has moved past: every
|
|
26
|
+
// candidate at or below it was positively cleared (referenced / reaped / already-deleted) or is
|
|
27
|
+
// held in `deferred`, so each pass resumes past permanently-live Hall assets instead of
|
|
28
|
+
// re-reading the same oldest page forever. `deferred` names each asset at most once, holds at
|
|
29
|
+
// most HALL_MEDIA_REAPER_MAX_DEFERRED entries, and holds only candidates at or below the cursor
|
|
30
|
+
// — deferring a candidate is what moves the cursor past it. A cursor written before the set
|
|
31
|
+
// existed has no `deferred`, which reads as empty.
|
|
12
32
|
// (functions/src/media/reapOrphanedHallMediaCopies.ts)
|
|
13
|
-
export const HallMediaReaperCursorSchema = z
|
|
33
|
+
export const HallMediaReaperCursorSchema = z
|
|
34
|
+
.object({
|
|
14
35
|
createdAtCursor: z.number(),
|
|
15
36
|
updatedAt: z.number(),
|
|
37
|
+
deferred: z.array(HallMediaReaperDeferredCandidateSchema).max(HALL_MEDIA_REAPER_MAX_DEFERRED).optional(),
|
|
38
|
+
})
|
|
39
|
+
.superRefine((val, ctx) => {
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
(val.deferred ?? []).forEach((entry, index) => {
|
|
42
|
+
if (seen.has(entry.mediaAssetId)) {
|
|
43
|
+
ctx.addIssue({
|
|
44
|
+
code: 'custom',
|
|
45
|
+
path: ['deferred', index, 'mediaAssetId'],
|
|
46
|
+
message: 'a candidate is deferred at most once',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
seen.add(entry.mediaAssetId);
|
|
50
|
+
if (entry.createdAt > val.createdAtCursor) {
|
|
51
|
+
ctx.addIssue({
|
|
52
|
+
code: 'custom',
|
|
53
|
+
path: ['deferred', index, 'createdAt'],
|
|
54
|
+
message: 'a deferred candidate sits at or below the cursor that moved past it',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
16
58
|
});
|
|
17
59
|
// _systemData/publicUsersReconcilerCursor — the scheduled publicUsers reconciler's sweep cursor.
|
|
18
60
|
// `profileIdCursor` is the last `userProfiles` document id the sweep has POSITIVELY cleared, so
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend-state.js","sourceRoot":"","sources":["../../src/doc-schemas/backend-state.ts"],"names":[],"mappings":"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","sourcesContent":["// Backend-only state document SCHEMAS — Cloud-Functions-owned bookkeeping docs that no\n// client ever reads or writes (firestore.rules denies client access to every path below).\n// They are registered here for the same reason every other stored shape is: ARCH-104 gives\n// the path ONE owner, and COLLECTION_SCHEMAS is the inventory the schema-doc generator and\n// the drift-check read. Types are inferred via z.infer.\n\nimport { z } from 'zod';\nimport {\n ChildSafetyNcmecCompletionChannelSchema,\n ChildSafetyNcmecCompletionProofTypeSchema,\n} from './safety/case.js';\n\n// _systemData/hallMediaReaperCursor — the scheduled Hall-media orphan reaper's scan cursor.\n// `createdAtCursor` is the highest mediaAssets `createdAt` the reaper has POSITIVELY cleared\n// (candidate resolved referenced / reaped / already-deleted), so each pass resumes past\n// permanently-live Hall assets instead of re-reading the same oldest page forever.\n// (functions/src/media/reapOrphanedHallMediaCopies.ts)\nexport const HallMediaReaperCursorSchema = z.object({\n createdAtCursor: z.number(),\n updatedAt: z.number(),\n});\nexport type HallMediaReaperCursor = z.infer<typeof HallMediaReaperCursorSchema>;\n\n// _systemData/publicUsersReconcilerCursor — the scheduled publicUsers reconciler's sweep cursor.\n// `profileIdCursor` is the last `userProfiles` document id the sweep has POSITIVELY cleared, so\n// each pass resumes after it instead of re-reading the same oldest page forever. An EMPTY STRING\n// means start from the beginning — both the first-ever pass and the wrap after the sweep exhausts\n// the collection. (functions/src/users/reconcilePublicUsers.ts)\nexport const PublicUsersReconcilerCursorSchema = z.object({\n profileIdCursor: z.string(),\n updatedAt: z.number(),\n});\nexport type PublicUsersReconcilerCursor = z.infer<typeof PublicUsersReconcilerCursorSchema>;\n\n// operatorStepUp/{uid} — [H-08] per-operator TOTP step-up state: the authenticator secret\n// plus the currently open grant window. The secret is returned to the client EXACTLY ONCE by\n// enrollOperatorStepUp and is never readable through Firestore afterwards; `status` goes\n// 'pending' → 'active' on confirm, and each successful verify pushes `grantExpiresAt` out by\n// the step-up window. `failedAttempts` and `lockedUntil` make the server-side TOTP brute-force\n// lockout durable; omitted fields are the compatible no-lockout state for rows written before\n// that protection shipped. (functions/src/safety/operatorStepUp.ts)\nexport const OperatorStepUpSchema = z.object({\n secret: z.string(),\n status: z.enum(['pending', 'active']),\n /** Absent until the first successful verify; a grant is live while now < grantExpiresAt. */\n grantExpiresAt: z.number().optional(),\n createdAt: z.number(),\n updatedAt: z.number(),\n /** Set when enrollment is confirmed (status flips to 'active'). */\n confirmedAt: z.number().optional(),\n /** Consecutive failed TOTP checks since the most recent success or lockout. */\n failedAttempts: z.number().int().nonnegative().optional(),\n /** Epoch ms until which TOTP verification is refused; absent or 0 means not locked. */\n lockedUntil: z.number().nonnegative().optional(),\n});\nexport type OperatorStepUp = z.infer<typeof OperatorStepUpSchema>;\n\n// safetyReconcilerCursors/{cursorKey} — one persisted pagination cursor per reconciler\n// needs-work backstop sweep (quarantine enqueue, NCMEC enqueue). The sweep pages a\n// needs-work-filtered query from `afterValue` and WRAPS to the start when exhausted, so\n// coverage does not stall on the oldest rows. (functions/src/safety/reconcilerCursor.ts)\nexport const SafetyReconcilerCursorSchema = z.object({\n /** `orderBy` value of the last row examined last pass (exclusive lower bound). Absent ⇒ 0. */\n afterValue: z.number().optional(),\n updatedAt: z.number(),\n});\nexport type SafetyReconcilerCursor = z.infer<typeof SafetyReconcilerCursorSchema>;\n\n// sweepState/{sweepName} — durable cadence + cursor state for one scheduled user sweep. One doc\n// per sweep name; the fields a given sweep uses are the ones it declares, so every field except\n// `updatedAt` is optional. `orphanRegistrationCleanup` carries all three (the ~daily privateData\n// reaper's last completed pass + its rolling page cursor, and the ~weekly `listUsers()` scan);\n// `reconcileAccountStatus` carries only `fullScanLastRunAt`. The stamps are wall-clock so a\n// container recycle cannot reset the cadence.\n// (functions/src/users/orphanRegistrationCleanup.ts, functions/src/users/reconcileAccountStatus.ts)\nexport const SweepStateSchema = z.object({\n /** Wall-clock ms the last COMPLETED privateData-reaper pass finished (absent = never). */\n reaperLastRunAt: z.number().optional(),\n /** Full document path of the last `privateData` doc the reaper examined. Empty/absent means no\n * pass is in flight; a non-empty value means one is mid-scan and must continue. */\n reaperCursorPath: z.string().optional(),\n /** Wall-clock ms the last full `listUsers()` scan ran (absent = never). */\n fullScanLastRunAt: z.number().optional(),\n updatedAt: z.number(),\n});\nexport type SweepState = z.infer<typeof SweepStateSchema>;\n\n// childSafetyCases/{caseId}/ncmecSubmissions/{submissionId}/ncmecCompletionProof/record —\n// [H-05] the IMMUTABLE record of a verified manual-portal completion. Written create-if-absent in\n// the SAME transaction as the submission's `completed` transition, so a submission can never reach\n// `completed` without a bound proof record; a re-completion keeps the original. The submission's\n// `completionProofRef` stores this doc's path, never an operator free-text string.\n// [Q13/H-06] `portalReceiptArtifactId` binds the proof to a verified NcmecPortalReceiptArtifactV1\n// on the same (caseId, submissionId) — an arbitrary operator string is never accepted.\n// (functions/src/safety/ncmecReporting.ts)\nexport const NcmecCompletionProofRecordV1Schema = z.object({\n caseId: z.string(),\n submissionId: z.string(),\n channel: ChildSafetyNcmecCompletionChannelSchema,\n proofType: ChildSafetyNcmecCompletionProofTypeSchema,\n /** Id of the verified portal-receipt artifact this completion is bound to. */\n portalReceiptArtifactId: z.string(),\n /** Optional operator free-text note about the confirmation. */\n proofText: z.string().optional(),\n /** The NCMEC-assigned report id — REQUIRED. The number IS the proof; there is no\n * \"filed, number pending\" grace. */\n ncmecReportId: z.string(),\n recordedByUid: z.string(),\n recordedAt: z.number(),\n});\nexport type NcmecCompletionProofRecordV1 = z.infer<typeof NcmecCompletionProofRecordV1Schema>;\n\n// childSafetyCases/{caseId}/portalReceiptArtifacts/{artifactId} — [Q13/H-06] the immutable\n// record of a PDF/screenshot of the completed NCMEC CyberTipline submission, stored in the\n// restricted evidence vault. `markNcmecPortalComplete` accepts only the id of a verified\n// artifact bound to the SAME (caseId, submissionId); an arbitrary operator string is never\n// proof. Written create-only, never overwritten, and carries the server-verified storage facts\n// (generation + sha256) so a later transactional verification can confirm the same object\n// still exists unchanged. (functions/src/safety/ncmecOperatorCommands.ts)\nexport const NcmecPortalReceiptArtifactV1Schema = z.object({\n schemaVersion: z.literal(1),\n caseId: z.string(),\n submissionId: z.string(),\n /** Key of the object in the restricted evidence vault bucket. */\n evidenceVaultKey: z.string(),\n /** GCS object generation at capture time (immutable identity peg). */\n objectGeneration: z.string(),\n /** SHA-256 hex digest of the object bytes at capture time. */\n sha256: z.string(),\n contentType: z.string(),\n sizeBytes: z.number(),\n capturedAt: z.number(),\n registeredByUid: z.string(),\n /** Optional operator free-text describing what the artifact is. */\n description: z.string().optional(),\n});\nexport type NcmecPortalReceiptArtifactV1 = z.infer<typeof NcmecPortalReceiptArtifactV1Schema>;\n\n// childSafetyCases/{caseId}/ncmecPortalCorrections/{correctionId} — the immutable record that\n// the operator filed a CORRECTION on the NCMEC manual portal. There is no in-app correction\n// submission pipeline; this records the portal filing. Its EXISTENCE is the gate on the\n// corrected-no-apparent-violation disposition, so it is never a bare-discretion close. Each\n// correction is a fresh id — a later correction is an additional record, never a mutation of\n// the prior one. (functions/src/safety/recordNcmecPortalCorrection.ts)\nexport const NcmecPortalCorrectionRecordV1Schema = z.object({\n schemaVersion: z.literal(1),\n caseId: z.string(),\n /** The portal-assigned NCMEC report id being corrected. */\n ncmecReportId: z.string(),\n /** When the operator filed the correction on the portal (operator-supplied). */\n correctionFiledAt: z.number(),\n reason: z.string(),\n recordedByUid: z.string(),\n recordedAt: z.number(),\n});\nexport type NcmecPortalCorrectionRecordV1 = z.infer<typeof NcmecPortalCorrectionRecordV1Schema>;\n"]}
|
|
1
|
+
{"version":3,"file":"backend-state.js","sourceRoot":"","sources":["../../src/doc-schemas/backend-state.ts"],"names":[],"mappings":"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","sourcesContent":["// Backend-only state document SCHEMAS — Cloud-Functions-owned bookkeeping docs that no\n// client ever reads or writes (firestore.rules denies client access to every path below).\n// They are registered here for the same reason every other stored shape is: ARCH-104 gives\n// the path ONE owner, and COLLECTION_SCHEMAS is the inventory the schema-doc generator and\n// the drift-check read. Types are inferred via z.infer.\n\nimport { z } from 'zod';\nimport { HALL_MEDIA_REAPER_MAX_DEFERRED } from '../constants/scheduled-jobs.js';\nimport {\n ChildSafetyNcmecCompletionChannelSchema,\n ChildSafetyNcmecCompletionProofTypeSchema,\n} from './safety/case.js';\n\n// One asset-phase candidate the Hall-media orphan reaper could not positively clear (a read\n// error, a missing owner). The cursor moves past it and the reaper retries it from here instead,\n// so one uncertain candidate never pins the scan window.\nexport const HallMediaReaperDeferredCandidateSchema = z\n .object({\n mediaAssetId: z.string().min(1),\n /** The candidate's mediaAssets `createdAt`. */\n createdAt: z.number().int().nonnegative(),\n /** Epoch ms from which a pass may retry it. */\n nextAttemptAt: z.number().int().nonnegative(),\n /** Passes that examined it without clearing it; the pass that deferred it is the first. The\n * HALL_MEDIA_REAPER_BACKOFF_BASE_MS / HALL_MEDIA_REAPER_BACKOFF_MAX_MS backoff grows from it. */\n attemptCount: z.number().int().positive(),\n })\n .strict();\nexport type HallMediaReaperDeferredCandidate = z.infer<typeof HallMediaReaperDeferredCandidateSchema>;\n\n// _systemData/hallMediaReaperCursor — the scheduled Hall-media orphan reaper's scan cursor.\n// `createdAtCursor` is the highest mediaAssets `createdAt` the reaper has moved past: every\n// candidate at or below it was positively cleared (referenced / reaped / already-deleted) or is\n// held in `deferred`, so each pass resumes past permanently-live Hall assets instead of\n// re-reading the same oldest page forever. `deferred` names each asset at most once, holds at\n// most HALL_MEDIA_REAPER_MAX_DEFERRED entries, and holds only candidates at or below the cursor\n// — deferring a candidate is what moves the cursor past it. A cursor written before the set\n// existed has no `deferred`, which reads as empty.\n// (functions/src/media/reapOrphanedHallMediaCopies.ts)\nexport const HallMediaReaperCursorSchema = z\n .object({\n createdAtCursor: z.number(),\n updatedAt: z.number(),\n deferred: z.array(HallMediaReaperDeferredCandidateSchema).max(HALL_MEDIA_REAPER_MAX_DEFERRED).optional(),\n })\n .superRefine((val, ctx) => {\n const seen = new Set<string>();\n (val.deferred ?? []).forEach((entry, index) => {\n if (seen.has(entry.mediaAssetId)) {\n ctx.addIssue({\n code: 'custom',\n path: ['deferred', index, 'mediaAssetId'],\n message: 'a candidate is deferred at most once',\n });\n }\n seen.add(entry.mediaAssetId);\n if (entry.createdAt > val.createdAtCursor) {\n ctx.addIssue({\n code: 'custom',\n path: ['deferred', index, 'createdAt'],\n message: 'a deferred candidate sits at or below the cursor that moved past it',\n });\n }\n });\n });\nexport type HallMediaReaperCursor = z.infer<typeof HallMediaReaperCursorSchema>;\n\n// _systemData/publicUsersReconcilerCursor — the scheduled publicUsers reconciler's sweep cursor.\n// `profileIdCursor` is the last `userProfiles` document id the sweep has POSITIVELY cleared, so\n// each pass resumes after it instead of re-reading the same oldest page forever. An EMPTY STRING\n// means start from the beginning — both the first-ever pass and the wrap after the sweep exhausts\n// the collection. (functions/src/users/reconcilePublicUsers.ts)\nexport const PublicUsersReconcilerCursorSchema = z.object({\n profileIdCursor: z.string(),\n updatedAt: z.number(),\n});\nexport type PublicUsersReconcilerCursor = z.infer<typeof PublicUsersReconcilerCursorSchema>;\n\n// operatorStepUp/{uid} — [H-08] per-operator TOTP step-up state: the authenticator secret\n// plus the currently open grant window. The secret is returned to the client EXACTLY ONCE by\n// enrollOperatorStepUp and is never readable through Firestore afterwards; `status` goes\n// 'pending' → 'active' on confirm, and each successful verify pushes `grantExpiresAt` out by\n// the step-up window. `failedAttempts` and `lockedUntil` make the server-side TOTP brute-force\n// lockout durable; omitted fields are the compatible no-lockout state for rows written before\n// that protection shipped. (functions/src/safety/operatorStepUp.ts)\nexport const OperatorStepUpSchema = z.object({\n secret: z.string(),\n status: z.enum(['pending', 'active']),\n /** Absent until the first successful verify; a grant is live while now < grantExpiresAt. */\n grantExpiresAt: z.number().optional(),\n createdAt: z.number(),\n updatedAt: z.number(),\n /** Set when enrollment is confirmed (status flips to 'active'). */\n confirmedAt: z.number().optional(),\n /** Consecutive failed TOTP checks since the most recent success or lockout. */\n failedAttempts: z.number().int().nonnegative().optional(),\n /** Epoch ms until which TOTP verification is refused; absent or 0 means not locked. */\n lockedUntil: z.number().nonnegative().optional(),\n});\nexport type OperatorStepUp = z.infer<typeof OperatorStepUpSchema>;\n\n// safetyReconcilerCursors/{cursorKey} — one persisted pagination cursor per reconciler\n// needs-work backstop sweep (quarantine enqueue, NCMEC enqueue). The sweep pages a\n// needs-work-filtered query from `afterValue` and WRAPS to the start when exhausted, so\n// coverage does not stall on the oldest rows. (functions/src/safety/reconcilerCursor.ts)\nexport const SafetyReconcilerCursorSchema = z.object({\n /** `orderBy` value of the last row examined last pass (exclusive lower bound). Absent ⇒ 0. */\n afterValue: z.number().optional(),\n updatedAt: z.number(),\n});\nexport type SafetyReconcilerCursor = z.infer<typeof SafetyReconcilerCursorSchema>;\n\n// sweepState/{sweepName} — durable cadence + cursor state for one scheduled user sweep. One doc\n// per sweep name; the fields a given sweep uses are the ones it declares, so every field except\n// `updatedAt` is optional. `orphanRegistrationCleanup` carries all three (the ~daily privateData\n// reaper's last completed pass + its rolling page cursor, and the ~weekly `listUsers()` scan);\n// `reconcileAccountStatus` carries only `fullScanLastRunAt`. The stamps are wall-clock so a\n// container recycle cannot reset the cadence.\n// (functions/src/users/orphanRegistrationCleanup.ts, functions/src/users/reconcileAccountStatus.ts)\nexport const SweepStateSchema = z.object({\n /** Wall-clock ms the last COMPLETED privateData-reaper pass finished (absent = never). */\n reaperLastRunAt: z.number().optional(),\n /** Full document path of the last `privateData` doc the reaper examined. Empty/absent means no\n * pass is in flight; a non-empty value means one is mid-scan and must continue. */\n reaperCursorPath: z.string().optional(),\n /** Wall-clock ms the last full `listUsers()` scan ran (absent = never). */\n fullScanLastRunAt: z.number().optional(),\n updatedAt: z.number(),\n});\nexport type SweepState = z.infer<typeof SweepStateSchema>;\n\n// childSafetyCases/{caseId}/ncmecSubmissions/{submissionId}/ncmecCompletionProof/record —\n// [H-05] the IMMUTABLE record of a verified manual-portal completion. Written create-if-absent in\n// the SAME transaction as the submission's `completed` transition, so a submission can never reach\n// `completed` without a bound proof record; a re-completion keeps the original. The submission's\n// `completionProofRef` stores this doc's path, never an operator free-text string.\n// [Q13/H-06] `portalReceiptArtifactId` binds the proof to a verified NcmecPortalReceiptArtifactV1\n// on the same (caseId, submissionId) — an arbitrary operator string is never accepted.\n// (functions/src/safety/ncmecReporting.ts)\nexport const NcmecCompletionProofRecordV1Schema = z.object({\n caseId: z.string(),\n submissionId: z.string(),\n channel: ChildSafetyNcmecCompletionChannelSchema,\n proofType: ChildSafetyNcmecCompletionProofTypeSchema,\n /** Id of the verified portal-receipt artifact this completion is bound to. */\n portalReceiptArtifactId: z.string(),\n /** Optional operator free-text note about the confirmation. */\n proofText: z.string().optional(),\n /** The NCMEC-assigned report id — REQUIRED. The number IS the proof; there is no\n * \"filed, number pending\" grace. */\n ncmecReportId: z.string(),\n recordedByUid: z.string(),\n recordedAt: z.number(),\n});\nexport type NcmecCompletionProofRecordV1 = z.infer<typeof NcmecCompletionProofRecordV1Schema>;\n\n// childSafetyCases/{caseId}/portalReceiptArtifacts/{artifactId} — [Q13/H-06] the immutable\n// record of a PDF/screenshot of the completed NCMEC CyberTipline submission, stored in the\n// restricted evidence vault. `markNcmecPortalComplete` accepts only the id of a verified\n// artifact bound to the SAME (caseId, submissionId); an arbitrary operator string is never\n// proof. Written create-only, never overwritten, and carries the server-verified storage facts\n// (generation + sha256) so a later transactional verification can confirm the same object\n// still exists unchanged. (functions/src/safety/ncmecOperatorCommands.ts)\nexport const NcmecPortalReceiptArtifactV1Schema = z.object({\n schemaVersion: z.literal(1),\n caseId: z.string(),\n submissionId: z.string(),\n /** Key of the object in the restricted evidence vault bucket. */\n evidenceVaultKey: z.string(),\n /** GCS object generation at capture time (immutable identity peg). */\n objectGeneration: z.string(),\n /** SHA-256 hex digest of the object bytes at capture time. */\n sha256: z.string(),\n contentType: z.string(),\n sizeBytes: z.number(),\n capturedAt: z.number(),\n registeredByUid: z.string(),\n /** Optional operator free-text describing what the artifact is. */\n description: z.string().optional(),\n});\nexport type NcmecPortalReceiptArtifactV1 = z.infer<typeof NcmecPortalReceiptArtifactV1Schema>;\n\n// childSafetyCases/{caseId}/ncmecPortalCorrections/{correctionId} — the immutable record that\n// the operator filed a CORRECTION on the NCMEC manual portal. There is no in-app correction\n// submission pipeline; this records the portal filing. Its EXISTENCE is the gate on the\n// corrected-no-apparent-violation disposition, so it is never a bare-discretion close. Each\n// correction is a fresh id — a later correction is an additional record, never a mutation of\n// the prior one. (functions/src/safety/recordNcmecPortalCorrection.ts)\nexport const NcmecPortalCorrectionRecordV1Schema = z.object({\n schemaVersion: z.literal(1),\n caseId: z.string(),\n /** The portal-assigned NCMEC report id being corrected. */\n ncmecReportId: z.string(),\n /** When the operator filed the correction on the portal (operator-supplied). */\n correctionFiledAt: z.number(),\n reason: z.string(),\n recordedByUid: z.string(),\n recordedAt: z.number(),\n});\nexport type NcmecPortalCorrectionRecordV1 = z.infer<typeof NcmecPortalCorrectionRecordV1Schema>;\n"]}
|
|
@@ -35,6 +35,8 @@ export declare const MediaCopyIntentSchema: z.ZodObject<{
|
|
|
35
35
|
}>;
|
|
36
36
|
createdAt: z.ZodNumber;
|
|
37
37
|
updatedAt: z.ZodNumber;
|
|
38
|
+
reapAfter: z.ZodNumber;
|
|
39
|
+
reapAttemptCount: z.ZodNumber;
|
|
38
40
|
reapClaimedAt: z.ZodOptional<z.ZodNumber>;
|
|
39
41
|
}, z.core.$strict>;
|
|
40
42
|
export type MediaCopyIntent = z.infer<typeof MediaCopyIntentSchema>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-copy-intents.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/media-copy-intents.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"media-copy-intents.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/media-copy-intents.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,0BAA0B;;;EAGrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAuC9B,CAAC;AACL,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// intent that outlives its copy is how the objects it names are found and removed.
|
|
6
6
|
// Server-only (`allow read, write: if false`).
|
|
7
7
|
//
|
|
8
|
-
// No TTL field: an intent removed before its
|
|
8
|
+
// No native-TTL field — `reapAfter` only schedules the sweep: an intent removed before its
|
|
9
|
+
// objects are reaped strands those objects.
|
|
9
10
|
import { z } from 'zod';
|
|
10
11
|
import { MEDIA_VARIANT_KEYS, MediaAssetOwnerTypeSchema, MediaVariantKeySchema } from './media-assets.js';
|
|
11
12
|
export const MediaCopyIntentStateSchema = z.enum([
|
|
@@ -23,6 +24,14 @@ export const MediaCopyIntentSchema = z
|
|
|
23
24
|
state: MediaCopyIntentStateSchema,
|
|
24
25
|
createdAt: z.number().int().nonnegative(),
|
|
25
26
|
updatedAt: z.number().int().nonnegative(),
|
|
27
|
+
// The earliest epoch ms the sweep may take this intent up; the sweep lists due intents in
|
|
28
|
+
// this order. Recording sets it HALL_MEDIA_ORPHAN_GRACE_MS out, so a copy in flight is never
|
|
29
|
+
// due; a sweep that defers the intent pushes it out again, so an intent that cannot finish
|
|
30
|
+
// yet rotates behind the ones that can instead of holding the head of every page.
|
|
31
|
+
reapAfter: z.number().int().nonnegative(),
|
|
32
|
+
// How many times the sweep has taken this intent up and deferred it — the count the
|
|
33
|
+
// HALL_MEDIA_REAPER_BACKOFF_BASE_MS / HALL_MEDIA_REAPER_BACKOFF_MAX_MS backoff grows from.
|
|
34
|
+
reapAttemptCount: z.number().int().nonnegative(),
|
|
26
35
|
reapClaimedAt: z.number().int().nonnegative().optional(),
|
|
27
36
|
})
|
|
28
37
|
.strict()
|
|
@@ -36,5 +45,12 @@ export const MediaCopyIntentSchema = z
|
|
|
36
45
|
if (val.state === 'copying' && val.reapClaimedAt !== undefined) {
|
|
37
46
|
ctx.addIssue({ code: 'custom', path: ['reapClaimedAt'], message: 'a copying intent has not been claimed' });
|
|
38
47
|
}
|
|
48
|
+
if (val.reapAfter < val.updatedAt) {
|
|
49
|
+
ctx.addIssue({
|
|
50
|
+
code: 'custom',
|
|
51
|
+
path: ['reapAfter'],
|
|
52
|
+
message: 'an intent is never due before the copy last recorded it',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
39
55
|
});
|
|
40
56
|
//# sourceMappingURL=media-copy-intents.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-copy-intents.js","sourceRoot":"","sources":["../../src/doc-schemas/media-copy-intents.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,wFAAwF;AACxF,0FAA0F;AAC1F,uFAAuF;AACvF,mFAAmF;AACnF,+CAA+C;AAC/C,EAAE;AACF,
|
|
1
|
+
{"version":3,"file":"media-copy-intents.js","sourceRoot":"","sources":["../../src/doc-schemas/media-copy-intents.ts"],"names":[],"mappings":"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","sourcesContent":["// mediaCopyIntents/{newAssetId} — the intent of one cross-owner media copy. A copy writes\n// its variant objects to deterministic keys BEFORE its asset doc exists, so a copy that\n// stops partway leaves objects no asset doc references. The intent is recorded before the\n// first object is copied and cleared in the transaction that creates the asset doc; an\n// intent that outlives its copy is how the objects it names are found and removed.\n// Server-only (`allow read, write: if false`).\n//\n// No native-TTL field — `reapAfter` only schedules the sweep: an intent removed before its\n// objects are reaped strands those objects.\n\nimport { z } from 'zod';\nimport { MEDIA_VARIANT_KEYS, MediaAssetOwnerTypeSchema, MediaVariantKeySchema } from './media-assets.js';\n\nexport const MediaCopyIntentStateSchema = z.enum([\n 'copying', // recorded before the first object is copied; the copy may still complete\n 'reaping', // claimed by the sweep, which is deleting the copied objects\n]);\nexport type MediaCopyIntentState = z.infer<typeof MediaCopyIntentStateSchema>;\n\nexport const MediaCopyIntentSchema = z\n .object({\n newAssetId: z.string().min(1),\n sourceAssetId: z.string().min(1),\n ownerType: MediaAssetOwnerTypeSchema,\n ownerId: z.string().min(1),\n // Variant NAMES, never object keys: each key derives from an asset id and a variant.\n variantKeys: z.array(MediaVariantKeySchema).min(1).max(MEDIA_VARIANT_KEYS.length),\n state: MediaCopyIntentStateSchema,\n createdAt: z.number().int().nonnegative(),\n updatedAt: z.number().int().nonnegative(),\n // The earliest epoch ms the sweep may take this intent up; the sweep lists due intents in\n // this order. Recording sets it HALL_MEDIA_ORPHAN_GRACE_MS out, so a copy in flight is never\n // due; a sweep that defers the intent pushes it out again, so an intent that cannot finish\n // yet rotates behind the ones that can instead of holding the head of every page.\n reapAfter: z.number().int().nonnegative(),\n // How many times the sweep has taken this intent up and deferred it — the count the\n // HALL_MEDIA_REAPER_BACKOFF_BASE_MS / HALL_MEDIA_REAPER_BACKOFF_MAX_MS backoff grows from.\n reapAttemptCount: z.number().int().nonnegative(),\n reapClaimedAt: z.number().int().nonnegative().optional(),\n })\n .strict()\n .superRefine((val, ctx) => {\n if (new Set(val.variantKeys).size !== val.variantKeys.length) {\n ctx.addIssue({ code: 'custom', path: ['variantKeys'], message: 'variantKeys names each variant at most once' });\n }\n if (val.state === 'reaping' && val.reapClaimedAt === undefined) {\n ctx.addIssue({ code: 'custom', path: ['reapClaimedAt'], message: 'a reaping intent records when it was claimed' });\n }\n if (val.state === 'copying' && val.reapClaimedAt !== undefined) {\n ctx.addIssue({ code: 'custom', path: ['reapClaimedAt'], message: 'a copying intent has not been claimed' });\n }\n if (val.reapAfter < val.updatedAt) {\n ctx.addIssue({\n code: 'custom',\n path: ['reapAfter'],\n message: 'an intent is never due before the copy last recorded it',\n });\n }\n });\nexport type MediaCopyIntent = z.infer<typeof MediaCopyIntentSchema>;\n"]}
|
|
@@ -4158,6 +4158,8 @@ export declare const COLLECTION_SCHEMAS: {
|
|
|
4158
4158
|
}>;
|
|
4159
4159
|
createdAt: z.ZodNumber;
|
|
4160
4160
|
updatedAt: z.ZodNumber;
|
|
4161
|
+
reapAfter: z.ZodNumber;
|
|
4162
|
+
reapAttemptCount: z.ZodNumber;
|
|
4161
4163
|
reapClaimedAt: z.ZodOptional<z.ZodNumber>;
|
|
4162
4164
|
}, z.core.$strict>;
|
|
4163
4165
|
readonly 'activeUserNotifications/{notificationId}': z.ZodObject<{
|
|
@@ -7276,6 +7278,12 @@ export declare const COLLECTION_SCHEMAS: {
|
|
|
7276
7278
|
readonly '_systemData/hallMediaReaperCursor': z.ZodObject<{
|
|
7277
7279
|
createdAtCursor: z.ZodNumber;
|
|
7278
7280
|
updatedAt: z.ZodNumber;
|
|
7281
|
+
deferred: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
7282
|
+
mediaAssetId: z.ZodString;
|
|
7283
|
+
createdAt: z.ZodNumber;
|
|
7284
|
+
nextAttemptAt: z.ZodNumber;
|
|
7285
|
+
attemptCount: z.ZodNumber;
|
|
7286
|
+
}, z.core.$strict>>>;
|
|
7279
7287
|
}, z.core.$strip>;
|
|
7280
7288
|
readonly '_systemData/publicUsersReconcilerCursor': z.ZodObject<{
|
|
7281
7289
|
profileIdCursor: z.ZodString;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/registry.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAoN7B,eAAO,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/registry.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAoN7B,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoQkB,CAAC;AAElD,MAAM,MAAM,wBAAwB,GAAG,MAAM,OAAO,kBAAkB,CAAC;AAEvE;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;CAmBiC,CAAC;AAEvE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAOvC,CAAC"}
|