@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/queue.js CHANGED
@@ -7,240 +7,209 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  });
8
8
 
9
9
  // src/queue.ts
10
- var systemClock = { now: () => Date.now() };
11
- var UNITS = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
12
- function durationMs(value) {
13
- if (typeof value === "number")
14
- return value;
15
- const match = /^(\d+)([smhd])$/.exec(value);
16
- if (!match)
17
- throw new Error(`"${value}" is not a duration like 30s, 5m, 1h, 1d.`);
18
- return Number(match[1]) * UNITS[match[2]];
19
- }
20
- var MODES = ["standalone", "cluster"];
21
- function memoryStore(clock = systemClock) {
22
- const messages = new Map;
23
- const leases = new Map;
24
- let fences = 0;
25
- const all = (queue) => messages.get(queue) ?? [];
26
- return {
27
- mode: "standalone",
28
- async append(queue, message) {
29
- messages.set(queue, [...all(queue), message]);
30
- },
31
- async findByDedupe(queue, dedupeKey, sinceMs) {
32
- return all(queue).find((message) => message.dedupeKey === dedupeKey && message.enqueuedAtMs >= sinceMs) ?? null;
33
- },
34
- async claimKey(queue, owner, ttlMs, nowMs) {
35
- const ready = all(queue).filter((message) => message.status === "ready" && message.availableAtMs <= nowMs);
36
- for (const message of ready.sort((a, b) => a.sequence - b.sequence)) {
37
- const held = leases.get(`${queue}\x00${message.key}`);
38
- if (held && held.untilMs > nowMs)
39
- continue;
40
- fences += 1;
41
- const lease = { key: message.key, fence: fences, untilMs: nowMs + ttlMs, owner };
42
- leases.set(`${queue}\x00${message.key}`, lease);
43
- return lease;
44
- }
45
- return null;
46
- },
47
- async renewKey(queue, lease, ttlMs, nowMs) {
48
- const held = leases.get(`${queue}\x00${lease.key}`);
49
- if (!held || held.fence !== lease.fence)
50
- return false;
51
- held.untilMs = nowMs + ttlMs;
52
- return true;
53
- },
54
- async releaseKey(queue, lease) {
55
- const held = leases.get(`${queue}\x00${lease.key}`);
56
- if (held?.fence === lease.fence)
57
- leases.delete(`${queue}\x00${lease.key}`);
58
- },
59
- async readKey(queue, key, nowMs, limit) {
60
- return all(queue).filter((message) => message.key === key && message.status === "ready" && message.availableAtMs <= nowMs).sort((a, b) => a.sequence - b.sequence).slice(0, limit);
61
- },
62
- async update(queue, id, patch) {
63
- const message = all(queue).find((entry) => entry.id === id);
64
- if (message)
65
- Object.assign(message, patch);
66
- },
67
- async stats(queue) {
68
- const list = all(queue);
69
- return {
70
- ready: list.filter((message) => message.status === "ready").length,
71
- leased: list.filter((message) => message.status === "leased").length,
72
- dead: list.filter((message) => message.status === "dead").length,
73
- keys: new Set(list.filter((message) => message.status === "ready").map((m) => m.key)).size
74
- };
75
- },
76
- async dead(queue, limit) {
77
- return all(queue).filter((message) => message.status === "dead").sort((a, b) => b.sequence - a.sequence).slice(0, limit);
78
- }
79
- };
10
+ class QueueStoppedError extends Error {
11
+ constructor() {
12
+ super("queue: stopped before this task could run");
13
+ this.name = "QueueStoppedError";
14
+ }
80
15
  }
81
- function createQueue(options) {
82
- const {
83
- name,
84
- store,
85
- handler,
86
- concurrency = 4,
87
- leaseMs = 30000,
88
- maxAttempts = 5,
89
- backoffMs = 1000,
90
- maxBackoffMs = 60000,
91
- dedupeWindowMs = 300000,
92
- batch = 32,
93
- clock = systemClock,
94
- owner = `worker-${Math.trunc(clock.now())}`
95
- } = options;
96
- if (options.require && store.mode !== options.require) {
97
- throw new Error(`Queue "${name}" requires a ${options.require} store but was given a ${store.mode} one. ` + "A memory store loses everything on restart.");
16
+
17
+ class TaskCancelledError extends Error {
18
+ constructor() {
19
+ super("queue: task cancelled");
20
+ this.name = "TaskCancelledError";
98
21
  }
22
+ }
23
+ var DEFAULT_RETRY = {
24
+ attempts: 1,
25
+ backoffMs: (attempt) => Math.min(30000, 2 ** attempt * 100)
26
+ };
27
+ function createQueue(options = {}) {
28
+ const width = Math.max(1, options.width ?? 8);
29
+ const retry = { ...DEFAULT_RETRY, ...options.retry };
30
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
31
+ const lanes = new Map;
32
+ const running = new Set;
33
+ const paused = new Set;
99
34
  let sequence = 0;
100
- let running = false;
101
- let inFlight = 0;
102
- const held = new Set;
103
- const backoffFor = (attempt) => Math.min(maxBackoffMs, backoffMs * 2 ** Math.max(0, attempt - 1));
104
- async function enqueue(args) {
105
- if (!args.key)
106
- throw new Error("Every message needs a key — it is what orders the work.");
107
- const nowMs = clock.now();
108
- if (args.dedupeKey) {
109
- const existing = await store.findByDedupe(name, args.dedupeKey, nowMs - dedupeWindowMs);
110
- if (existing)
111
- return { id: existing.id, duplicate: true };
35
+ let globallyPaused = false;
36
+ let accepting = true;
37
+ let aborted = false;
38
+ let completed = 0;
39
+ let failed = 0;
40
+ const idle = [];
41
+ const announceIdle = () => {
42
+ if (running.size > 0)
43
+ return;
44
+ for (const lane of lanes.values())
45
+ if (lane.length > 0)
46
+ return;
47
+ while (idle.length > 0)
48
+ idle.shift()();
49
+ };
50
+ function pump() {
51
+ if (globallyPaused || aborted)
52
+ return;
53
+ for (const [key, lane] of lanes) {
54
+ if (running.size >= width)
55
+ break;
56
+ if (running.has(key) || paused.has(key) || lane.length === 0)
57
+ continue;
58
+ execute(key);
112
59
  }
113
- sequence += 1;
114
- const message = {
115
- id: `${name}-${nowMs}-${sequence}`,
116
- key: args.key,
117
- body: args.body,
118
- sequence,
119
- status: "ready",
120
- attempts: 0,
121
- availableAtMs: nowMs + (args.delayMs ?? 0),
122
- dedupeKey: args.dedupeKey,
123
- enqueuedAtMs: nowMs
124
- };
125
- await store.append(name, message);
126
- return { id: message.id, duplicate: false };
60
+ announceIdle();
127
61
  }
128
- async function drainKey(lease) {
62
+ async function execute(key) {
63
+ running.add(key);
129
64
  try {
130
- for (let taken = 0;taken < batch; taken += 1) {
131
- const nowMs = clock.now();
132
- if (!await store.renewKey(name, lease, leaseMs, nowMs))
133
- return;
134
- const [message] = await store.readKey(name, lease.key, nowMs, 1);
135
- if (!message)
136
- return;
137
- await store.update(name, message.id, { status: "leased", attempts: message.attempts + 1 });
138
- try {
139
- await handler({ ...message, attempts: message.attempts + 1 }, {
140
- holdsKey: () => store.renewKey(name, lease, leaseMs, clock.now()),
141
- log: (line) => options.onError?.({ ...message }, line),
142
- attempt: message.attempts + 1
143
- });
144
- await store.update(name, message.id, { status: "done", completedAtMs: clock.now() });
145
- } catch (cause) {
146
- const attempts = message.attempts + 1;
147
- const reason = cause instanceof Error ? cause.message : String(cause);
148
- options.onError?.(message, cause);
149
- if (attempts >= maxAttempts) {
150
- await store.update(name, message.id, { status: "dead", lastError: reason });
151
- continue;
152
- }
153
- await store.update(name, message.id, {
154
- status: "ready",
155
- lastError: reason,
156
- availableAtMs: clock.now() + backoffFor(attempts)
157
- });
158
- return;
159
- }
65
+ for (;; ) {
66
+ const lane = lanes.get(key);
67
+ const entry = lane?.[0];
68
+ if (!entry || globallyPaused || paused.has(key) || aborted)
69
+ break;
70
+ lane.shift();
71
+ await attempt(entry);
160
72
  }
161
73
  } finally {
162
- await store.releaseKey(name, lease);
163
- held.delete(lease.key);
74
+ running.delete(key);
75
+ if (lanes.get(key)?.length === 0)
76
+ lanes.delete(key);
77
+ pump();
164
78
  }
165
79
  }
166
- async function tick() {
167
- const claimed = [];
168
- while (inFlight + claimed.length < concurrency) {
169
- const lease = await store.claimKey(name, owner, leaseMs, clock.now());
170
- if (!lease)
171
- break;
172
- if (held.has(lease.key))
173
- break;
174
- held.add(lease.key);
175
- claimed.push(lease);
80
+ async function attempt(entry) {
81
+ if (entry.cancelled) {
82
+ entry.reject(new TaskCancelledError);
83
+ return;
176
84
  }
177
- if (claimed.length === 0)
178
- return 0;
179
- inFlight += claimed.length;
180
- try {
181
- await Promise.all(claimed.map((lease) => drainKey(lease)));
182
- } finally {
183
- inFlight -= claimed.length;
85
+ for (;; ) {
86
+ entry.attempt += 1;
87
+ try {
88
+ const value = await entry.run();
89
+ completed += 1;
90
+ entry.resolve(value);
91
+ return;
92
+ } catch (cause) {
93
+ if (entry.attempt >= retry.attempts || entry.cancelled || aborted) {
94
+ failed += 1;
95
+ entry.reject(cause);
96
+ return;
97
+ }
98
+ await sleep(retry.backoffMs(entry.attempt));
99
+ }
184
100
  }
185
- return claimed.length;
186
101
  }
187
102
  return {
188
- enqueue,
189
- tick,
190
- async drain(maxPasses = 100) {
191
- for (let pass = 0;pass < maxPasses; pass += 1) {
192
- if (await tick() === 0)
103
+ run(key, handler) {
104
+ const id = `q_${++sequence}`;
105
+ if (!accepting) {
106
+ const refused = Promise.reject(new QueueStoppedError);
107
+ refused.catch(() => {
193
108
  return;
109
+ });
110
+ return { id, key, result: refused };
194
111
  }
112
+ let resolve;
113
+ let reject;
114
+ const result = new Promise((ok, no) => {
115
+ resolve = ok;
116
+ reject = no;
117
+ });
118
+ const entry = {
119
+ id,
120
+ key,
121
+ run: async () => handler(),
122
+ resolve,
123
+ reject,
124
+ attempt: 0,
125
+ cancelled: false
126
+ };
127
+ const lane = lanes.get(key);
128
+ if (lane)
129
+ lane.push(entry);
130
+ else
131
+ lanes.set(key, [entry]);
132
+ pump();
133
+ return { id, key, result };
195
134
  },
196
- start(intervalMs = 200) {
197
- if (running)
198
- return;
199
- running = true;
200
- const loop = async () => {
201
- while (running) {
202
- try {
203
- if (await tick() === 0)
204
- await new Promise((r) => setTimeout(r, intervalMs));
205
- } catch {
206
- await new Promise((r) => setTimeout(r, intervalMs));
207
- }
208
- }
135
+ cancel(id) {
136
+ for (const [key, lane] of lanes) {
137
+ const index = lane.findIndex((entry2) => entry2.id === id);
138
+ if (index === -1)
139
+ continue;
140
+ const [entry] = lane.splice(index, 1);
141
+ entry.cancelled = true;
142
+ entry.reject(new TaskCancelledError);
143
+ if (lane.length === 0 && !running.has(key))
144
+ lanes.delete(key);
145
+ return true;
146
+ }
147
+ return false;
148
+ },
149
+ pauseKey(key) {
150
+ paused.add(key);
151
+ },
152
+ resumeKey(key) {
153
+ paused.delete(key);
154
+ pump();
155
+ },
156
+ pause() {
157
+ globallyPaused = true;
158
+ },
159
+ resume() {
160
+ globallyPaused = false;
161
+ pump();
162
+ },
163
+ snapshot() {
164
+ let queued = 0;
165
+ for (const lane of lanes.values())
166
+ queued += lane.length;
167
+ return {
168
+ running: running.size,
169
+ queued,
170
+ keys: lanes.size,
171
+ paused: globallyPaused,
172
+ pausedKeys: [...paused],
173
+ completed,
174
+ failed
209
175
  };
210
- loop();
211
176
  },
212
- stop() {
213
- running = false;
177
+ whenIdle() {
178
+ if (running.size === 0 && [...lanes.values()].every((lane) => lane.length === 0)) {
179
+ return Promise.resolve();
180
+ }
181
+ return new Promise((resolve) => idle.push(resolve));
214
182
  },
215
- mode: store.mode,
216
- stats: () => store.stats(name),
217
- dead: (limit = 50) => store.dead(name, limit),
218
- async revive(id) {
219
- await store.update(name, id, { status: "ready", attempts: 0, availableAtMs: clock.now() });
183
+ async stop(deadlineMs = 30000) {
184
+ accepting = false;
185
+ const before = { completed, failed };
186
+ let timedOut = false;
187
+ await Promise.race([
188
+ this.whenIdle(),
189
+ sleep(deadlineMs).then(() => {
190
+ timedOut = true;
191
+ })
192
+ ]);
193
+ if (timedOut)
194
+ aborted = true;
195
+ let abandoned = 0;
196
+ for (const lane of lanes.values()) {
197
+ abandoned += lane.length;
198
+ for (const entry of lane.splice(0))
199
+ entry.reject(new QueueStoppedError);
200
+ }
201
+ abandoned += running.size;
202
+ return {
203
+ completed: completed - before.completed,
204
+ failed: failed - before.failed,
205
+ abandoned,
206
+ timedOut
207
+ };
220
208
  }
221
209
  };
222
210
  }
223
- var VERSION = "0.1.0";
224
- function clusterStore(adapter) {
225
- return {
226
- mode: "cluster",
227
- append: (queue, message) => adapter.insert(queue, message),
228
- findByDedupe: (queue, dedupeKey, sinceMs) => adapter.findDuplicate(queue, dedupeKey, sinceMs),
229
- claimKey: (queue, owner, ttlMs, nowMs) => adapter.takeKey({ queue, owner, untilMs: nowMs + ttlMs, nowMs }),
230
- renewKey: (queue, lease, ttlMs, nowMs) => adapter.extendKey(queue, lease.key, lease.fence, nowMs + ttlMs),
231
- releaseKey: (queue, lease) => adapter.dropKey(queue, lease.key, lease.fence),
232
- readKey: (queue, key, nowMs, limit) => adapter.readReady(queue, key, nowMs, limit),
233
- update: (queue, id, patch) => adapter.patch(queue, id, patch),
234
- stats: (queue) => adapter.counts(queue),
235
- dead: (queue, limit) => adapter.deadLetter(queue, limit)
236
- };
237
- }
238
211
  export {
239
- systemClock,
240
- memoryStore,
241
- durationMs,
242
212
  createQueue,
243
- clusterStore,
244
- VERSION,
245
- MODES
213
+ TaskCancelledError,
214
+ QueueStoppedError
246
215
  };
@@ -46,6 +46,19 @@ export declare const AGENT_TIMEOUT_MS = 3000;
46
46
  export declare function listIdentities(socketPath?: string): Promise<AgentIdentity[]>;
47
47
  /** Ed25519 only. See the module note — determinism is the whole mechanism. */
48
48
  export declare function listCustodyIdentities(socketPath?: string): Promise<AgentIdentity[]>;
49
+ /**
50
+ * Sign arbitrary bytes with an identity the agent holds.
51
+ *
52
+ * Exported because a fresh proof from a terminal needs it: the server issues a
53
+ * nonce and this is what turns it into something the server can verify against
54
+ * a registered public key. `deriveCustodyKey` signs a FIXED challenge and hashes
55
+ * the result — that is a key-derivation, not a proof, and using it as one would
56
+ * replay.
57
+ *
58
+ * Returns the raw ed25519 signature, unwrapped from the agent's blob, because
59
+ * that is what a verifier takes.
60
+ */
61
+ export declare function signWithIdentity(identity: AgentIdentity, data: Uint8Array, socketPath?: string): Promise<Uint8Array>;
49
62
  /**
50
63
  * Derive the 32-byte custody key for an identity.
51
64
  *
package/dist/ssh-agent.js CHANGED
@@ -100,6 +100,11 @@ async function listIdentities(socketPath) {
100
100
  async function listCustodyIdentities(socketPath) {
101
101
  return (await listIdentities(socketPath)).filter((id) => id.type === "ssh-ed25519");
102
102
  }
103
+ async function signWithIdentity(identity, data, socketPath) {
104
+ const wrapped = await sign(identity.blob, Buffer.from(data), socketPath);
105
+ const [raw] = readString(wrapped, readString(wrapped, 0)[1]);
106
+ return new Uint8Array(raw);
107
+ }
103
108
  async function sign(blob, data, socketPath) {
104
109
  const payload = Buffer.concat([
105
110
  Buffer.from([SSH_AGENTC_SIGN_REQUEST]),
@@ -132,6 +137,7 @@ async function assertDeterministic(identity, socketPath) {
132
137
  return first;
133
138
  }
134
139
  export {
140
+ signWithIdentity,
135
141
  listIdentities,
136
142
  listCustodyIdentities,
137
143
  deriveCustodyKey,