@forgezero/runtime 0.1.0 → 0.1.2
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 +27 -4
- package/dist/audit.js +262 -23
- package/dist/custody-crypto.d.ts +53 -0
- package/dist/custody-crypto.js +89 -0
- package/dist/custody-share.d.ts +117 -0
- package/dist/custody-share.js +313 -0
- package/dist/jobs.d.ts +3 -2
- package/dist/jobs.js +288 -13
- package/dist/outbox.d.ts +10 -1
- package/dist/outbox.js +8 -3
- package/dist/pipeline.d.ts +11 -37
- package/dist/pipeline.js +0 -27
- package/dist/queue.d.ts +101 -228
- package/dist/queue.js +246 -210
- package/dist/snp.d.ts +7 -6
- package/dist/snp.js +6 -1
- package/dist/ssh-agent.d.ts +13 -0
- package/dist/ssh-agent.js +6 -0
- package/package.json +254 -246
package/dist/outbox.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export declare class OutboxError extends Error {
|
|
|
47
47
|
export declare const EVENT_STATES: readonly ["pending", "delivering", "delivered", "dead"];
|
|
48
48
|
export type EventState = (typeof EVENT_STATES)[number];
|
|
49
49
|
export interface OutboxEvent {
|
|
50
|
+
/** Set when a drainer claims it; proves ownership at settle time. */
|
|
51
|
+
claimToken?: string;
|
|
50
52
|
id: string;
|
|
51
53
|
type: string;
|
|
52
54
|
/** The aggregate this concerns. Events sharing one are delivered in order. */
|
|
@@ -134,7 +136,14 @@ export interface OutboxStore {
|
|
|
134
136
|
nowMs: number;
|
|
135
137
|
claimTtlMs: number;
|
|
136
138
|
}): Promise<OutboxEvent[]>;
|
|
137
|
-
|
|
139
|
+
/**
|
|
140
|
+
* `claimToken` proves the caller still owns this event.
|
|
141
|
+
*
|
|
142
|
+
* Without it a drainer whose lease had expired could overwrite the result of
|
|
143
|
+
* the drainer that took over, landing an old outcome on top of a newer one.
|
|
144
|
+
* Optional so an in-memory store that cannot lose ownership need not carry it.
|
|
145
|
+
*/
|
|
146
|
+
settle(id: string, patch: Partial<OutboxEvent>, claimToken?: string): Promise<void>;
|
|
138
147
|
byState(state: EventState, limit?: number): Promise<OutboxEvent[]>;
|
|
139
148
|
get(id: string): Promise<OutboxEvent | null>;
|
|
140
149
|
}
|
package/dist/outbox.js
CHANGED
|
@@ -27,13 +27,18 @@ function backoffMs(attempts, policy = DEFAULT_POLICY, random = Math.random) {
|
|
|
27
27
|
const exponential = Math.min(policy.baseDelayMs * 2 ** Math.max(0, attempts - 1), policy.maxDelayMs);
|
|
28
28
|
return Math.round(exponential * (1 + policy.jitter * random()));
|
|
29
29
|
}
|
|
30
|
+
function randomSuffix() {
|
|
31
|
+
const bytes = new Uint8Array(4);
|
|
32
|
+
crypto.getRandomValues(bytes);
|
|
33
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
34
|
+
}
|
|
30
35
|
function createOutbox(options) {
|
|
31
36
|
const policy = { ...DEFAULT_POLICY, ...options.policy };
|
|
32
37
|
const now = options.now ?? Date.now;
|
|
33
38
|
const random = options.random ?? Math.random;
|
|
34
39
|
const drainerId = options.drainerId ?? `drainer-${Math.floor(Math.random() * 1e9).toString(36)}`;
|
|
35
40
|
let sequence = 0;
|
|
36
|
-
const nextId = () => `evt_${now().toString(36)}_${(sequence += 1).toString(36)}`;
|
|
41
|
+
const nextId = () => `evt_${now().toString(36)}_${(sequence += 1).toString(36)}_${randomSuffix()}`;
|
|
37
42
|
async function settleOne(event) {
|
|
38
43
|
let result;
|
|
39
44
|
try {
|
|
@@ -47,7 +52,7 @@ function createOutbox(options) {
|
|
|
47
52
|
deliveredAtMs: now(),
|
|
48
53
|
claimedBy: undefined,
|
|
49
54
|
claimedUntilMs: undefined
|
|
50
|
-
});
|
|
55
|
+
}, event.claimToken);
|
|
51
56
|
return "delivered";
|
|
52
57
|
}
|
|
53
58
|
const attempts = event.attempts + 1;
|
|
@@ -67,7 +72,7 @@ function createOutbox(options) {
|
|
|
67
72
|
lastError: result.error,
|
|
68
73
|
claimedBy: undefined,
|
|
69
74
|
claimedUntilMs: undefined
|
|
70
|
-
});
|
|
75
|
+
}, event.claimToken);
|
|
71
76
|
options.onDeadLetter?.(dead);
|
|
72
77
|
return "dead";
|
|
73
78
|
}
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deciding whether a push should cause a deploy
|
|
2
|
+
* Deciding whether a push should cause a deploy.
|
|
3
3
|
*
|
|
4
4
|
* A webhook receiver is a public endpoint that runs commands on your machines
|
|
5
5
|
* when something posts to it. Everything worth getting right is in the gap
|
|
6
|
-
* between those two facts, so this module holds
|
|
7
|
-
* doing: no network, no shell, no clone
|
|
8
|
-
*
|
|
6
|
+
* between those two facts, so this module holds trigger verification and none
|
|
7
|
+
* of the doing: no network, no shell, no clone, and no executable plan. The
|
|
8
|
+
* agent later reads the only deployment definition from the exact checked-out
|
|
9
|
+
* commit.
|
|
9
10
|
*
|
|
10
11
|
* That split is not tidiness. It means the interesting cases — a forged
|
|
11
12
|
* signature, a push to a branch nobody deploys, two pushes racing, a repository
|
|
@@ -21,8 +22,8 @@
|
|
|
21
22
|
* matters when reading logs during an incident.
|
|
22
23
|
*/
|
|
23
24
|
export declare class PipelineError extends Error {
|
|
24
|
-
readonly code: 'BAD_SIGNATURE' | 'UNSUPPORTED_PROVIDER' | 'MALFORMED_EVENT' | '
|
|
25
|
-
constructor(code: 'BAD_SIGNATURE' | 'UNSUPPORTED_PROVIDER' | 'MALFORMED_EVENT' | '
|
|
25
|
+
readonly code: 'BAD_SIGNATURE' | 'UNSUPPORTED_PROVIDER' | 'MALFORMED_EVENT' | 'NO_SECRET';
|
|
26
|
+
constructor(code: 'BAD_SIGNATURE' | 'UNSUPPORTED_PROVIDER' | 'MALFORMED_EVENT' | 'NO_SECRET', message: string);
|
|
26
27
|
}
|
|
27
28
|
export declare const GIT_PROVIDERS: readonly ["github", "gitlab", "generic"];
|
|
28
29
|
export type GitProvider = (typeof GIT_PROVIDERS)[number];
|
|
@@ -73,50 +74,23 @@ export interface PushEvent {
|
|
|
73
74
|
* failure every time somebody opened an issue.
|
|
74
75
|
*/
|
|
75
76
|
export declare function parsePush(provider: GitProvider, body: unknown): PushEvent | null;
|
|
76
|
-
export interface
|
|
77
|
+
export interface DeployTrigger {
|
|
77
78
|
provider: GitProvider;
|
|
78
79
|
/** `owner/name`. Compared against the delivery. */
|
|
79
80
|
repository: string;
|
|
80
81
|
/** Only this branch deploys. One branch per pipeline, deliberately. */
|
|
81
82
|
branch: string;
|
|
82
|
-
/** Where the checkout lives on the compute. */
|
|
83
|
-
workdir: string;
|
|
84
|
-
/** Shell steps, in order. Empty means clone only. */
|
|
85
|
-
steps: readonly string[];
|
|
86
|
-
cloneUrl: string;
|
|
87
83
|
}
|
|
88
|
-
export declare const PLAN_ACTIONS: readonly ["clone", "fetch", "checkout", "run"];
|
|
89
|
-
export type PlanAction = (typeof PLAN_ACTIONS)[number];
|
|
90
|
-
export interface PlanStep {
|
|
91
|
-
action: PlanAction;
|
|
92
|
-
command: string;
|
|
93
|
-
/** Shown to an operator. Never contains a credential. */
|
|
94
|
-
label: string;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* The commands that would deploy this push, in order.
|
|
98
|
-
*
|
|
99
|
-
* `clone` and `fetch` are both emitted, guarded on the directory existing,
|
|
100
|
-
* because a runner cannot know in advance whether the first deploy has
|
|
101
|
-
* happened — and branching on that in the caller means two code paths where one
|
|
102
|
-
* of them is exercised once per compute, ever.
|
|
103
|
-
*
|
|
104
|
-
* The commit is checked out by SHA rather than by branch. A branch moves: a
|
|
105
|
-
* deploy that fetched and then checked out `main` could deploy a commit that
|
|
106
|
-
* arrived after the one that triggered it, so the thing tested is not the thing
|
|
107
|
-
* shipped. The SHA is what the webhook said, so what deploys is what fired.
|
|
108
|
-
*/
|
|
109
|
-
export declare function planDeploy(config: PipelineConfig, event: PushEvent): PlanStep[];
|
|
110
84
|
/**
|
|
111
85
|
* Should this delivery deploy at all?
|
|
112
86
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
87
|
+
* "We received it and deliberately did nothing" is a first-class outcome with
|
|
88
|
+
* a reason attached. A receiver that silently
|
|
115
89
|
* ignored non-matching branches would be indistinguishable from one that is
|
|
116
90
|
* broken, and the first question during an incident is always whether the hook
|
|
117
91
|
* arrived.
|
|
118
92
|
*/
|
|
119
|
-
export declare function shouldDeploy(config:
|
|
93
|
+
export declare function shouldDeploy(config: Pick<DeployTrigger, 'repository' | 'branch'>, event: PushEvent | null): {
|
|
120
94
|
deploy: boolean;
|
|
121
95
|
reason: string;
|
|
122
96
|
};
|
package/dist/pipeline.js
CHANGED
|
@@ -72,31 +72,6 @@ function parsePush(provider, body) {
|
|
|
72
72
|
message: typeof payload.head_commit?.message === "string" ? String(payload.head_commit.message) : undefined
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
-
var PLAN_ACTIONS = ["clone", "fetch", "checkout", "run"];
|
|
76
|
-
function planDeploy(config, event) {
|
|
77
|
-
if (event.repository !== config.repository) {
|
|
78
|
-
throw new PipelineError("WRONG_REPOSITORY", `That delivery is for ${event.repository}, and this pipeline deploys ${config.repository}.`);
|
|
79
|
-
}
|
|
80
|
-
const dir = config.workdir;
|
|
81
|
-
return [
|
|
82
|
-
{
|
|
83
|
-
action: "clone",
|
|
84
|
-
command: `[ -d ${dir}/.git ] || git clone ${config.cloneUrl} ${dir}`,
|
|
85
|
-
label: "clone if this is the first deploy"
|
|
86
|
-
},
|
|
87
|
-
{ action: "fetch", command: `git -C ${dir} fetch --prune origin`, label: "fetch" },
|
|
88
|
-
{
|
|
89
|
-
action: "checkout",
|
|
90
|
-
command: `git -C ${dir} checkout --detach ${event.commit}`,
|
|
91
|
-
label: `check out ${event.commit.slice(0, 8)}`
|
|
92
|
-
},
|
|
93
|
-
...config.steps.map((step) => ({
|
|
94
|
-
action: "run",
|
|
95
|
-
command: `cd ${dir} && ${step}`,
|
|
96
|
-
label: step
|
|
97
|
-
}))
|
|
98
|
-
];
|
|
99
|
-
}
|
|
100
75
|
function shouldDeploy(config, event) {
|
|
101
76
|
if (!event)
|
|
102
77
|
return { deploy: false, reason: "Not a branch push — nothing to deploy." };
|
|
@@ -113,9 +88,7 @@ export {
|
|
|
113
88
|
webhookPath,
|
|
114
89
|
verifyWebhook,
|
|
115
90
|
shouldDeploy,
|
|
116
|
-
planDeploy,
|
|
117
91
|
parsePush,
|
|
118
92
|
PipelineError,
|
|
119
|
-
PLAN_ACTIONS,
|
|
120
93
|
GIT_PROVIDERS
|
|
121
94
|
};
|
package/dist/queue.d.ts
CHANGED
|
@@ -1,243 +1,116 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* its key forever.
|
|
30
|
-
*
|
|
31
|
-
* NOT GUARANTEED exactly-once execution. Nothing can guarantee that: a worker
|
|
32
|
-
* can complete the side effect and die before recording that it
|
|
33
|
-
* did. `dedupeKey` collapses duplicate ENQUEUES, and the lease
|
|
34
|
-
* stops concurrent delivery, but a crash between "money moved" and
|
|
35
|
-
* "message acknowledged" will redeliver.
|
|
36
|
-
*
|
|
37
|
-
* ## So for money, read this
|
|
38
|
-
*
|
|
39
|
-
* The queue is not what makes a financial operation safe — the HANDLER is. Make
|
|
40
|
-
* the effect idempotent at the point of effect: a ledger posting keyed on the
|
|
41
|
-
* message id and refused by a unique index, or a transfer keyed on an
|
|
42
|
-
* idempotency key the payment provider also honours. Then redelivery is free
|
|
43
|
-
* and the queue only has to provide ordering and durability, which it does.
|
|
44
|
-
*
|
|
45
|
-
* Anything that relies on "the queue delivered it once" is relying on something
|
|
46
|
-
* no queue provides, including this one, including the expensive ones.
|
|
2
|
+
* Ordered work: sequential within a key, parallel across keys.
|
|
3
|
+
*
|
|
4
|
+
* That sentence is the whole contract, and the previous version buried it under
|
|
5
|
+
* a persistence framework — `QueueStore`, leases, standalone/cluster modes and
|
|
6
|
+
* an unfinished cluster adapter — that nothing in production ever used. The
|
|
7
|
+
* framework was also wrong on its own terms: a message left `leased` after its
|
|
8
|
+
* lease expired became invisible to both the claim and ready reads, so a stale
|
|
9
|
+
* lease made work unrunnable rather than runnable again.
|
|
10
|
+
*
|
|
11
|
+
* Worse for the property that matters, retry backoff did not hold the key. A
|
|
12
|
+
* task waiting to retry was skipped and a LATER task for the same key ran
|
|
13
|
+
* first, which is precisely the ordering this module exists to guarantee.
|
|
14
|
+
*
|
|
15
|
+
* So persistence is gone. The layer that knows what a pending business request
|
|
16
|
+
* means is the runtime that issued it — ForgeZero's own durability record is an
|
|
17
|
+
* Arango row written in the same transaction as the change it describes, and
|
|
18
|
+
* the outbox drains through here rather than reimplementing ordering.
|
|
19
|
+
*
|
|
20
|
+
* ## What it does guarantee
|
|
21
|
+
*
|
|
22
|
+
* - Tasks for one key run one at a time, in submission order, INCLUDING across
|
|
23
|
+
* a retry: a task that is waiting to try again blocks its key.
|
|
24
|
+
* - Different keys run concurrently, up to `width`.
|
|
25
|
+
* - `run()` returns the handler's own value, typed, so a caller can await work
|
|
26
|
+
* instead of polling for it.
|
|
27
|
+
* - `stop()` awaits what is running and what is queued, with a deadline, and
|
|
28
|
+
* reports what it managed to finish.
|
|
47
29
|
*/
|
|
48
|
-
export interface
|
|
49
|
-
|
|
30
|
+
export interface QueueTask<T> {
|
|
31
|
+
/** Resolves with the handler's value, or rejects with its final error. */
|
|
32
|
+
readonly result: Promise<T>;
|
|
33
|
+
/** Stable id, so a caller can cancel or look up this task. */
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly key: string;
|
|
50
36
|
}
|
|
51
|
-
export
|
|
52
|
-
/**
|
|
53
|
-
export declare function durationMs(value: string | number): number;
|
|
54
|
-
export type MessageStatus = 'ready' | 'leased' | 'done' | 'dead';
|
|
55
|
-
export interface Message<Body = unknown> {
|
|
56
|
-
id: string;
|
|
57
|
-
/** The partition. Everything sharing it runs in order, one at a time. */
|
|
58
|
-
key: string;
|
|
59
|
-
body: Body;
|
|
60
|
-
/** Monotonic per queue. Decides order within a key. */
|
|
61
|
-
sequence: number;
|
|
62
|
-
status: MessageStatus;
|
|
37
|
+
export interface RetryPolicy {
|
|
38
|
+
/** How many ATTEMPTS in total, including the first. */
|
|
63
39
|
attempts: number;
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
/** Collapses repeat enqueues within a window. See `enqueue`. */
|
|
67
|
-
dedupeKey?: string;
|
|
68
|
-
lastError?: string;
|
|
69
|
-
enqueuedAtMs: number;
|
|
70
|
-
completedAtMs?: number;
|
|
40
|
+
/** Milliseconds before attempt n+1. Called with the 1-based attempt made. */
|
|
41
|
+
backoffMs: (attempt: number) => number;
|
|
71
42
|
}
|
|
72
|
-
export interface
|
|
73
|
-
key
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
43
|
+
export interface QueueOptions {
|
|
44
|
+
/** How many keys may run at once. Ordering within a key is unaffected. */
|
|
45
|
+
width?: number;
|
|
46
|
+
retry?: Partial<RetryPolicy>;
|
|
47
|
+
/** Retry delay injection. Shutdown deadlines use a cancellable native timer. */
|
|
48
|
+
sleep?: (ms: number) => Promise<void>;
|
|
78
49
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* hardest possible thing to reproduce.
|
|
86
|
-
*/
|
|
87
|
-
export interface QueueStore<Body = unknown> {
|
|
88
|
-
/** Which deployment shape this store can serve. See `MODES`. */
|
|
89
|
-
readonly mode: QueueMode;
|
|
90
|
-
append(queue: string, message: Message<Body>): Promise<void>;
|
|
91
|
-
/** Existing message with this dedupe key, if the window has not passed. */
|
|
92
|
-
findByDedupe(queue: string, dedupeKey: string, sinceMs: number): Promise<Message<Body> | null>;
|
|
93
|
-
/**
|
|
94
|
-
* Take the next key that has ready work and is not leased. Atomic.
|
|
95
|
-
* `exclude` skips keys this worker already holds.
|
|
96
|
-
*/
|
|
97
|
-
claimKey(queue: string, owner: string, ttlMs: number, nowMs: number): Promise<KeyLease | null>;
|
|
98
|
-
renewKey(queue: string, lease: KeyLease, ttlMs: number, nowMs: number): Promise<boolean>;
|
|
99
|
-
releaseKey(queue: string, lease: KeyLease): Promise<void>;
|
|
100
|
-
/** Ready messages for one key, in sequence order. */
|
|
101
|
-
readKey(queue: string, key: string, nowMs: number, limit: number): Promise<Message<Body>[]>;
|
|
102
|
-
update(queue: string, id: string, patch: Partial<Message<Body>>): Promise<void>;
|
|
103
|
-
stats(queue: string): Promise<QueueStats>;
|
|
104
|
-
/** Dead-lettered messages, newest first. */
|
|
105
|
-
dead(queue: string, limit: number): Promise<Message<Body>[]>;
|
|
50
|
+
export interface DrainReport {
|
|
51
|
+
completed: number;
|
|
52
|
+
failed: number;
|
|
53
|
+
/** Still queued or running when the deadline passed. */
|
|
54
|
+
abandoned: number;
|
|
55
|
+
timedOut: boolean;
|
|
106
56
|
}
|
|
107
|
-
export
|
|
108
|
-
|
|
109
|
-
leased: number;
|
|
110
|
-
dead: number;
|
|
111
|
-
keys: number;
|
|
57
|
+
export declare class QueueStoppedError extends Error {
|
|
58
|
+
constructor();
|
|
112
59
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
* standalone one process, state in memory. No coordination, nothing to
|
|
117
|
-
* install, and the ordering guarantee still holds — because a
|
|
118
|
-
* single process claiming its own keys cannot race itself.
|
|
119
|
-
* Everything is lost on restart, which is correct for a
|
|
120
|
-
* development machine and wrong for anything holding money.
|
|
121
|
-
*
|
|
122
|
-
* cluster several processes over shared storage. The guarantee is the
|
|
123
|
-
* same and the requirement is stricter: `claimKey` must be
|
|
124
|
-
* ATOMIC. A read-then-write implementation hands one key to two
|
|
125
|
-
* workers under exactly the load that makes ordering matter, and
|
|
126
|
-
* the bug presents as "the queue occasionally processes out of
|
|
127
|
-
* order" — the hardest possible thing to reproduce.
|
|
128
|
-
*
|
|
129
|
-
* The distinction is declared on the store rather than inferred, so a
|
|
130
|
-
* deployment can refuse to start when a durable queue was expected and a memory
|
|
131
|
-
* one was wired in. That mistake is otherwise invisible until a restart.
|
|
132
|
-
*/
|
|
133
|
-
export declare const MODES: readonly ["standalone", "cluster"];
|
|
134
|
-
export type QueueMode = (typeof MODES)[number];
|
|
135
|
-
/**
|
|
136
|
-
* Single process. Correct for tests and for one instance, and honest about
|
|
137
|
-
* being no more — nothing here coordinates across processes.
|
|
138
|
-
*/
|
|
139
|
-
export declare function memoryStore<Body = unknown>(clock?: Clock): QueueStore<Body>;
|
|
140
|
-
export interface Handler<Body> {
|
|
141
|
-
(message: Message<Body>, context: HandlerContext): Promise<void>;
|
|
60
|
+
export declare class QueueKeyStoppedError extends Error {
|
|
61
|
+
readonly key: string;
|
|
62
|
+
constructor(key: string);
|
|
142
63
|
}
|
|
143
|
-
export
|
|
144
|
-
|
|
145
|
-
holdsKey(): Promise<boolean>;
|
|
146
|
-
log(message: string): void;
|
|
147
|
-
attempt: number;
|
|
64
|
+
export declare class TaskCancelledError extends Error {
|
|
65
|
+
constructor();
|
|
148
66
|
}
|
|
149
|
-
export
|
|
150
|
-
name: string;
|
|
151
|
-
store: QueueStore<Body>;
|
|
152
|
-
handler: Handler<Body>;
|
|
153
|
-
/** How many KEYS one worker processes at once. Not messages — see the note. */
|
|
154
|
-
concurrency?: number;
|
|
155
|
-
/** How long a key lease lasts. Renewed between messages. */
|
|
156
|
-
leaseMs?: number;
|
|
157
|
-
/** Attempts before a message is dead-lettered. */
|
|
158
|
-
maxAttempts?: number;
|
|
159
|
-
/** Backoff between attempts. Doubles, capped. */
|
|
160
|
-
backoffMs?: number;
|
|
161
|
-
maxBackoffMs?: number;
|
|
162
|
-
/** Repeat enqueues with the same `dedupeKey` inside this window collapse. */
|
|
163
|
-
dedupeWindowMs?: number;
|
|
164
|
-
/** Messages taken per key per turn, so one hot key cannot hold a slot forever. */
|
|
165
|
-
batch?: number;
|
|
166
|
-
clock?: Clock;
|
|
167
|
-
owner?: string;
|
|
168
|
-
onError?: (message: Message<Body>, error: unknown) => void;
|
|
67
|
+
export declare function createQueue(options?: QueueOptions): {
|
|
169
68
|
/**
|
|
170
|
-
*
|
|
69
|
+
* Submit work and await its value.
|
|
171
70
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
71
|
+
* The handler's return type flows through, so a caller gets a result
|
|
72
|
+
* rather than an id to poll — the previous `enqueue` returned only
|
|
73
|
+
* `{ id, duplicate }` and had nowhere to put an answer.
|
|
175
74
|
*/
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
export declare function createQueue<Body = unknown>(options: QueueOptions<Body>): {
|
|
184
|
-
enqueue: (args: {
|
|
185
|
-
key: string;
|
|
186
|
-
body: Body;
|
|
187
|
-
dedupeKey?: string;
|
|
188
|
-
delayMs?: number;
|
|
189
|
-
}) => Promise<EnqueueResult>;
|
|
190
|
-
tick: () => Promise<number>;
|
|
191
|
-
/** Drain until nothing is ready. For tests and for a one-shot worker. */
|
|
192
|
-
drain(maxPasses?: number): Promise<void>;
|
|
193
|
-
/** Poll forever. `stop()` lets in-flight keys finish. */
|
|
194
|
-
start(intervalMs?: number): void;
|
|
195
|
-
stop(): void;
|
|
196
|
-
mode: "standalone" | "cluster";
|
|
197
|
-
stats: () => Promise<QueueStats>;
|
|
198
|
-
dead: (limit?: number) => Promise<Message<Body>[]>;
|
|
199
|
-
/** Put a dead message back at the front of its key. An operator decision. */
|
|
200
|
-
revive(id: string): Promise<void>;
|
|
201
|
-
};
|
|
202
|
-
export type Queue<Body = unknown> = ReturnType<typeof createQueue<Body>>;
|
|
203
|
-
export declare const VERSION = "0.1.0";
|
|
204
|
-
/**
|
|
205
|
-
* The four operations a shared database has to provide, and the one that is
|
|
206
|
-
* hard.
|
|
207
|
-
*
|
|
208
|
-
* Everything except `claimKey` is an ordinary read or write. `claimKey` has to
|
|
209
|
-
* find a key with ready work, check that nobody holds it, and take it — as ONE
|
|
210
|
-
* atomic step. Split into a read and a write, two workers pass the check
|
|
211
|
-
* together and both proceed, and the queue silently stops ordering anything.
|
|
212
|
-
*
|
|
213
|
-
* Written as a small adapter rather than a driver so the same queue runs over
|
|
214
|
-
* ArangoDB, Postgres or Redis, and so this package keeps no dependency.
|
|
215
|
-
*/
|
|
216
|
-
export interface ClusterAdapter<Body = unknown> {
|
|
217
|
-
insert(queue: string, message: Message<Body>): Promise<void>;
|
|
218
|
-
findDuplicate(queue: string, dedupeKey: string, sinceMs: number): Promise<Message<Body> | null>;
|
|
75
|
+
run<Args extends unknown[], T>(key: string, handler: (...args: Args) => Promise<T> | T, ...args: Args): QueueTask<T>;
|
|
76
|
+
/** Remove a task that has not started. Running work is left alone. */
|
|
77
|
+
cancel(id: string): boolean;
|
|
78
|
+
/** Hold one key. Work already running for it finishes. */
|
|
79
|
+
pauseKey(key: string): void;
|
|
80
|
+
resumeKey(key: string): void;
|
|
219
81
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
82
|
+
* Close one key and reject everything for it that has not started.
|
|
83
|
+
*
|
|
84
|
+
* JavaScript cannot safely kill an arbitrary running function. The current
|
|
85
|
+
* handler is therefore allowed to finish; every pending handler is removed,
|
|
86
|
+
* and future submissions are refused until `startKey()` is explicit.
|
|
222
87
|
*/
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
88
|
+
stopKey(key: string): number;
|
|
89
|
+
/** Re-open a key deliberately; pausing and stopping are not aliases. */
|
|
90
|
+
startKey(key: string): boolean;
|
|
91
|
+
/** Hold everything. New submissions are accepted and wait. */
|
|
92
|
+
pause(): void;
|
|
93
|
+
resume(): void;
|
|
94
|
+
/** How much is outstanding, for a health endpoint or a drain decision. */
|
|
95
|
+
snapshot(): {
|
|
96
|
+
running: number;
|
|
97
|
+
queued: number;
|
|
98
|
+
keys: number;
|
|
99
|
+
paused: boolean;
|
|
100
|
+
pausedKeys: string[];
|
|
101
|
+
stoppedKeys: string[];
|
|
102
|
+
completed: number;
|
|
103
|
+
failed: number;
|
|
104
|
+
};
|
|
105
|
+
/** Resolve when nothing is running and nothing is queued. */
|
|
106
|
+
whenIdle(): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Stop accepting work and await what is left, with a deadline.
|
|
109
|
+
*
|
|
110
|
+
* `stop()` used to flip a boolean and return `void`, which is not a
|
|
111
|
+
* shutdown — a process could exit with work in flight and report success.
|
|
112
|
+
* This reports what it actually managed.
|
|
113
|
+
*/
|
|
114
|
+
stop(deadlineMs?: number): Promise<DrainReport>;
|
|
115
|
+
};
|
|
116
|
+
export type Queue = ReturnType<typeof createQueue>;
|