@forgezero/runtime 0.1.0 → 0.1.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/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/outbox.d.ts +10 -1
- package/dist/outbox.js +8 -3
- package/dist/queue.d.ts +88 -229
- package/dist/queue.js +180 -211
- package/dist/ssh-agent.d.ts +13 -0
- package/dist/ssh-agent.js +6 -0
- package/package.json +258 -246
package/dist/queue.d.ts
CHANGED
|
@@ -1,243 +1,102 @@
|
|
|
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
|
+
/** Injected so tests need no timers and no wall clock. */
|
|
48
|
+
sleep?: (ms: number) => Promise<void>;
|
|
49
|
+
now?: () => number;
|
|
78
50
|
}
|
|
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>[]>;
|
|
106
|
-
}
|
|
107
|
-
export interface QueueStats {
|
|
108
|
-
ready: number;
|
|
109
|
-
leased: number;
|
|
110
|
-
dead: number;
|
|
111
|
-
keys: number;
|
|
51
|
+
export interface DrainReport {
|
|
52
|
+
completed: number;
|
|
53
|
+
failed: number;
|
|
54
|
+
/** Still queued or running when the deadline passed. */
|
|
55
|
+
abandoned: number;
|
|
56
|
+
timedOut: boolean;
|
|
112
57
|
}
|
|
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>;
|
|
58
|
+
export declare class QueueStoppedError extends Error {
|
|
59
|
+
constructor();
|
|
142
60
|
}
|
|
143
|
-
export
|
|
144
|
-
|
|
145
|
-
holdsKey(): Promise<boolean>;
|
|
146
|
-
log(message: string): void;
|
|
147
|
-
attempt: number;
|
|
61
|
+
export declare class TaskCancelledError extends Error {
|
|
62
|
+
constructor();
|
|
148
63
|
}
|
|
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;
|
|
64
|
+
export declare function createQueue(options?: QueueOptions): {
|
|
169
65
|
/**
|
|
170
|
-
*
|
|
66
|
+
* Submit work and await its value.
|
|
171
67
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
68
|
+
* The handler's return type flows through, so a caller gets a result
|
|
69
|
+
* rather than an id to poll — the previous `enqueue` returned only
|
|
70
|
+
* `{ id, duplicate }` and had nowhere to put an answer.
|
|
175
71
|
*/
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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>;
|
|
72
|
+
run<T>(key: string, handler: () => Promise<T> | T): QueueTask<T>;
|
|
73
|
+
/** Remove a task that has not started. Running work is left alone. */
|
|
74
|
+
cancel(id: string): boolean;
|
|
75
|
+
/** Hold one key. Work already running for it finishes. */
|
|
76
|
+
pauseKey(key: string): void;
|
|
77
|
+
resumeKey(key: string): void;
|
|
78
|
+
/** Hold everything. New submissions are accepted and wait. */
|
|
79
|
+
pause(): void;
|
|
80
|
+
resume(): void;
|
|
81
|
+
/** How much is outstanding, for a health endpoint or a drain decision. */
|
|
82
|
+
snapshot(): {
|
|
83
|
+
running: number;
|
|
84
|
+
queued: number;
|
|
85
|
+
keys: number;
|
|
86
|
+
paused: boolean;
|
|
87
|
+
pausedKeys: string[];
|
|
88
|
+
completed: number;
|
|
89
|
+
failed: number;
|
|
90
|
+
};
|
|
91
|
+
/** Resolve when nothing is running and nothing is queued. */
|
|
92
|
+
whenIdle(): Promise<void>;
|
|
219
93
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
94
|
+
* Stop accepting work and await what is left, with a deadline.
|
|
95
|
+
*
|
|
96
|
+
* `stop()` used to flip a boolean and return `void`, which is not a
|
|
97
|
+
* shutdown — a process could exit with work in flight and report success.
|
|
98
|
+
* This reports what it actually managed.
|
|
222
99
|
*/
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
untilMs: number;
|
|
227
|
-
nowMs: number;
|
|
228
|
-
}): Promise<KeyLease | null>;
|
|
229
|
-
extendKey(queue: string, key: string, fence: number, untilMs: number): Promise<boolean>;
|
|
230
|
-
dropKey(queue: string, key: string, fence: number): Promise<void>;
|
|
231
|
-
readReady(queue: string, key: string, nowMs: number, limit: number): Promise<Message<Body>[]>;
|
|
232
|
-
patch(queue: string, id: string, patch: Partial<Message<Body>>): Promise<void>;
|
|
233
|
-
counts(queue: string): Promise<QueueStats>;
|
|
234
|
-
deadLetter(queue: string, limit: number): Promise<Message<Body>[]>;
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* A durable store over any shared database.
|
|
238
|
-
*
|
|
239
|
-
* Declares `mode: 'cluster'`, which is what lets a deployment refuse to start if
|
|
240
|
-
* somebody wires the memory store into production by mistake — a mistake that
|
|
241
|
-
* is otherwise invisible until the first restart takes the queue with it.
|
|
242
|
-
*/
|
|
243
|
-
export declare function clusterStore<Body = unknown>(adapter: ClusterAdapter<Body>): QueueStore<Body>;
|
|
100
|
+
stop(deadlineMs?: number): Promise<DrainReport>;
|
|
101
|
+
};
|
|
102
|
+
export type Queue = ReturnType<typeof createQueue>;
|