pg-boss 12.33.2 → 12.33.4
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/README.md +1 -1
- package/dist/attorney.d.ts.map +1 -1
- package/dist/attorney.js +13 -12
- package/dist/bam.d.ts.map +1 -1
- package/dist/bam.js +74 -28
- package/dist/boss.d.ts +1 -1
- package/dist/boss.js +14 -14
- package/dist/claimTimer.d.ts +11 -4
- package/dist/claimTimer.d.ts.map +1 -1
- package/dist/claimTimer.js +14 -7
- package/dist/cli.js +15 -14
- package/dist/contractor.js +2 -2
- package/dist/db.js +2 -2
- package/dist/drifter.js +21 -21
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/manager.js +18 -18
- package/dist/migrationStore.js +11 -11
- package/dist/plans.d.ts +8 -7
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +117 -70
- package/dist/timekeeper.d.ts +34 -8
- package/dist/timekeeper.d.ts.map +1 -1
- package/dist/timekeeper.js +73 -12
- package/dist/types.d.ts +21 -21
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.js +1 -1
- package/package.json +6 -5
package/dist/plans.js
CHANGED
|
@@ -15,7 +15,7 @@ const SEVEN_DAYS = 60 * 60 * 24 * 7;
|
|
|
15
15
|
// A bam row stuck at 'in_progress' past this means the process that claimed it died or was
|
|
16
16
|
// stopped mid-command (bam.ts #processCommands returns without marking the row on #stopped or a
|
|
17
17
|
// crash), and getNextBamCommand needs to reclaim it or every future async migration wedges forever.
|
|
18
|
-
// This is the *fallback* backstop only
|
|
18
|
+
// This is the *fallback* backstop only, deliberately long (24h) because false-reclaim is the worse
|
|
19
19
|
// failure: reclaiming a still-running CREATE INDEX CONCURRENTLY runs two builds on the same index at
|
|
20
20
|
// once, and an interrupted CONCURRENTLY leaves an INVALID index needing manual cleanup. The intended
|
|
21
21
|
// *primary* trigger is a liveness check against pg_stat_progress_create_index (reclaim as soon as no
|
|
@@ -25,7 +25,7 @@ const BAM_STALE_SECONDS = 60 * 60 * 24;
|
|
|
25
25
|
// Liveness grace window: on the native-Postgres path we don't trust a "no build running" reading
|
|
26
26
|
// until a claimed command has had time to actually start and register in pg_stat_progress_create_index
|
|
27
27
|
// (pool latency between claiming the row and issuing the build). Below this age, an in_progress row is
|
|
28
|
-
// only reclaimed via the 24h fallback, never via liveness
|
|
28
|
+
// only reclaimed via the 24h fallback, never via liveness, so a genuinely-running build is never
|
|
29
29
|
// yanked out from under itself.
|
|
30
30
|
const BAM_LIVENESS_GRACE_SECONDS = 60 * 5;
|
|
31
31
|
export const JOB_STATES = Object.freeze({
|
|
@@ -323,7 +323,7 @@ function createTableSubscription(schema) {
|
|
|
323
323
|
`;
|
|
324
324
|
}
|
|
325
325
|
// created_on defaults to clock_timestamp(), not ${schema}.job_now(), so multiple job_table_run_async() enqueues
|
|
326
|
-
// within a single migration transaction keep their insertion order
|
|
326
|
+
// within a single migration transaction keep their insertion order, BAM applies queued commands in
|
|
327
327
|
// created_on order, and some migrations enqueue an ordered drop-then-rebuild pair (see v33).
|
|
328
328
|
/* eslint-enable no-restricted-syntax */
|
|
329
329
|
/* eslint-disable no-restricted-syntax -- bam.created_on orders several enqueues within one transaction; the schema clock would tie them */
|
|
@@ -825,14 +825,14 @@ function createIndexJobFetch(schema, noCoveringIndex = false) {
|
|
|
825
825
|
// linearly. INCLUDE cannot help: FOR UPDATE forces an Index Scan, never an Index Only Scan.
|
|
826
826
|
//
|
|
827
827
|
// - `id` is deliberately NOT a key column. The obvious shape, (name, priority DESC, created_on,
|
|
828
|
-
// id), also satisfies the ordering
|
|
828
|
+
// id), also satisfies the ordering, but id is a random uuid, so it defeats btree
|
|
829
829
|
// deduplication and dominates size: 24 MB against 19 MB for this shape at 500k rows, on the
|
|
830
830
|
// hot insert path and on every vacuum's bulkdelete. It buys nothing, because fetchNextJob
|
|
831
831
|
// dropped id from its ORDER BY for the same reason (a random uuid was never creation order);
|
|
832
832
|
// with the ordering ending at created_on, this index satisfies it outright and the plan
|
|
833
833
|
// carries no sort node at all.
|
|
834
834
|
//
|
|
835
|
-
// Also measured: neutral for key_strict_fifo (that fetch never reaches this index
|
|
835
|
+
// Also measured: neutral for key_strict_fifo (that fetch never reaches this index. The
|
|
836
836
|
// strict_fifo_heads CTE on job_i10 feeds the outer CTE), and a ~7x win on CockroachDB, which
|
|
837
837
|
// takes the noSkipLocked path and so does not depend on Incremental Sort at all.
|
|
838
838
|
//
|
|
@@ -996,23 +996,29 @@ export function setMonitorBackoff(schema, elapsedSeconds) {
|
|
|
996
996
|
export function trySetQueueDeletionTime(schema, queues, seconds) {
|
|
997
997
|
return trySetQueueTimestamp(schema, queues, 'maintain_on', seconds);
|
|
998
998
|
}
|
|
999
|
-
// The cron claim, which also answers with the timestamp it replaced
|
|
1000
|
-
// instance last ran a pass, so the pass reads it to find out how long
|
|
1001
|
-
// the window a schedule's `missed` policy catches up over. Null on a
|
|
1002
|
-
// against, where there is no gap to catch up on.
|
|
999
|
+
// The cron claim, which also answers with the timestamp it replaced and how old that timestamp was.
|
|
1000
|
+
// The timestamp is when an instance last ran a pass, so the pass reads it to find out how long
|
|
1001
|
+
// scheduling was off, which is the window a schedule's `missed` policy catches up over. Null on a
|
|
1002
|
+
// database no pass has ever run against, where there is no gap to catch up on.
|
|
1003
1003
|
//
|
|
1004
|
-
//
|
|
1004
|
+
// Both answers have to come from a CTE of their own: RETURNING sees the row as the UPDATE leaves
|
|
1005
1005
|
// it, while a WITH sub-statement reads the snapshot the whole statement was planned against, so it
|
|
1006
|
-
// sees the value the UPDATE is replacing.
|
|
1007
|
-
//
|
|
1006
|
+
// sees the value the UPDATE is replacing.
|
|
1007
|
+
//
|
|
1008
|
+
// This answers on both outcomes rather than on a claim alone, which is why `claimed` is a column
|
|
1009
|
+
// and not the row count: an instance that was refused needs to know when the row it lost to comes
|
|
1010
|
+
// due, so its next attempt can be measured from the claim that beat it rather than from its own
|
|
1011
|
+
// failure. See Timekeeper.onCron(). Reading `claim` through EXISTS does not make the UPDATE
|
|
1012
|
+
// conditional - a data-modifying CTE always runs, exactly once, whether or not the outer query
|
|
1013
|
+
// reads it.
|
|
1008
1014
|
export function trySetCronTime(schema, seconds) {
|
|
1009
1015
|
return `
|
|
1010
1016
|
WITH prior AS (
|
|
1011
|
-
SELECT cron_on FROM ${schema}.version
|
|
1017
|
+
SELECT cron_on, EXTRACT( EPOCH FROM (${schema}.job_now() - cron_on) ) as elapsed FROM ${schema}.version
|
|
1012
1018
|
), claim AS (
|
|
1013
1019
|
${trySetTimestamp(schema, 'cron_on', seconds)}
|
|
1014
1020
|
)
|
|
1015
|
-
SELECT prior.cron_on as "priorCronOn"
|
|
1021
|
+
SELECT prior.cron_on as "priorCronOn", prior.elapsed as "elapsed", EXISTS (SELECT 1 FROM claim) as "claimed" FROM prior
|
|
1016
1022
|
`;
|
|
1017
1023
|
}
|
|
1018
1024
|
export function trySetBamTime(schema, seconds) {
|
|
@@ -1318,8 +1324,8 @@ export function createIndexQueueStats(schema, noCoveringIndex = false) {
|
|
|
1318
1324
|
// explicit `+00` timestamptz literals. This keeps partitioning correct regardless of the database
|
|
1319
1325
|
// session TimeZone (a bare date literal like '2026-06-25' would otherwise be cast to timestamptz in
|
|
1320
1326
|
// the session TZ, so rows written near UTC midnight could fall outside every existing partition).
|
|
1321
|
-
// Computing the date in SQL (rather than interpolating new Date()) also keeps emitted DDL
|
|
1322
|
-
// the v35 migration and exported create plans
|
|
1327
|
+
// Computing the date in SQL (rather than interpolating new Date()) also keeps emitted DDL, including
|
|
1328
|
+
// the v35 migration and exported create plans, deterministic and apply-time accurate.
|
|
1323
1329
|
export function ensureQueueStatsPartitions(schema) {
|
|
1324
1330
|
return `
|
|
1325
1331
|
DO $$
|
|
@@ -1394,7 +1400,7 @@ export function insertQueueStats(schema, queues, noAdvisoryLocks) {
|
|
|
1394
1400
|
return locked(schema, sql, 'queue-stats-insert', noAdvisoryLocks);
|
|
1395
1401
|
}
|
|
1396
1402
|
// Cheap single-row read of the cached counts the monitor maintains on the queue table. capturedOn
|
|
1397
|
-
// is monitor_on
|
|
1403
|
+
// is monitor_on. The moment those counts were last refreshed, or NULL if the queue has never been
|
|
1398
1404
|
// monitored (so the caller knows to recompute rather than trust default-zero counts).
|
|
1399
1405
|
//
|
|
1400
1406
|
// monitorBackoff rides along because the caller must serve this cache even when it is stale while
|
|
@@ -1438,7 +1444,7 @@ export function getQueueStatsHistory(schema) {
|
|
|
1438
1444
|
`;
|
|
1439
1445
|
}
|
|
1440
1446
|
// Per-bucket aggregate over a count column. The function name can't be a bind parameter, so it's
|
|
1441
|
-
// interpolated
|
|
1447
|
+
// interpolated, safe because the manager validates `aggregate` against this whitelist first. Every
|
|
1442
1448
|
// result is cast back to int: it honors the int count contract (avg rounds) and keeps Postgres
|
|
1443
1449
|
// returning the value as a JS number rather than a numeric string.
|
|
1444
1450
|
const STATS_AGG = {
|
|
@@ -1450,8 +1456,8 @@ const STATS_AGG = {
|
|
|
1450
1456
|
// bucket's counts with `aggregate`, so a wide window returns a manageable, representative sample
|
|
1451
1457
|
// instead of just the newest `limit` raw rows.
|
|
1452
1458
|
//
|
|
1453
|
-
// mode 'bucket'
|
|
1454
|
-
// mode 'auto'
|
|
1459
|
+
// mode 'bucket', $5 is the bucket width in seconds (explicit resolution).
|
|
1460
|
+
// mode 'auto', $5 is maxDataPoints; the width is derived so the series fits in $5 points.
|
|
1455
1461
|
// from/to sets the range, but they cannot exceed the data's own min/max values.
|
|
1456
1462
|
//
|
|
1457
1463
|
// The bucket key avoids date_bin() (PG14+): pg-boss supports PostgreSQL 13+ and CockroachDB/
|
|
@@ -1668,7 +1674,7 @@ export function fetchNextJob(options, noSkipLocked = false) {
|
|
|
1668
1674
|
: '';
|
|
1669
1675
|
// state DESC (retry > created in the enum) makes a retry job its key's head. job_i8
|
|
1670
1676
|
// guarantees at most one job per key in active/retry/failed, and the NOT EXISTS blocker
|
|
1671
|
-
// below rejects every sibling of a retry job
|
|
1677
|
+
// below rejects every sibling of a retry job, so if created_on picked the head, an older
|
|
1672
1678
|
// deferred job whose start_after has since arrived would claim the head slot while being
|
|
1673
1679
|
// unfetchable, and the retry job (fetchable but not the head) would deadlock the key.
|
|
1674
1680
|
const strictFifoHeadsCte = keyStrictFifo
|
|
@@ -1715,13 +1721,13 @@ export function fetchNextJob(options, noSkipLocked = false) {
|
|
|
1715
1721
|
//
|
|
1716
1722
|
// The two conditionals this replaces existed only to serve the `priority` and `orderByCreatedOn`
|
|
1717
1723
|
// fetch options, which were requested as performance escapes from this sort. job_i11 now satisfies
|
|
1718
|
-
// the ordering directly, so the escapes have nothing to escape
|
|
1724
|
+
// the ordering directly, so the escapes have nothing to escape. See the deprecation in
|
|
1719
1725
|
// manager.fetch(). Dropping them also deletes two branches from the hottest SQL builder here.
|
|
1720
1726
|
//
|
|
1721
1727
|
// `id` goes too. It is a random uuid, so it never provided creation order: a batch insert shares
|
|
1722
1728
|
// one ${schema}.job_now() and therefore ties on created_on, and those ties resolve today in random uuid order,
|
|
1723
1729
|
// not insertion order. Without it the index satisfies the ordering outright rather than through
|
|
1724
|
-
// an Incremental Sort
|
|
1730
|
+
// an Incremental Sort (0.097 -> 0.031 ms and 50 -> 5 buffers at limit=1) and ties fall back to
|
|
1725
1731
|
// index order, which tracks insertion order better than a uuid does.
|
|
1726
1732
|
//
|
|
1727
1733
|
// key_strict_fifo keeps its own id tiebreak in strict_fifo_heads, where DISTINCT ON does need a
|
|
@@ -1929,7 +1935,7 @@ export function restoreJobs(schema, table) {
|
|
|
1929
1935
|
// A `startAfter` string is either an absolute date time or a delay expressed as a Postgres
|
|
1930
1936
|
// interval. A trailing 'Z' has always marked a date time; a leading ISO 8601 calendar date
|
|
1931
1937
|
// (YYYY-MM-DD) marks one as well, which is what lets the other 8601 zone designators through
|
|
1932
|
-
// ('+00:00', '+05:30', '-08:00') along with zone-less and date-only strings
|
|
1938
|
+
// ('+00:00', '+05:30', '-08:00') along with zone-less and date-only strings, all of which
|
|
1933
1939
|
// used to reach the interval cast and fail as 'invalid input syntax for type interval'.
|
|
1934
1940
|
//
|
|
1935
1941
|
// Recognition is only ever widened, so anything that resolves as an interval today still
|
|
@@ -2526,17 +2532,17 @@ export function insertDeadLetterJob(schema) {
|
|
|
2526
2532
|
// Dead-letter redrive. Moves un-started jobs out of a dead-letter queue and
|
|
2527
2533
|
// re-creates them as fresh jobs on their original source queue (or $2 destination override),
|
|
2528
2534
|
// oldest-first, capped at $4. The JOIN in `candidates` only matches jobs whose destination queue
|
|
2529
|
-
// exists, so legacy/orphaned jobs (NULL source_name, no override) are never deleted
|
|
2535
|
+
// exists, so legacy/orphaned jobs (NULL source_name, no override) are never deleted. They stay
|
|
2530
2536
|
// in the DLQ rather than being lost. Re-created jobs get a new id, `created` state, retry_count 0,
|
|
2531
2537
|
// cleared output, NULL source_*, and every queue-config column (retry/retention/policy/expiry/
|
|
2532
|
-
// heartbeat/dead_letter) from the destination queue as it is configured now
|
|
2538
|
+
// heartbeat/dead_letter) from the destination queue as it is configured now, per-job overrides
|
|
2533
2539
|
// from the original send() are not preserved, since the DLQ copy never stored them. `dead_letter`
|
|
2534
2540
|
// is the same value send() falls back to, so a second terminal failure re-enters the DLQ.
|
|
2535
2541
|
// Job-identity columns (priority, singleton_key, group_id, group_tier) are carried over instead.
|
|
2536
2542
|
//
|
|
2537
2543
|
// The insert's ON CONFLICT DO NOTHING is load-bearing: a destination queue's short/stately policy
|
|
2538
2544
|
// can still collide on (name, singleton_key) if two redriven jobs share a key (job_i1/job_i3), and
|
|
2539
|
-
// dropping just that row
|
|
2545
|
+
// dropping just that row (matching retried_jobs' ON CONFLICT DO NOTHING elsewhere) is preferable
|
|
2540
2546
|
// to aborting the whole batch. The dropped job has already been deleted from the DLQ by the moved
|
|
2541
2547
|
// CTE and is not restored.
|
|
2542
2548
|
export function redriveJobs(schema, table) {
|
|
@@ -2711,7 +2717,7 @@ export function getQueueStats(schema, table, queues) {
|
|
|
2711
2717
|
// Length of the recent-ready-count sliding window kept on queue.ready_history for the dashboard
|
|
2712
2718
|
// sparkline. One sample is appended per monitor cycle (default 60s), so this is roughly the last
|
|
2713
2719
|
// READY_HISTORY_SIZE minutes of trend. Sized to comfortably render the sparkline (the widest is the
|
|
2714
|
-
// ~160px detail card) without over-collecting
|
|
2720
|
+
// ~160px detail card) without over-collecting, more points than pixels add nothing visible.
|
|
2715
2721
|
export const READY_HISTORY_SIZE = 60;
|
|
2716
2722
|
/* eslint-disable no-restricted-syntax -- how long this transaction has held its snapshot: real elapsed time, not job time */
|
|
2717
2723
|
const PIN_SECONDS_SQL = 'EXTRACT(EPOCH FROM (clock_timestamp() - transaction_timestamp()))::float8';
|
|
@@ -2889,7 +2895,7 @@ function advisoryLock(schema, key) {
|
|
|
2889
2895
|
// winning the race to duplicate a scan nobody asked for costs the job table.
|
|
2890
2896
|
//
|
|
2891
2897
|
// The guard is a one-time filter, so a lost race skips the scan rather than running and discarding
|
|
2892
|
-
// it
|
|
2898
|
+
// it, verified on the plan: `Parallel Seq Scan on job (never executed)`. It also has to gate the
|
|
2893
2899
|
// UPDATE, not just the aggregate: an empty stats CTE still LEFT JOINs, and the COALESCE(…, 0) would
|
|
2894
2900
|
// write every count to zero.
|
|
2895
2901
|
function tryAdvisoryLock(schema, key, noAdvisoryLocks) {
|
|
@@ -3004,48 +3010,56 @@ export function getNextBamCommand(schema, { useLiveness = false } = {}) {
|
|
|
3004
3010
|
// a stuck in_progress row is reclaimed purely on the 24h fallback. No CONCURRENTLY healing, so no
|
|
3005
3011
|
// reclaimed flag is emitted (bam.ts skips healing when noIndexProgressView is set anyway).
|
|
3006
3012
|
return `
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
SELECT id FROM ${schema}.bam
|
|
3013
|
+
WITH candidate AS (
|
|
3014
|
+
SELECT c.id, c.status AS prior_status, c.started_on AS prior_started_on
|
|
3015
|
+
FROM ${schema}.bam c
|
|
3011
3016
|
WHERE (
|
|
3012
|
-
status IN ('pending', 'failed')
|
|
3013
|
-
OR (status = 'in_progress' AND started_on < ${schema}.job_now() - interval '${BAM_STALE_SECONDS} seconds')
|
|
3017
|
+
c.status IN ('pending', 'failed')
|
|
3018
|
+
OR (c.status = 'in_progress' AND c.started_on < ${schema}.job_now() - interval '${BAM_STALE_SECONDS} seconds')
|
|
3014
3019
|
)
|
|
3015
3020
|
AND NOT EXISTS (
|
|
3016
|
-
SELECT 1 FROM ${schema}.bam
|
|
3017
|
-
WHERE status = 'in_progress' AND started_on >= ${schema}.job_now() - interval '${BAM_STALE_SECONDS} seconds'
|
|
3021
|
+
SELECT 1 FROM ${schema}.bam g
|
|
3022
|
+
WHERE g.status = 'in_progress' AND g.started_on >= ${schema}.job_now() - interval '${BAM_STALE_SECONDS} seconds'
|
|
3018
3023
|
)
|
|
3019
|
-
ORDER BY (status != 'pending'), created_on
|
|
3024
|
+
ORDER BY (c.status != 'pending'), c.created_on
|
|
3020
3025
|
LIMIT 1
|
|
3021
3026
|
)
|
|
3022
|
-
|
|
3023
|
-
|
|
3027
|
+
UPDATE ${schema}.bam b
|
|
3028
|
+
SET status = 'in_progress', started_on = ${schema}.job_now()
|
|
3029
|
+
FROM candidate
|
|
3030
|
+
WHERE b.id = candidate.id
|
|
3031
|
+
AND b.status = candidate.prior_status
|
|
3032
|
+
AND b.started_on IS NOT DISTINCT FROM candidate.prior_started_on
|
|
3033
|
+
RETURNING b.id, b.name, b.version, b.status, b.queue, b.table_name as "table", b.command, b.error,
|
|
3034
|
+
b.created_on as "createdOn", b.started_on as "startedOn", b.completed_on as "completedOn",
|
|
3035
|
+
(candidate.prior_status <> 'pending') as reattempt,
|
|
3036
|
+
candidate.prior_status as "priorStatus", candidate.prior_started_on::text as "priorStartedOn",
|
|
3037
|
+
b.started_on::text as "claimedStartedOn"
|
|
3024
3038
|
`;
|
|
3025
3039
|
}
|
|
3026
3040
|
// Native-Postgres liveness path. An in_progress row counts as "stale" (reclaimable) when it is past
|
|
3027
3041
|
// the grace window AND no backend is actually building its index right now. The same predicate,
|
|
3028
|
-
// negated, defines a genuinely-live command that must still block the queue
|
|
3042
|
+
// negated, defines a genuinely-live command that must still block the queue, so a running build is
|
|
3029
3043
|
// NEVER reclaimed (no matter how long it runs), and a dead one recovers within the grace window.
|
|
3030
3044
|
// There is deliberately no 24h absolute cap here: liveBuild=true always means a build is in flight, so
|
|
3031
3045
|
// capping on elapsed time would reclaim a genuinely-running build and start a second
|
|
3032
|
-
// CREATE INDEX CONCURRENTLY on the same index
|
|
3046
|
+
// CREATE INDEX CONCURRENTLY on the same index. The exact double-build this path exists to prevent.
|
|
3033
3047
|
// (The timeout-only path's BAM_STALE_SECONDS fallback covers engines with no way to detect liveness.)
|
|
3034
3048
|
//
|
|
3035
3049
|
// liveBuild(tableCol): is a CREATE INDEX CONCURRENTLY actively building this table's index right now?
|
|
3036
|
-
// Detected via pg_locks, NOT pg_stat_progress_create_index
|
|
3050
|
+
// Detected via pg_locks, NOT pg_stat_progress_create_index, and that choice is load-bearing for
|
|
3037
3051
|
// multi-instance safety. pg_stat_progress_* is filtered to the querying role's OWN backends (only a
|
|
3038
3052
|
// superuser or a member of pg_read_all_stats sees another role's builds), so a progress-view check
|
|
3039
3053
|
// silently reads a peer's live build as "dead" whenever pg-boss instances connect under different DB
|
|
3040
|
-
// roles
|
|
3054
|
+
// roles, and the heal step (bamHealProbe/bamHealDrop in bam.ts) would then DROP INDEX CONCURRENTLY a
|
|
3041
3055
|
// live index mid-build, racing the builder into a double CREATE. pg_locks, by contrast, is cluster-wide
|
|
3042
3056
|
// and visible to every role (verified empirically). CREATE INDEX CONCURRENTLY holds a
|
|
3043
3057
|
// ShareUpdateExclusiveLock on the target table for the ENTIRE build and releases it the instant the
|
|
3044
3058
|
// statement finishes or the backend dies, so a granted SUExclusive lock on the row's table is a
|
|
3045
|
-
// crash-safe, role-agnostic "build in flight" signal
|
|
3059
|
+
// crash-safe, role-agnostic "build in flight" signal, instances may run under different roles with no
|
|
3046
3060
|
// loss of safety. Ordinary queue DML never takes SUExclusive (it uses AccessShare/RowShare/
|
|
3047
3061
|
// RowExclusive), so it can't false-trigger; a concurrent autovacuum/ANALYZE on the same table DOES take
|
|
3048
|
-
// SUExclusive and reads as "live", but that false positive is in the SAFE direction
|
|
3062
|
+
// SUExclusive and reads as "live", but that false positive is in the SAFE direction. It only briefly
|
|
3049
3063
|
// DEFERS a reclaim, never drops a live index. Scoped to the current database (l.database) because
|
|
3050
3064
|
// pg_locks is cluster-wide while relation OIDs are only unique per database. Correlating on the table
|
|
3051
3065
|
// is enough because the queue runs only one in_progress command at a time.
|
|
@@ -3077,7 +3091,7 @@ export function getNextBamCommand(schema, { useLiveness = false } = {}) {
|
|
|
3077
3091
|
// 'pending' rows have nothing to heal.
|
|
3078
3092
|
return `
|
|
3079
3093
|
WITH candidate AS (
|
|
3080
|
-
SELECT c.id, c.status AS prior_status
|
|
3094
|
+
SELECT c.id, c.status AS prior_status, c.started_on AS prior_started_on
|
|
3081
3095
|
FROM ${schema}.bam c
|
|
3082
3096
|
WHERE (
|
|
3083
3097
|
c.status IN ('pending', 'failed')
|
|
@@ -3095,9 +3109,13 @@ export function getNextBamCommand(schema, { useLiveness = false } = {}) {
|
|
|
3095
3109
|
SET status = 'in_progress', started_on = ${schema}.job_now()
|
|
3096
3110
|
FROM candidate
|
|
3097
3111
|
WHERE b.id = candidate.id
|
|
3112
|
+
AND b.status = candidate.prior_status
|
|
3113
|
+
AND b.started_on IS NOT DISTINCT FROM candidate.prior_started_on
|
|
3098
3114
|
RETURNING b.id, b.name, b.version, b.status, b.queue, b.table_name as "table", b.command, b.error,
|
|
3099
3115
|
b.created_on as "createdOn", b.started_on as "startedOn", b.completed_on as "completedOn",
|
|
3100
|
-
(candidate.prior_status <> 'pending') as reattempt
|
|
3116
|
+
(candidate.prior_status <> 'pending') as reattempt,
|
|
3117
|
+
candidate.prior_status as "priorStatus", candidate.prior_started_on::text as "priorStartedOn",
|
|
3118
|
+
b.started_on::text as "claimedStartedOn"
|
|
3101
3119
|
`;
|
|
3102
3120
|
}
|
|
3103
3121
|
// Derives the drop-then-rebuild heal step for a re-attempted BAM index build: an interrupted
|
|
@@ -3114,8 +3132,8 @@ export function bamHealDrop(schema, command) {
|
|
|
3114
3132
|
}
|
|
3115
3133
|
// Probe run before bamHealDrop: returns `invalid = true` only when the index the re-attempted command
|
|
3116
3134
|
// would build already exists AND is INVALID (an interrupted CREATE INDEX CONCURRENTLY left a stub).
|
|
3117
|
-
// A build that actually finished but whose BAM row was never marked completed
|
|
3118
|
-
// landed between the CREATE succeeding and markCompleted
|
|
3135
|
+
// A build that actually finished but whose BAM row was never marked completed, e.g. a graceful stop
|
|
3136
|
+
// landed between the CREATE succeeding and markCompleted, leaves a VALID index; dropping that would
|
|
3119
3137
|
// tear down a live production index for the whole rebuild window, so the caller must NOT heal it (the
|
|
3120
3138
|
// re-run's own IF NOT EXISTS then no-ops and just marks the row done). Returns null for non-index
|
|
3121
3139
|
// commands and for an absent index (no row), where there is nothing to drop. Mirrors bamHealDrop's
|
|
@@ -3132,10 +3150,39 @@ export function bamHealProbe(schema, command) {
|
|
|
3132
3150
|
WHERE n.nspname = '${resolveSchemaName(schema).replace(SINGLE_QUOTE_REGEX, "''")}' AND c.relname = '${match[1].replace(SINGLE_QUOTE_REGEX, "''")}'
|
|
3133
3151
|
`;
|
|
3134
3152
|
}
|
|
3153
|
+
// Undoes a claim that never ran: hands the row back exactly as getNextBamCommand found it, restoring
|
|
3154
|
+
// both the status and the started_on the claim overwrote. Used when a stop lands between the claim
|
|
3155
|
+
// UPDATE returning and the runner picking the command up. Leaving the row in_progress instead would
|
|
3156
|
+
// block every other BAM command until it went stale - BAM_LIVENESS_GRACE_SECONDS on native Postgres,
|
|
3157
|
+
// BAM_STALE_SECONDS (24 hours) on the timeout-only backends - and restoring started_on is what makes a
|
|
3158
|
+
// released stale-in_progress row immediately reclaimable again rather than starting that clock over.
|
|
3159
|
+
export function releaseBamCommand(schema, id, priorStatus, priorStartedOn, claimedStartedOn) {
|
|
3160
|
+
// priorStartedOn arrives as the text rendering of the original timestamptz (the claim casts it in
|
|
3161
|
+
// SQL rather than round-tripping a JS Date), so it carries its offset and full precision back.
|
|
3162
|
+
const startedOn = priorStartedOn == null
|
|
3163
|
+
? 'NULL'
|
|
3164
|
+
: `'${priorStartedOn.replace(SINGLE_QUOTE_REGEX, "''")}'::timestamptz`;
|
|
3165
|
+
// Compare-and-swap on the claim this runner actually took, not on the id alone. The timeout-only
|
|
3166
|
+
// claim has no SKIP LOCKED (see getNextBamCommand), so two overlapping claims can both return the
|
|
3167
|
+
// same row - verified: both get rowCount 1 and the same prior_status. Releasing on the id alone
|
|
3168
|
+
// would then reset a row a peer is actively building back to 'pending', and the next poll would
|
|
3169
|
+
// start a second CREATE INDEX CONCURRENTLY on the same index. Matching started_on makes the release
|
|
3170
|
+
// a no-op unless the claim is still ours.
|
|
3171
|
+
return `
|
|
3172
|
+
UPDATE ${schema}.bam
|
|
3173
|
+
SET status = '${priorStatus.replace(SINGLE_QUOTE_REGEX, "''")}', started_on = ${startedOn}
|
|
3174
|
+
WHERE id = '${id}'
|
|
3175
|
+
AND status = 'in_progress'
|
|
3176
|
+
AND started_on = '${claimedStartedOn.replace(SINGLE_QUOTE_REGEX, "''")}'::timestamptz
|
|
3177
|
+
`;
|
|
3178
|
+
}
|
|
3135
3179
|
export function setBamCompleted(schema, id) {
|
|
3180
|
+
// error is cleared: a row reaching 'completed' is commonly a retry of a prior 'failed' attempt, and
|
|
3181
|
+
// getBamEntries() returns error for every status - a stale message would keep reading as a failure
|
|
3182
|
+
// on a command that succeeded.
|
|
3136
3183
|
return `
|
|
3137
3184
|
UPDATE ${schema}.bam
|
|
3138
|
-
SET status = 'completed', completed_on = ${schema}.job_now()
|
|
3185
|
+
SET status = 'completed', completed_on = ${schema}.job_now(), error = NULL
|
|
3139
3186
|
WHERE id = '${id}'
|
|
3140
3187
|
`;
|
|
3141
3188
|
}
|
|
@@ -3176,7 +3223,7 @@ export function jobCommonExists(schema) {
|
|
|
3176
3223
|
export function getManagedQueuePartitions(schema) {
|
|
3177
3224
|
return `SELECT table_name as "table", policy FROM ${schema}.queue WHERE partition = true`;
|
|
3178
3225
|
}
|
|
3179
|
-
// The CREATE INDEX command text of every BAM row not yet completed
|
|
3226
|
+
// The CREATE INDEX command text of every BAM row not yet completed, used to tell a genuinely-missing
|
|
3180
3227
|
// index apart from one an async build is still working on (or retrying after a failure).
|
|
3181
3228
|
export function getIncompleteBamCommands(schema) {
|
|
3182
3229
|
return `SELECT command FROM ${schema}.bam WHERE status <> 'completed'`;
|
|
@@ -3199,7 +3246,7 @@ const POLICY_JOB_INDEXES = {
|
|
|
3199
3246
|
8: QUEUE_POLICIES.key_strict_fifo,
|
|
3200
3247
|
10: QUEUE_POLICIES.key_strict_fifo
|
|
3201
3248
|
};
|
|
3202
|
-
// job_iN indexes with no policy gate
|
|
3249
|
+
// job_iN indexes with no policy gate, created on every job table regardless of policy
|
|
3203
3250
|
// (throttle i4, fetch i11, group-concurrency i7, blocking i9). 5 is absent, not missing: the fetch
|
|
3204
3251
|
// index was replaced in v40 and the retired number is not reused.
|
|
3205
3252
|
const BASE_JOB_INDEXES = [4, 7, 9, 11];
|
|
@@ -3214,7 +3261,7 @@ function applyManifestSchema(text, schema) {
|
|
|
3214
3261
|
return text.split(schemaManifest.schemaToken).join(schema);
|
|
3215
3262
|
}
|
|
3216
3263
|
// The job_state enum values in declaration order, from the manifest (both sections carry the same enum).
|
|
3217
|
-
// Order is significant
|
|
3264
|
+
// Order is significant. The numeric base type makes created < retry < … < failed load-bearing.
|
|
3218
3265
|
export const EXPECTED_JOB_STATES = schemaManifest.partitioned.enum;
|
|
3219
3266
|
// The tables pg-boss expects to exist. The manifest lists the fixed tables plus job (and job_common in
|
|
3220
3267
|
// partitioned mode); per-queue partition tables are dynamic, so they are appended from the live set.
|
|
@@ -3228,7 +3275,7 @@ export function expectedManagedTables(schema, partitioned, partitions = []) {
|
|
|
3228
3275
|
// The columns pg-boss expects on each managed table, taken from the manifest (introspected column type,
|
|
3229
3276
|
// nullability, and default). Fixed tables carry the full defaults + types maps so column defaults, data
|
|
3230
3277
|
// types, and nullability are diffed; the job table (shared by job_common and every per-queue partition
|
|
3231
|
-
// in partitioned mode) is name-only
|
|
3278
|
+
// in partitioned mode) is name-only. Its FKs are profile-dependent and keep_until's interval default is
|
|
3232
3279
|
// not worth pinning per-partition. A table with no live columns is skipped by the diff, so listing one
|
|
3233
3280
|
// that does not exist yet is harmless.
|
|
3234
3281
|
export function expectedManagedColumns(schema, partitioned, partitions = []) {
|
|
@@ -3296,7 +3343,7 @@ export function expectedManagedFunctions(schema, partitioned, options = {}) {
|
|
|
3296
3343
|
// `partitioned` reflects the live database (job_common present ⇒ partitioned architecture), so this
|
|
3297
3344
|
// needs no boss config. The manifest holds every fixed index (the static ones plus the job_iN set on
|
|
3298
3345
|
// job/job_common); each per-queue partition is dynamic, so its indexes are templated here from the
|
|
3299
|
-
// job_common set
|
|
3346
|
+
// job_common set. The base indexes always, plus the one for the queue's policy. keys/predicate/
|
|
3300
3347
|
// definition are derived from the catalog-canonical pg_get_indexdef the manifest stores.
|
|
3301
3348
|
export function expectedManagedIndexes(schema, partitioned, partitions = []) {
|
|
3302
3349
|
const managed = (name, table, indexdef) => {
|
|
@@ -3330,13 +3377,13 @@ export function expectedManagedIndexes(schema, partitioned, partitions = []) {
|
|
|
3330
3377
|
//
|
|
3331
3378
|
// Autovacuum reclaims heap space but never returns btree pages to the OS, so a job index sizes
|
|
3332
3379
|
// itself to the largest backlog its queue has ever held and stays there. A drained 1M-job peak
|
|
3333
|
-
// leaves job_common_i11 at 38 MB and job_common_pkey at 51 MB holding 1k live rows
|
|
3380
|
+
// leaves job_common_i11 at 38 MB and job_common_pkey at 51 MB holding 1k live rows, and every
|
|
3334
3381
|
// subsequent vacuum with dead tuples to clean does a full physical scan of every one of those
|
|
3335
3382
|
// pages (measured: 17,969 buffer hits + 3,892 reads vs 750 hits / 0 reads after a rebuild), which
|
|
3336
3383
|
// is what drains IO burst credits on managed Postgres.
|
|
3337
3384
|
//
|
|
3338
|
-
// The bloat is a high-water mark rather than an unbounded leak
|
|
3339
|
-
// inserts
|
|
3385
|
+
// The bloat is a high-water mark rather than an unbounded leak, empty pages ARE recycled by later
|
|
3386
|
+
// inserts, so the trigger is density, not elapsed time.
|
|
3340
3387
|
export const REINDEX_DEFAULTS = {
|
|
3341
3388
|
// 1 MB. Below this the absolute waste is irrelevant and reltuples/relpages is noisy.
|
|
3342
3389
|
minPages: 128,
|
|
@@ -3365,8 +3412,8 @@ export const REINDEX_DEFAULTS = {
|
|
|
3365
3412
|
// int4 is not an option - the index that exceeds maxIndexBytes is exactly the one that overflows it.
|
|
3366
3413
|
// float8 is exact to 2^53 bytes, and reltuples is a float4 estimate to begin with.
|
|
3367
3414
|
// The pages the index's *live* entries actually need, from the average width of what it indexes.
|
|
3368
|
-
// pg_stats carries that per column, and
|
|
3369
|
-
// `COALESCE(singleton_key, '')
|
|
3415
|
+
// pg_stats carries that per column, and. The part that matters here, since i1-i4 and i6 key on
|
|
3416
|
+
// `COALESCE(singleton_key, '')`, ANALYZE also collects it for an index's expression columns, keyed
|
|
3370
3417
|
// by the index relation. Plain attnums read the table's stats, attnum 0 (an expression) falls back
|
|
3371
3418
|
// to the index's own. 20 bytes covers the btree tuple header and line pointer.
|
|
3372
3419
|
const INDEX_WIDTH_JOIN = `LEFT JOIN LATERAL (
|
|
@@ -3409,15 +3456,15 @@ function jobTableScope(schema, tables) {
|
|
|
3409
3456
|
* healthy one by three orders of magnitude on their own.
|
|
3410
3457
|
*
|
|
3411
3458
|
* PostgreSQL-only (including PGlite, where both the bloat and REINDEX CONCURRENTLY behave normally).
|
|
3412
|
-
* Callers must gate on noReindex
|
|
3459
|
+
* Callers must gate on noReindex. See the note in boss.ts #reindex.
|
|
3413
3460
|
*
|
|
3414
3461
|
* `owned` is returned rather than filtered on, so an installation whose role cannot reindex still
|
|
3415
|
-
* gets told what is bloated. Only leaf indexes (relkind 'i') are candidates
|
|
3462
|
+
* gets told what is bloated. Only leaf indexes (relkind 'i') are candidates. pg-boss creates
|
|
3416
3463
|
* i1..i10 directly on each partition, and a partitioned index (relkind 'I') cannot be reindexed
|
|
3417
3464
|
* except through its leaves anyway.
|
|
3418
3465
|
*
|
|
3419
3466
|
* Caveat: relpages/reltuples are refreshed by VACUUM/ANALYZE, so they go stale where autovacuum is
|
|
3420
|
-
* disabled for the table. That is acceptable
|
|
3467
|
+
* disabled for the table. That is acceptable. The entire failure mode assumes autovacuum runs.
|
|
3421
3468
|
*/
|
|
3422
3469
|
export function getBloatedIndexes(schema, tables, options) {
|
|
3423
3470
|
// These are interpolated into the statement, and supervise()/getReindexCommands() accept them
|
|
@@ -3454,7 +3501,7 @@ export function getBloatedIndexes(schema, tables, options) {
|
|
|
3454
3501
|
`;
|
|
3455
3502
|
}
|
|
3456
3503
|
/**
|
|
3457
|
-
* Every owned leaf index on the job tables, ignoring the density gate
|
|
3504
|
+
* Every owned leaf index on the job tables, ignoring the density gate. The target list for
|
|
3458
3505
|
* `{ force: true }`.
|
|
3459
3506
|
*/
|
|
3460
3507
|
export function getJobIndexes(schema, tables) {
|
|
@@ -3528,13 +3575,13 @@ function quoteIdentifier(name) {
|
|
|
3528
3575
|
*
|
|
3529
3576
|
* `pg_stat_activity.backend_xmin` is readable by an unprivileged role (it is not one of the columns
|
|
3530
3577
|
* restricted to the owning user), as are the other three views, so the degraded path is defensive
|
|
3531
|
-
* rather than expected
|
|
3578
|
+
* rather than expected. A managed provider may still revoke them.
|
|
3532
3579
|
*/
|
|
3533
3580
|
/* eslint-disable no-restricted-syntax -- these measure real backend and vacuum age against pg_stat_* timestamps Postgres wrote; a fake clock would compare two different clocks */
|
|
3534
3581
|
export const XMIN_HORIZON_SOURCES = {
|
|
3535
3582
|
// Restricted to backends whose transaction was already open when the failed vacuum ran ($1).
|
|
3536
3583
|
// Every backend executing a query advertises a backend_xmin, including the one asking this
|
|
3537
|
-
// question, so presence alone is not evidence
|
|
3584
|
+
// question, so presence alone is not evidence. The ordering against a vacuum that reclaimed
|
|
3538
3585
|
// nothing is. Compared as timestamps inside one statement rather than as two ages read from two
|
|
3539
3586
|
// queries a moment apart, which would let a connection that started after the vacuum qualify.
|
|
3540
3587
|
//
|
|
@@ -3543,7 +3590,7 @@ export const XMIN_HORIZON_SOURCES = {
|
|
|
3543
3590
|
// ordinary role only pid, application_name, usename and backend_xmin for a backend owned by a
|
|
3544
3591
|
// *different* role; xact_start reads NULL and query reads '<insufficient privilege>' (measured on
|
|
3545
3592
|
// PG18). Since `NULL < $1` is NULL, the strict form silently discarded exactly the holder an
|
|
3546
|
-
// operator most needs named
|
|
3593
|
+
// operator most needs named (another application's analytical query) leaving every source NULL,
|
|
3547
3594
|
// no holder attributable, and the warning withheld. Admitting them costs a possible false
|
|
3548
3595
|
// positive from a young foreign backend, which in practice loses the max(age()) to any real
|
|
3549
3596
|
// holder, and opaqueBackends below reports how many could not be timed.
|
|
@@ -3560,7 +3607,7 @@ export const XMIN_HORIZON_QUERY_SOURCES = Object.keys(XMIN_HORIZON_SOURCES);
|
|
|
3560
3607
|
* `age()` measures transactions elapsed since the pinned xid, not wall time, which is the metric
|
|
3561
3608
|
* that matters: dead tuples accumulate per transaction, so a horizon pinned across an idle night
|
|
3562
3609
|
* costs nothing while the same age on a busy queue is a backlog of rows autovacuum cannot reclaim.
|
|
3563
|
-
* Measured directly
|
|
3610
|
+
* Measured directly. A REPEATABLE READ holder open on an idle database reports 0, and 2,000 after
|
|
3564
3611
|
* 2,000 transactions with the same holder still open.
|
|
3565
3612
|
*
|
|
3566
3613
|
* Also returns the oldest in-transaction backend's wall-clock age, purely so the warning can say
|
|
@@ -3687,7 +3734,7 @@ export function dropIndexConcurrently(schema, name) {
|
|
|
3687
3734
|
* The statement list an operator runs by hand: every stale stub dropped first, then the rebuilds.
|
|
3688
3735
|
*
|
|
3689
3736
|
* Stubs are emitted whole rather than paired to the index they came from. Postgres does not append
|
|
3690
|
-
* `_ccnew
|
|
3737
|
+
* `_ccnew`. It truncates the *base* so the result fits in 63 bytes, so `j<sha224>_i11` (61 chars)
|
|
3691
3738
|
* leaves behind `j<sha224>__ccnew` with the `i11` gone, and a prefix match never fires on a
|
|
3692
3739
|
* partitioned queue (every partition table name is `'j' || sha224(queue_name)`, 57 chars). Dropping
|
|
3693
3740
|
* them unconditionally is also what the background pass does: an invalid stub is dead weight that
|
package/dist/timekeeper.d.ts
CHANGED
|
@@ -31,6 +31,28 @@ declare class Timekeeper extends EventEmitter implements types.EventsMixin {
|
|
|
31
31
|
private scheduleSkewMonitor;
|
|
32
32
|
cacheClockSkew(): Promise<void>;
|
|
33
33
|
onCron(): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* How long an instance whose claim was refused waits before trying again: until the row it lost
|
|
36
|
+
* to comes due, plus a beat.
|
|
37
|
+
*
|
|
38
|
+
* The beat keeps the retry off the due instant itself, where the holder's own attempt lands and
|
|
39
|
+
* where the row can still be a microsecond short of the interval. A quarter of the interval,
|
|
40
|
+
* capped at a second, so it stays a beat at every configurable interval instead of doubling the
|
|
41
|
+
* smallest one.
|
|
42
|
+
*
|
|
43
|
+
* No jitter. What it would buy is a spread the retries already have: each is measured from its
|
|
44
|
+
* own statement's snapshot and its own round trip, so several instances do not land on one
|
|
45
|
+
* instant, and a refused claim is a single-row UPDATE whose predicate fails. What it would cost
|
|
46
|
+
* is the one thing in this path a TestClock cannot drive to a known instant, which is what the
|
|
47
|
+
* regression test for this needs.
|
|
48
|
+
*
|
|
49
|
+
* `elapsed` is how old the row was in the snapshot the claim read, so a winner that commits
|
|
50
|
+
* between that snapshot and this statement's UPDATE leaves it measured against the previous
|
|
51
|
+
* stamp: the retry then comes back early, is refused, and measures again. Self-correcting, and it
|
|
52
|
+
* costs one extra statement in a race that has to happen inside a round trip. A null reads as a
|
|
53
|
+
* full interval, which is the row a pass has never stamped - a state no claim is refused in.
|
|
54
|
+
*/
|
|
55
|
+
private claimRetryDelay;
|
|
34
56
|
/**
|
|
35
57
|
* `priorCronOn` is when an instance last ran a pass, as the claim in onCron() read it off the row
|
|
36
58
|
* it advanced. Null on a database no pass has run against, and left null by a caller that does
|
|
@@ -63,14 +85,18 @@ declare class Timekeeper extends EventEmitter implements types.EventsMixin {
|
|
|
63
85
|
* The gap is (lastPass, windowStart]: older than the due window, so no pass has sent it, and
|
|
64
86
|
* newer than the moment an instance last ran a pass, so no pass has skipped it either. A pass
|
|
65
87
|
* claims `cronMonitorIntervalSeconds` after the one before it plus the round trip the claim
|
|
66
|
-
* itself cost
|
|
67
|
-
* the
|
|
68
|
-
* down, between deploys, or running
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
88
|
+
* itself cost and the beat a refused instance waits past the due moment, 46 seconds and change at
|
|
89
|
+
* the configurable ceiling, against a 60-second window, so the range is empty while passes keep
|
|
90
|
+
* running and fills up when they stop: a deployment that is down, between deploys, or running
|
|
91
|
+
* with scheduling switched off.
|
|
92
|
+
*
|
|
93
|
+
* That the spacing is an interval and not two of them takes both halves of onCron()'s answer. The
|
|
94
|
+
* claim timer schedules a winner's next attempt from the moment its claim stamped the row rather
|
|
95
|
+
* than on a grid, so a statement that lands quickly cannot measure the row short of the interval
|
|
96
|
+
* and lose the claim for a whole one. A loser schedules its next attempt for when the row it lost
|
|
97
|
+
* to comes due rather than an interval after its own failure, so the deployment keeps the spacing
|
|
98
|
+
* when the instance holding the claim stops rather than waiting out that instance's phase as
|
|
99
|
+
* well. See ClaimTimer and claimRetryDelay().
|
|
74
100
|
*
|
|
75
101
|
* The most recent occurrence rather than all of them, which is the whole of what `once` promises:
|
|
76
102
|
* a job carries the schedule's `data` and nothing else, so a job per missed occurrence would be
|
package/dist/timekeeper.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timekeeper.d.ts","sourceRoot":"","sources":["../src/timekeeper.ts"],"names":[],"mappings":"AAEA,OAAO,YAAY,MAAM,aAAa,CAAA;AAItC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAKvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAGnC,eAAO,MAAM,MAAM;;CAElB,CAAA;AAwBD,kGAAkG;AAClG,eAAO,MAAM,iBAAiB,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"timekeeper.d.ts","sourceRoot":"","sources":["../src/timekeeper.ts"],"names":[],"mappings":"AAEA,OAAO,YAAY,MAAM,aAAa,CAAA;AAItC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAKvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAGnC,eAAO,MAAM,MAAM;;CAElB,CAAA;AAwBD,kGAAkG;AAClG,eAAO,MAAM,iBAAiB,OAAO,CAAA;AAgLrC,cAAM,UAAW,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;IAChE,EAAE,EAAE,KAAK,CAAC,SAAS,CAAA;IACnB,MAAM,EAAE,KAAK,CAAC,0BAA0B,CAAA;IACxC,OAAO,EAAE,OAAO,CAAA;IAEhB,OAAO,CAAC,OAAO,CAAO;IACtB,OAAO,CAAC,gBAAgB,CAA+B;IACvD,OAAO,CAAC,gBAAgB,CAAqC;IAC7D,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,aAAa,CAAQ;IAS7B,OAAO,CAAC,eAAe,CAAoB;IAE3C,SAAS,SAAI;IACb,MAAM;;;;MAAS;gBAEF,EAAE,EAAE,KAAK,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAQ5F,IAAI,YAAY,IAAK,OAAO,CAE3B;IAMD,OAAO,KAAK,YAAY,GAEvB;IAED,OAAO,KAAK,cAAc,GASzB;IAEK,KAAK;IA4BL,IAAI;IAmCV,OAAO,CAAC,mBAAmB;IAUrB,cAAc;IA8Cd,MAAM;IAgDZ;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,eAAe;IAQvB;;;;;OAKG;IACG,IAAI,CAAE,WAAW,GAAE,OAAc;IA0IvC,YAAY,CAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,GAAE,KAAK,CAAC,YAAwC;IAIlG;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,cAAc;IAgBtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,OAAO,CAAC,gBAAgB;IAgBxB;;;;;;;OAOG;IACH,OAAO,CAAC,sBAAsB;IAgB9B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,eAAe;IAkBvB,OAAO,CAAC,iBAAiB;YAQX,QAAQ;YAsDR,aAAa;IAerB,YAAY,CAAE,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;IAiBrE,WAAW,CAAE,IAAI,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC;IAe1E;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IAYxB;;;;;;;;;;;;;;;;OAgBG;IACH,eAAe,CAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,KAAK,CAAC,sBAA2B,GAAG,IAAI,EAAE;IA0C5E,QAAQ,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,KAAK,CAAC,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IA6CzG,UAAU,CAAE,IAAI,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,OAAO,CAAC,IAAI,CAAC;CAIzD;AAED,eAAe,UAAU,CAAA"}
|