pg-boss 12.32.0 → 12.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/pglite.d.ts +1 -0
- package/dist/adapters/pglite.d.ts.map +1 -1
- package/dist/adapters/pglite.js +106 -2
- package/dist/attorney.d.ts.map +1 -1
- package/dist/attorney.js +12 -0
- package/dist/bam.d.ts.map +1 -1
- package/dist/bam.js +10 -5
- package/dist/boss.d.ts.map +1 -1
- package/dist/boss.js +24 -7
- package/dist/claimTimer.d.ts +44 -0
- package/dist/claimTimer.d.ts.map +1 -0
- package/dist/claimTimer.js +95 -0
- package/dist/cli.js +56 -2
- package/dist/clock.d.ts +31 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +156 -0
- package/dist/contractor.d.ts +5 -1
- package/dist/contractor.d.ts.map +1 -1
- package/dist/contractor.js +24 -2
- package/dist/db.d.ts +6 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +50 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +59 -27
- package/dist/manager.d.ts +2 -2
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +17 -17
- package/dist/migrationStore.d.ts.map +1 -1
- package/dist/migrationStore.js +210 -2
- package/dist/navigator.d.ts.map +1 -1
- package/dist/navigator.js +10 -5
- package/dist/plans.d.ts +17 -2
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +203 -83
- package/dist/schema.json +11 -3
- package/dist/timekeeper.d.ts +12 -6
- package/dist/timekeeper.d.ts.map +1 -1
- package/dist/timekeeper.js +64 -20
- package/dist/tools.d.ts +9 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +35 -8
- package/dist/types.d.ts +50 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.d.ts +3 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +12 -9
- package/package.json +6 -3
|
@@ -7,6 +7,7 @@ export interface PGliteLike {
|
|
|
7
7
|
rows: any[];
|
|
8
8
|
}>>;
|
|
9
9
|
listen?(channel: string, callback: (payload: string) => void): Promise<() => Promise<void>>;
|
|
10
|
+
onLeaderChange?(callback: () => void): () => void;
|
|
10
11
|
}
|
|
11
12
|
export declare function fromPglite(pglite: PGliteLike): IDatabase;
|
|
12
13
|
//# sourceMappingURL=pglite.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pglite.d.ts","sourceRoot":"","sources":["../../src/adapters/pglite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAM5C,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,EAAE,CAAA;KAAE,CAAC,CAAA;IACzE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC,CAAC,CAAA;IACpD,MAAM,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"pglite.d.ts","sourceRoot":"","sources":["../../src/adapters/pglite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAM5C,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,EAAE,CAAA;KAAE,CAAC,CAAA;IACzE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC,CAAC,CAAA;IACpD,MAAM,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3F,cAAc,CAAC,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAClD;AAmBD,wBAAgB,UAAU,CAAE,MAAM,EAAE,UAAU,GAAG,SAAS,CAwJzD"}
|
package/dist/adapters/pglite.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// PGliteWorker's error when leadership moves while a call is in flight. Matched on the message
|
|
2
|
+
// rather than the constructor name: the class is anonymous after bundling, so `name` is just
|
|
3
|
+
// 'Error', while the message is fixed in the worker source.
|
|
4
|
+
const LEADER_CHANGED_MESSAGE = 'Leader changed, pending operation in indeterminate state';
|
|
5
|
+
function isLeaderChange(err) {
|
|
6
|
+
return err instanceof Error && err.message === LEADER_CHANGED_MESSAGE;
|
|
7
|
+
}
|
|
1
8
|
// Adapts a PGlite instance (embedded single-connection WASM PostgreSQL) to pg-boss's IDatabase.
|
|
2
9
|
// PGlite is full PostgreSQL, so it needs none of the distributed compatibility flags — pair it
|
|
3
10
|
// with `backend: 'pglite'`. The user owns the PGlite instance lifecycle (construction and close).
|
|
@@ -23,17 +30,114 @@ export function fromPglite(pglite) {
|
|
|
23
30
|
const results = await pglite.exec(text);
|
|
24
31
|
return { rows: results.flatMap(r => r.rows ?? []) };
|
|
25
32
|
};
|
|
33
|
+
// The statements every session must carry, kept so they can be reapplied. A plain PGlite has one
|
|
34
|
+
// session for the life of the instance and never needs that; PGliteWorker does — see below.
|
|
35
|
+
let sessionStatements = [];
|
|
36
|
+
let unsubscribeLeaderChange = null;
|
|
37
|
+
// While a reapply is in flight, every statement waits for it. Without this a query issued between
|
|
38
|
+
// the leader change and the reapply lands on a session that has not been set up yet.
|
|
39
|
+
let reapplying = null;
|
|
40
|
+
// Statements currently in flight, so a leader change can fail them itself — see executeSql.
|
|
41
|
+
const inFlight = new Set();
|
|
42
|
+
// Note what this cannot fix: because one leader instance serves every tab, session state is shared
|
|
43
|
+
// by all of them. A SET issued through one tab's adapter applies to every other tab's queries
|
|
44
|
+
// against that database, and there is no way to scope it to the instance that asked for it.
|
|
45
|
+
//
|
|
46
|
+
// Only the PGliteWorker leader holds an actual PGlite instance; the other tabs proxy into it. When
|
|
47
|
+
// the leader tab goes away, the next tab's worker constructs a *new* PGlite over the same data
|
|
48
|
+
// directory — a new backend, and so a new session. Anything set with SET is gone, and nothing in
|
|
49
|
+
// the worker replays it, so without this the session would silently revert to defaults: for the
|
|
50
|
+
// clock override specifically, back to real time with no error anywhere.
|
|
51
|
+
const watchLeaderChange = () => {
|
|
52
|
+
if (unsubscribeLeaderChange || typeof pglite.onLeaderChange !== 'function') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
unsubscribeLeaderChange = pglite.onLeaderChange(() => {
|
|
56
|
+
// Fail the in-flight statements first: they were issued against a backend that no longer
|
|
57
|
+
// exists, and one of them may be holding a reapply behind it.
|
|
58
|
+
for (const fail of [...inFlight])
|
|
59
|
+
fail(new Error(LEADER_CHANGED_MESSAGE));
|
|
60
|
+
if (!sessionStatements.length)
|
|
61
|
+
return;
|
|
62
|
+
// Cleared only by the chain that set it: a second leader change during a reapply assigns a
|
|
63
|
+
// new one, and the first must not open the gate on a session the new chain has not set up.
|
|
64
|
+
const chain = applySessionStatements().catch(() => { }).then(() => {
|
|
65
|
+
if (reapplying === chain)
|
|
66
|
+
reapplying = null;
|
|
67
|
+
});
|
|
68
|
+
reapplying = chain;
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
// PGliteWorker settles a statement that was in flight across a leader change only when it was
|
|
72
|
+
// still queued on the transaction lock: that rejection is raised before _runExclusiveTransaction
|
|
73
|
+
// enters its try/finally, so it reaches us. A statement that had already *taken* the lock never
|
|
74
|
+
// settles at all — its own rpc rejects, but the `finally` then posts _releaseTransactionLock to a
|
|
75
|
+
// tab channel the new leader has not attached to yet, and nothing will ever reply or reject it.
|
|
76
|
+
// That await swallows the original rejection and hangs forever, which for pg-boss means a
|
|
77
|
+
// locked() block stalling a maintenance cycle silently and permanently.
|
|
78
|
+
//
|
|
79
|
+
// So the leader change fails its own in-flight statements, giving a lock holder the same
|
|
80
|
+
// indeterminate-state error a queued statement already gets. The abandoned promise is left to
|
|
81
|
+
// settle or not on its own; only its late rejection has to be swallowed.
|
|
82
|
+
const raceLeaderChange = async (text, values) => {
|
|
83
|
+
if (typeof pglite.onLeaderChange !== 'function') {
|
|
84
|
+
return await run(text, values);
|
|
85
|
+
}
|
|
86
|
+
let fail;
|
|
87
|
+
const lost = new Promise((_resolve, reject) => { fail = reject; });
|
|
88
|
+
inFlight.add(fail);
|
|
89
|
+
const statement = run(text, values);
|
|
90
|
+
statement.catch(() => { });
|
|
91
|
+
try {
|
|
92
|
+
return await Promise.race([statement, lost]);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
inFlight.delete(fail);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
// Through raceLeaderChange, not run(): a reapply orphaned by a *second* leader change is the
|
|
99
|
+
// same upstream hang as any other statement, and worse here, because executeSql waits on
|
|
100
|
+
// `reapplying` - a reapply that never settles stalls the adapter permanently and silently.
|
|
101
|
+
// Failing it instead leaves the next leader change to reapply from scratch.
|
|
102
|
+
const applySessionStatements = async () => {
|
|
103
|
+
for (const statement of sessionStatements) {
|
|
104
|
+
await raceLeaderChange(statement);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
26
107
|
const db = {
|
|
108
|
+
// A plain PGlite is one session for the life of the instance, so these are applied once and
|
|
109
|
+
// every later statement sees them. A pooled driver re-runs them per connection; here the only
|
|
110
|
+
// thing that can take the session away is a PGliteWorker leader change.
|
|
111
|
+
async setSessionStatements(statements) {
|
|
112
|
+
sessionStatements = statements;
|
|
113
|
+
await applySessionStatements();
|
|
114
|
+
},
|
|
27
115
|
async executeSql(text, values) {
|
|
116
|
+
// Re-checked, not awaited once: a second leader change during a reapply fails the first
|
|
117
|
+
// chain's in-flight statement, so that chain settles early and hands the gate back while the
|
|
118
|
+
// chain that replaced it has not reissued anything yet. Waiting again on whatever is there
|
|
119
|
+
// now is what keeps a query from reaching a session the current chain has not set up.
|
|
120
|
+
for (let pending = reapplying; pending; pending = reapplying) {
|
|
121
|
+
await pending;
|
|
122
|
+
}
|
|
28
123
|
try {
|
|
29
|
-
return await
|
|
124
|
+
return await raceLeaderChange(text, values);
|
|
30
125
|
}
|
|
31
126
|
catch (err) {
|
|
32
|
-
|
|
127
|
+
// A leader change leaves nothing to roll back on this side: the transaction died with the
|
|
128
|
+
// old leader's instance, and a ROLLBACK now would go to a different session that was never
|
|
129
|
+
// in it. The statement's own outcome is genuinely unknown, which is what the error says.
|
|
130
|
+
if (!isLeaderChange(err)) {
|
|
131
|
+
await pglite.query('ROLLBACK').catch(() => { });
|
|
132
|
+
}
|
|
33
133
|
throw err;
|
|
34
134
|
}
|
|
35
135
|
}
|
|
36
136
|
};
|
|
137
|
+
// Taken out once and kept for the adapter's life. It used to be tied to having session statements
|
|
138
|
+
// to reapply; it now also fails in-flight statements, which every adapter needs whether or not a
|
|
139
|
+
// TestClock is involved. A plain PGlite has no onLeaderChange and subscribes to nothing.
|
|
140
|
+
watchLeaderChange();
|
|
37
141
|
// PGlite is embedded single-connection PostgreSQL, so LISTEN/NOTIFY works entirely in-process:
|
|
38
142
|
// the same instance both NOTIFYs (via pg-boss's inlined pg_notify) and delivers to listeners.
|
|
39
143
|
// Only expose `listen` when the instance actually supports it (older builds/mocks may not), so
|
package/dist/attorney.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attorney.d.ts","sourceRoot":"","sources":["../src/attorney.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"attorney.d.ts","sourceRoot":"","sources":["../src/attorney.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,QAAA,MAAM,MAAM;;;;CAIX,CAAA;AAKD,QAAA,MAAM,mBAAmB,uQAaf,CAAA;AA2EV,iBAAS,iBAAiB,CAAE,MAAM,GAAE,GAAQ,QAc3C;AAgBD,iBAAS,mBAAmB,CAAE,KAAK,EAAE,MAAM,UAE1C;AAED,iBAAS,aAAa,CAAE,IAAI,EAAE,GAAG,GAAG,KAAK,CAAC,OAAO,CAgDhD;AAOD,iBAAS,eAAe,CAAE,IAAI,EAAE,GAAG,EAAE,EAAE,MAAc,EAAE;;CAAK,GAAG,KAAK,CAAC,OAAO,CA4D3E;AAED,iBAAS,mBAAmB,CAAE,MAAM,EAAE,GAAG,QAOxC;AAED,iBAAS,gBAAgB,CAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,QA2D/C;AA2GD,iBAAS,aAAa,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG;IAClD,OAAO,EAAE,KAAK,CAAC,mBAAmB,CAAA;IAClC,QAAQ,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;CACjC,CAgCA;AAqBD,iBAAS,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAQlD;AAED,iBAAS,SAAS,CAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,kBAAkB,GAAG,KAAK,CAAC,0BAA0B,CA0B9F;AA8GD,iBAAS,wBAAwB,CAAE,IAAI,EAAE,MAAM,QAiC9C;AAED,iBAAS,eAAe,CAAE,IAAI,EAAE,MAAM,QAIrC;AAED,iBAAS,SAAS,CAAE,GAAG,EAAE,MAAM,QAI9B;AA+LD,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,aAAa,EACb,SAAS,EACT,mBAAmB,EACnB,MAAM,EACN,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EAClB,CAAA"}
|
package/dist/attorney.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
|
+
import { systemClock } from "./clock.js";
|
|
2
3
|
import { DEFAULT_SCHEMA } from "./plans.js";
|
|
3
4
|
const POLICY = {
|
|
4
5
|
MAX_EXPIRATION_HOURS: 24,
|
|
@@ -409,6 +410,7 @@ function getConfig(value) {
|
|
|
409
410
|
config.useListenNotify = ('useListenNotify' in config) ? config.useListenNotify : false;
|
|
410
411
|
config.reindex = ('reindex' in config) ? config.reindex : true;
|
|
411
412
|
resolveBackend(config);
|
|
413
|
+
applyClockConfig(config);
|
|
412
414
|
applySchemaConfig(config);
|
|
413
415
|
applyOpsConfig(config);
|
|
414
416
|
applyScheduleConfig(config);
|
|
@@ -417,6 +419,16 @@ function getConfig(value) {
|
|
|
417
419
|
validateWarningConfig(config);
|
|
418
420
|
return config;
|
|
419
421
|
}
|
|
422
|
+
const CLOCK_METHODS = ['now', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'];
|
|
423
|
+
function applyClockConfig(config) {
|
|
424
|
+
if (config.clock) {
|
|
425
|
+
const clock = config.clock;
|
|
426
|
+
for (const method of CLOCK_METHODS) {
|
|
427
|
+
assert(typeof clock[method] === 'function', `configuration assert: clock must implement ${method}()`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
config.clock = config.clock || systemClock;
|
|
431
|
+
}
|
|
420
432
|
function applySchemaConfig(config) {
|
|
421
433
|
if (config.schema) {
|
|
422
434
|
assertPostgresObjectName(config.schema);
|
package/dist/bam.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bam.d.ts","sourceRoot":"","sources":["../src/bam.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"bam.d.ts","sourceRoot":"","sources":["../src/bam.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAItC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAOnC,cAAM,GAAI,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAOzD,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAU1C,IAAI,OAAO,IAAK,OAAO,CAEtB;IAEK,KAAK;IAcL,IAAI;CAkIX;AAED,eAAe,GAAG,CAAA"}
|
package/dist/bam.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import EventEmitter from 'node:events';
|
|
2
|
+
import { ClaimTimer } from "./claimTimer.js";
|
|
2
3
|
import * as plans from "./plans.js";
|
|
3
4
|
import { delay } from "./tools.js";
|
|
4
5
|
import * as types from "./types.js";
|
|
@@ -9,7 +10,7 @@ const events = {
|
|
|
9
10
|
class Bam extends EventEmitter {
|
|
10
11
|
#stopped;
|
|
11
12
|
#working;
|
|
12
|
-
#
|
|
13
|
+
#pollTimer;
|
|
13
14
|
#db;
|
|
14
15
|
#config;
|
|
15
16
|
events = events;
|
|
@@ -27,16 +28,17 @@ class Bam extends EventEmitter {
|
|
|
27
28
|
if (!this.#stopped)
|
|
28
29
|
return;
|
|
29
30
|
this.#stopped = false;
|
|
31
|
+
this.#pollTimer = new ClaimTimer(this.#config.clock, this.#config.bamIntervalSeconds, () => this.#onPoll());
|
|
30
32
|
setImmediate(() => this.#onPoll());
|
|
31
|
-
this.#
|
|
33
|
+
this.#pollTimer.start();
|
|
32
34
|
}
|
|
33
35
|
async stop() {
|
|
34
36
|
if (this.#stopped)
|
|
35
37
|
return;
|
|
36
38
|
this.#stopped = true;
|
|
37
|
-
if (this.#
|
|
38
|
-
|
|
39
|
-
this.#
|
|
39
|
+
if (this.#pollTimer) {
|
|
40
|
+
this.#pollTimer.stop();
|
|
41
|
+
this.#pollTimer = undefined;
|
|
40
42
|
}
|
|
41
43
|
while (this.#working) {
|
|
42
44
|
await delay(10);
|
|
@@ -55,6 +57,9 @@ class Bam extends EventEmitter {
|
|
|
55
57
|
}
|
|
56
58
|
const sql = plans.trySetBamTime(this.#config.schema, this.#config.bamIntervalSeconds);
|
|
57
59
|
const { rows } = await this.#db.executeSql(sql);
|
|
60
|
+
// The row is stamped; the next attempt is measured from here rather than from the tick that
|
|
61
|
+
// started this one. See ClaimTimer.
|
|
62
|
+
this.#pollTimer?.anchor();
|
|
58
63
|
if (rows.length === 1) {
|
|
59
64
|
await this.#processCommands();
|
|
60
65
|
}
|
package/dist/boss.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boss.d.ts","sourceRoot":"","sources":["../src/boss.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"boss.d.ts","sourceRoot":"","sources":["../src/boss.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAEtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAGvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAgHnC,cAAM,IAAK,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAsC1D,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAa1C,IAAI,WAAW,IAAK,OAAO,CAE1B;IAEK,KAAK;IAaL,IAAI;IAmGJ,SAAS,CAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,gBAAgB;IAorBvF;;;;OAIG;IACG,kBAAkB,CAAE,OAAO,CAAC,EAAE,KAAK,CAAC,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAwB7E;AAED,eAAe,IAAI,CAAA"}
|
package/dist/boss.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import EventEmitter from 'node:events';
|
|
2
|
+
import { ClaimTimer } from "./claimTimer.js";
|
|
2
3
|
import * as plans from "./plans.js";
|
|
3
4
|
import { delay, unwrapSQLResult } from "./tools.js";
|
|
4
5
|
import * as types from "./types.js";
|
|
@@ -75,7 +76,7 @@ class Boss extends EventEmitter {
|
|
|
75
76
|
#stopped;
|
|
76
77
|
#stopping;
|
|
77
78
|
#maintaining;
|
|
78
|
-
#
|
|
79
|
+
#superviseTimer;
|
|
79
80
|
#db;
|
|
80
81
|
#config;
|
|
81
82
|
#manager;
|
|
@@ -125,15 +126,16 @@ class Boss extends EventEmitter {
|
|
|
125
126
|
async start() {
|
|
126
127
|
if (this.#stopped) {
|
|
127
128
|
this.#stopping = false;
|
|
128
|
-
this.#
|
|
129
|
+
this.#superviseTimer = new ClaimTimer(this.#config.clock, this.#config.superviseIntervalSeconds, () => this.#onSupervise());
|
|
130
|
+
this.#superviseTimer.start();
|
|
129
131
|
this.#stopped = false;
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
async stop() {
|
|
133
135
|
if (!this.#stopped) {
|
|
134
136
|
this.#stopping = true;
|
|
135
|
-
if (this.#
|
|
136
|
-
|
|
137
|
+
if (this.#superviseTimer)
|
|
138
|
+
this.#superviseTimer.stop();
|
|
137
139
|
this.#stopped = true;
|
|
138
140
|
while (this.#maintaining) {
|
|
139
141
|
await delay(10);
|
|
@@ -154,6 +156,7 @@ class Boss extends EventEmitter {
|
|
|
154
156
|
if (typeof (query) === 'string') {
|
|
155
157
|
query = { text: query, values: [] };
|
|
156
158
|
}
|
|
159
|
+
// Real time: a stopwatch around I/O reads zero on a frozen clock.
|
|
157
160
|
const started = Date.now();
|
|
158
161
|
const result = unwrapSQLResult(await this.#db.executeSql(query.text, query.values));
|
|
159
162
|
const elapsed = (Date.now() - started) / 1000;
|
|
@@ -177,7 +180,11 @@ class Boss extends EventEmitter {
|
|
|
177
180
|
await delay(this.#config.__test__delay_maint_ms);
|
|
178
181
|
}
|
|
179
182
|
const queues = await this.#manager.getQueues();
|
|
180
|
-
|
|
183
|
+
// The timer's own pass is the only one that re-anchors it. A supervise() call from the
|
|
184
|
+
// application stamps the same claims, but it may be scoped to a single queue, and anchoring
|
|
185
|
+
// on it would push the background pass - the one that covers every other queue - out by a
|
|
186
|
+
// full interval each time. An application polling one hot queue would starve the rest.
|
|
187
|
+
!this.#stopped && (await this.#supervisePass(queues, undefined, () => this.#superviseTimer?.anchor()));
|
|
181
188
|
}
|
|
182
189
|
catch (err) {
|
|
183
190
|
this.emit(events.error, err);
|
|
@@ -207,6 +214,11 @@ class Boss extends EventEmitter {
|
|
|
207
214
|
await this.#executeQuery(sql);
|
|
208
215
|
}
|
|
209
216
|
async supervise(value, options) {
|
|
217
|
+
await this.#supervisePass(value, options);
|
|
218
|
+
}
|
|
219
|
+
// onClaimsSettled fires once every monitor and maintain claim in the pass has been stamped, which
|
|
220
|
+
// is where the background timer measures its next attempt from. Only #onSupervise passes one.
|
|
221
|
+
async #supervisePass(value, options, onClaimsSettled) {
|
|
210
222
|
let queues;
|
|
211
223
|
if (Array.isArray(value)) {
|
|
212
224
|
queues = value;
|
|
@@ -241,6 +253,11 @@ class Boss extends EventEmitter {
|
|
|
241
253
|
await this.#maintain(table, chunk);
|
|
242
254
|
}
|
|
243
255
|
}
|
|
256
|
+
// Every monitor and maintain claim in this pass has now been stamped, so the next pass is
|
|
257
|
+
// measured from here rather than from the tick that started this one. See ClaimTimer. Before
|
|
258
|
+
// the tail below and not at the end of the pass: the tail ends in a rebuild that is DDL and can
|
|
259
|
+
// run for seconds, and the claims must not be measured from the far side of it.
|
|
260
|
+
onClaimsSettled?.();
|
|
244
261
|
if (this.#stopping)
|
|
245
262
|
return;
|
|
246
263
|
// Immediately after the aggregates it measures, not at the head of the next pass. Deferring the
|
|
@@ -701,9 +718,9 @@ class Boss extends EventEmitter {
|
|
|
701
718
|
return;
|
|
702
719
|
}
|
|
703
720
|
else {
|
|
704
|
-
if (
|
|
721
|
+
if (this.#config.clock.now() < this.#detectOnly)
|
|
705
722
|
return;
|
|
706
|
-
this.#detectOnly =
|
|
723
|
+
this.#detectOnly = this.#config.clock.now() + this.#config.reindexIntervalSeconds * 1000;
|
|
707
724
|
}
|
|
708
725
|
}
|
|
709
726
|
if (this.#stopping)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Clock } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The timer behind an interval claim, anchored to the claim instead of to a fixed grid.
|
|
4
|
+
*
|
|
5
|
+
* An interval claim is a conditional UPDATE that only goes through when the row it stamps is at
|
|
6
|
+
* least `seconds` old by the server's clock, so exactly one instance in a deployment runs the pass
|
|
7
|
+
* per interval (see plans.trySetTimestamp). Every instance tries on a timer of its own.
|
|
8
|
+
*
|
|
9
|
+
* setInterval schedules the next tick from a grid fixed before the callback runs, while the row is
|
|
10
|
+
* stamped when the UPDATE reaches the server - a moment later by however long the pool wait, the
|
|
11
|
+
* round trip and the plan took. The two do not move together. When a tick's statement lands faster
|
|
12
|
+
* than the previous tick's did, the two stamps come out closer together than the period, the claim
|
|
13
|
+
* is refused, and the pass after it is a whole interval late. Measured on an idle Linux box that is
|
|
14
|
+
* 4 of 19 ticks at a 45-second period, all of them between 44.990 and 44.998 seconds old, and a
|
|
15
|
+
* quarter of the queue monitor's passes at its defaults. For the cron pass a lost claim is a
|
|
16
|
+
* 90-second gap against a 60-second due window, and the occurrences inside it are never sent.
|
|
17
|
+
*
|
|
18
|
+
* Anchoring removes the mismatch rather than tolerating it: the next attempt is scheduled once the
|
|
19
|
+
* claim has been stamped, so the gap between two stamps is the period plus one round trip rather
|
|
20
|
+
* than the period minus the difference between two of them. It can only ever come out long, never
|
|
21
|
+
* short, which is the direction that costs nothing - a pass never runs before its interval is up.
|
|
22
|
+
*
|
|
23
|
+
* Note what does not appear here: a clock. The claim compares two timestamps that the server wrote,
|
|
24
|
+
* so the client's clock is already absent from it and skew between the two cancels exactly. There
|
|
25
|
+
* is nothing for a skew correction to correct, which is why the fix is in when the attempt is made
|
|
26
|
+
* rather than in what it is measured against.
|
|
27
|
+
*
|
|
28
|
+
* `anchor()` is what a pass calls once its claim has been stamped. A pass that returns without ever
|
|
29
|
+
* reaching its claim - stopped, already working, an error on the way in - is re-armed from the end
|
|
30
|
+
* of the callback instead, so the chain cannot die on a path that never anchored it.
|
|
31
|
+
*/
|
|
32
|
+
export declare class ClaimTimer {
|
|
33
|
+
#private;
|
|
34
|
+
constructor(clock: Clock, seconds: number, fn: () => Promise<void>);
|
|
35
|
+
start(): void;
|
|
36
|
+
stop(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Re-anchors the next attempt to now, because the claim this timer drives has just been stamped.
|
|
39
|
+
* Called whether the claim was won or lost: the winner needs its next attempt to fall after its
|
|
40
|
+
* own stamp, and a loser's phase is its own business either way.
|
|
41
|
+
*/
|
|
42
|
+
anchor(): void;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=claimTimer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claimTimer.d.ts","sourceRoot":"","sources":["../src/claimTimer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAc,MAAM,YAAY,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,UAAU;;gBASR,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC;IAMnE,KAAK,IAAK,IAAI;IAOd,IAAI,IAAK,IAAI;IAKb;;;;OAIG;IACH,MAAM,IAAK,IAAI;CAwChB"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The timer behind an interval claim, anchored to the claim instead of to a fixed grid.
|
|
3
|
+
*
|
|
4
|
+
* An interval claim is a conditional UPDATE that only goes through when the row it stamps is at
|
|
5
|
+
* least `seconds` old by the server's clock, so exactly one instance in a deployment runs the pass
|
|
6
|
+
* per interval (see plans.trySetTimestamp). Every instance tries on a timer of its own.
|
|
7
|
+
*
|
|
8
|
+
* setInterval schedules the next tick from a grid fixed before the callback runs, while the row is
|
|
9
|
+
* stamped when the UPDATE reaches the server - a moment later by however long the pool wait, the
|
|
10
|
+
* round trip and the plan took. The two do not move together. When a tick's statement lands faster
|
|
11
|
+
* than the previous tick's did, the two stamps come out closer together than the period, the claim
|
|
12
|
+
* is refused, and the pass after it is a whole interval late. Measured on an idle Linux box that is
|
|
13
|
+
* 4 of 19 ticks at a 45-second period, all of them between 44.990 and 44.998 seconds old, and a
|
|
14
|
+
* quarter of the queue monitor's passes at its defaults. For the cron pass a lost claim is a
|
|
15
|
+
* 90-second gap against a 60-second due window, and the occurrences inside it are never sent.
|
|
16
|
+
*
|
|
17
|
+
* Anchoring removes the mismatch rather than tolerating it: the next attempt is scheduled once the
|
|
18
|
+
* claim has been stamped, so the gap between two stamps is the period plus one round trip rather
|
|
19
|
+
* than the period minus the difference between two of them. It can only ever come out long, never
|
|
20
|
+
* short, which is the direction that costs nothing - a pass never runs before its interval is up.
|
|
21
|
+
*
|
|
22
|
+
* Note what does not appear here: a clock. The claim compares two timestamps that the server wrote,
|
|
23
|
+
* so the client's clock is already absent from it and skew between the two cancels exactly. There
|
|
24
|
+
* is nothing for a skew correction to correct, which is why the fix is in when the attempt is made
|
|
25
|
+
* rather than in what it is measured against.
|
|
26
|
+
*
|
|
27
|
+
* `anchor()` is what a pass calls once its claim has been stamped. A pass that returns without ever
|
|
28
|
+
* reaching its claim - stopped, already working, an error on the way in - is re-armed from the end
|
|
29
|
+
* of the callback instead, so the chain cannot die on a path that never anchored it.
|
|
30
|
+
*/
|
|
31
|
+
export class ClaimTimer {
|
|
32
|
+
#clock;
|
|
33
|
+
#ms;
|
|
34
|
+
#fn;
|
|
35
|
+
#handle;
|
|
36
|
+
#stopped = true;
|
|
37
|
+
#anchored = false;
|
|
38
|
+
constructor(clock, seconds, fn) {
|
|
39
|
+
this.#clock = clock;
|
|
40
|
+
this.#ms = seconds * 1000;
|
|
41
|
+
this.#fn = fn;
|
|
42
|
+
}
|
|
43
|
+
start() {
|
|
44
|
+
if (!this.#stopped)
|
|
45
|
+
return;
|
|
46
|
+
this.#stopped = false;
|
|
47
|
+
this.#arm();
|
|
48
|
+
}
|
|
49
|
+
stop() {
|
|
50
|
+
this.#stopped = true;
|
|
51
|
+
this.#disarm();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Re-anchors the next attempt to now, because the claim this timer drives has just been stamped.
|
|
55
|
+
* Called whether the claim was won or lost: the winner needs its next attempt to fall after its
|
|
56
|
+
* own stamp, and a loser's phase is its own business either way.
|
|
57
|
+
*/
|
|
58
|
+
anchor() {
|
|
59
|
+
if (this.#stopped)
|
|
60
|
+
return;
|
|
61
|
+
this.#anchored = true;
|
|
62
|
+
this.#arm();
|
|
63
|
+
}
|
|
64
|
+
#arm() {
|
|
65
|
+
this.#disarm();
|
|
66
|
+
if (this.#stopped)
|
|
67
|
+
return;
|
|
68
|
+
this.#handle = this.#clock.setTimeout(() => { this.#run(); }, this.#ms);
|
|
69
|
+
}
|
|
70
|
+
#disarm() {
|
|
71
|
+
if (this.#handle !== undefined) {
|
|
72
|
+
this.#clock.clearTimeout(this.#handle);
|
|
73
|
+
this.#handle = undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async #run() {
|
|
77
|
+
this.#handle = undefined;
|
|
78
|
+
this.#anchored = false;
|
|
79
|
+
// Deliberately not caught. Every pass this drives ends its own catch in an error event, so the
|
|
80
|
+
// only rejection that reaches here is one that could not be reported - an error event with
|
|
81
|
+
// nothing listening, which EventEmitter throws on. That belongs to the process, the way it did
|
|
82
|
+
// when these ran on setInterval; a timer that turned it into anything else would be inventing a
|
|
83
|
+
// failure mode for the host to handle. What the timer does owe is the chain, and finally pays
|
|
84
|
+
// that whether the pass resolved or not.
|
|
85
|
+
try {
|
|
86
|
+
await this.#fn();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
// A pass that reached its claim already armed the next attempt through anchor(); this is the
|
|
90
|
+
// path for one that did not, including one that threw.
|
|
91
|
+
if (!this.#anchored)
|
|
92
|
+
this.#arm();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -39,6 +39,8 @@ Options:
|
|
|
39
39
|
Non-postgres backends need this to emit schema they accept.
|
|
40
40
|
--dry-run Output SQL without executing (for plans and reindex commands)
|
|
41
41
|
--force Rebuild every job index, not just the bloated ones (reindex)
|
|
42
|
+
--fix Restore a job_now() left overridden by a killed TestClock run
|
|
43
|
+
(doctor). Only run this when no instance holds a live TestClock.
|
|
42
44
|
|
|
43
45
|
Environment Variables:
|
|
44
46
|
PGBOSS_DATABASE_URL Full connection string
|
|
@@ -161,7 +163,8 @@ function parseCliArgs() {
|
|
|
161
163
|
ssl: { type: 'boolean' },
|
|
162
164
|
backend: { type: 'string' },
|
|
163
165
|
'dry-run': { type: 'boolean' },
|
|
164
|
-
force: { type: 'boolean' }
|
|
166
|
+
force: { type: 'boolean' },
|
|
167
|
+
fix: { type: 'boolean' }
|
|
165
168
|
},
|
|
166
169
|
allowPositionals: true
|
|
167
170
|
});
|
|
@@ -179,6 +182,7 @@ function parseCliArgs() {
|
|
|
179
182
|
backend: values.backend,
|
|
180
183
|
dryRun: values['dry-run'],
|
|
181
184
|
force: values.force,
|
|
185
|
+
fix: values.fix,
|
|
182
186
|
command: positionals[0],
|
|
183
187
|
subCommand: positionals[1]
|
|
184
188
|
};
|
|
@@ -385,7 +389,32 @@ async function cmdDoctor(args) {
|
|
|
385
389
|
// carried divergent best-effort/backend-gating bugs). Contractor.detectDrift handles the
|
|
386
390
|
// partitioned probe, best-effort catalog fallbacks, and backend-specific gating in one place.
|
|
387
391
|
const contractor = new Contractor(db, { ...config, schema });
|
|
388
|
-
|
|
392
|
+
let report = await contractor.detectDrift();
|
|
393
|
+
// --fix repairs exactly one cause of drift, and only when asked. A TestClock restores job_now()
|
|
394
|
+
// when its handle is disposed, so a run killed first leaves the override installed; but a
|
|
395
|
+
// leftover override is indistinguishable from one a peer instance is holding right now, which is
|
|
396
|
+
// why this cannot be done automatically at startup. An operator knows which it is. Everything
|
|
397
|
+
// else doctor finds is reported and never repaired.
|
|
398
|
+
if (args.fix) {
|
|
399
|
+
// The drift report is the first source, but not the only one: mismatchedFunctions comes from
|
|
400
|
+
// pg_get_functiondef, and the whole function check is skipped when that query is unsupported
|
|
401
|
+
// (CockroachDB). detectClockOverride() reads prosrc instead, so a leftover override is still
|
|
402
|
+
// found - and repaired - on a backend whose functions never reach the report at all.
|
|
403
|
+
const override = report.mismatchedFunctions.some(f => f.name === 'job_now' && plans.clockFunctionIsOverridden(f.actualDefinition)) ||
|
|
404
|
+
await contractor.detectClockOverride();
|
|
405
|
+
if (override) {
|
|
406
|
+
console.log('\njob_now() carries a TestClock override, left behind by a test run that was');
|
|
407
|
+
console.log('killed before releasing its clock. Restoring it — make sure no instance is');
|
|
408
|
+
console.log('holding a live TestClock against this schema.');
|
|
409
|
+
await contractor.restoreClockFunction();
|
|
410
|
+
console.log(' restored.');
|
|
411
|
+
// Report on the repaired schema, so the summary and the exit code describe what is there now.
|
|
412
|
+
report = await contractor.detectDrift();
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
console.log('\nNothing for --fix to repair: job_now() carries no TestClock override.');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
389
418
|
if (report.building.length) {
|
|
390
419
|
console.log(`\nBuilding (async index build in progress — not yet drift) (${report.building.length}):`);
|
|
391
420
|
for (const i of report.building)
|
|
@@ -461,6 +490,14 @@ async function cmdDoctor(args) {
|
|
|
461
490
|
console.log(` expected: ${f.definition}`);
|
|
462
491
|
}
|
|
463
492
|
}
|
|
493
|
+
let clockOverrideNamed = false;
|
|
494
|
+
const printClockOverrideHint = (marker) => {
|
|
495
|
+
console.log(`\n${marker}job_now() carries a TestClock override, left behind by a test run that was`);
|
|
496
|
+
console.log(' killed before releasing its clock. Time is still correct, but the function no');
|
|
497
|
+
console.log(' longer inlines, so every statement that reads the clock is slower. Restore it');
|
|
498
|
+
console.log(' with "pg-boss doctor --fix", or by hand:');
|
|
499
|
+
console.log(plans.restoreClockFunction(schema).split('\n').filter(l => l.trim()).map(l => ` ${l.trim()}`).join('\n'));
|
|
500
|
+
};
|
|
464
501
|
if (report.mismatchedFunctions.length) {
|
|
465
502
|
console.log(`\nMISMATCHED FUNCTIONS (body differs) (${report.mismatchedFunctions.length}):`);
|
|
466
503
|
for (const f of report.mismatchedFunctions) {
|
|
@@ -468,6 +505,23 @@ async function cmdDoctor(args) {
|
|
|
468
505
|
console.log(` expected: ${f.definition}`);
|
|
469
506
|
console.log(` actual: ${f.actualDefinition}`);
|
|
470
507
|
}
|
|
508
|
+
// One cause of a job_now() mismatch is common and self-inflicted: a TestClock only restores
|
|
509
|
+
// the function when its handle is disposed, so a killed run leaves the override installed.
|
|
510
|
+
// The body still returns real time for a session that never opted in, which is why nothing
|
|
511
|
+
// surfaces it at runtime - but it no longer inlines, so every statement that reads the clock
|
|
512
|
+
// pays a per-row call. Name it rather than leave an operator to read two bodies and guess.
|
|
513
|
+
const clockOverride = report.mismatchedFunctions.find(f => f.name === 'job_now' && plans.clockFunctionIsOverridden(f.actualDefinition));
|
|
514
|
+
if (clockOverride) {
|
|
515
|
+
clockOverrideNamed = true;
|
|
516
|
+
printClockOverrideHint(' ');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Same hint for a backend that never produced function rows to mismatch: the check above is
|
|
520
|
+
// gated on pg_get_functiondef, which CockroachDB does not support, so the override is invisible
|
|
521
|
+
// to drift there. This probe reads prosrc and finds it anyway. It is not drift the report can
|
|
522
|
+
// show a body for, so it names the condition and stops short of printing an "actual:".
|
|
523
|
+
if (!clockOverrideNamed && await contractor.detectClockOverride()) {
|
|
524
|
+
printClockOverrideHint('⚠ ');
|
|
471
525
|
}
|
|
472
526
|
if (report.columnDrift.length) {
|
|
473
527
|
console.log(`\nCOLUMN DRIFT (missing/unexpected columns, or default/type/nullability drift) (${report.columnDrift.length}):`);
|
package/dist/clock.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AttachableClock, Clock, ClockTimer, IDatabase } from './types.ts';
|
|
2
|
+
export declare const systemClock: Clock;
|
|
3
|
+
export declare function isAttachable(clock: Clock): clock is AttachableClock;
|
|
4
|
+
interface Target {
|
|
5
|
+
db: IDatabase;
|
|
6
|
+
schema: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A clock a test drives by hand. Time only moves through setTime() and tick(); timers only fire
|
|
10
|
+
* from tick(). When attached to a schema, every pg-boss statement there reads the same time
|
|
11
|
+
* through ${schema}.job_now(), so JavaScript and Postgres agree on what "now" is.
|
|
12
|
+
*/
|
|
13
|
+
export declare class TestClock implements AttachableClock {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(start?: Date | number | string);
|
|
16
|
+
now(): number;
|
|
17
|
+
setTimeout(fn: () => void, ms: number): ClockTimer;
|
|
18
|
+
setInterval(fn: () => void, ms: number): ClockTimer;
|
|
19
|
+
clearTimeout(handle: ClockTimer): void;
|
|
20
|
+
clearInterval(handle: ClockTimer): void;
|
|
21
|
+
/** Jumps to a time, forwards or backwards, firing nothing. */
|
|
22
|
+
setTime(t: Date | number | string): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Advances by ms, firing each due timer in order at its own due time. Does not wait for I/O the
|
|
25
|
+
* callbacks start; observe effects with spies or by querying.
|
|
26
|
+
*/
|
|
27
|
+
tick(ms: number): Promise<void>;
|
|
28
|
+
attach(target: Target): Promise<AsyncDisposable>;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=clock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE/E,eAAO,MAAM,WAAW,EAAE,KAMzB,CAAA;AAED,wBAAgB,YAAY,CAAE,KAAK,EAAE,KAAK,GAAG,KAAK,IAAI,eAAe,CAEpE;AASD,UAAU,MAAM;IACd,EAAE,EAAE,SAAS,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAUD;;;;GAIG;AACH,qBAAa,SAAU,YAAW,eAAe;;gBAWlC,KAAK,GAAE,IAAI,GAAG,MAAM,GAAG,MAAmB;IAIvD,GAAG,IAAK,MAAM;IAId,UAAU,CAAE,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU;IAInD,WAAW,CAAE,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU;IAIpD,YAAY,CAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAKvC,aAAa,CAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAIxC,8DAA8D;IACxD,OAAO,CAAE,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxD;;;OAGG;IACG,IAAI,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsChC,MAAM,CAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;CA4ExD"}
|