@gate-forge/witness 0.0.0-stage → 0.8.0
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/LICENSE +202 -0
- package/README.md +114 -2
- package/dist/adapter/contract-suite.d.ts +167 -0
- package/dist/adapter/contract-suite.d.ts.map +1 -0
- package/dist/adapter/contract-suite.js +348 -0
- package/dist/adapter/contract-suite.js.map +1 -0
- package/dist/adapter/contract.d.ts +266 -0
- package/dist/adapter/contract.d.ts.map +1 -0
- package/dist/adapter/contract.js +2 -0
- package/dist/adapter/contract.js.map +1 -0
- package/dist/adapter/index.d.ts +8 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +2 -0
- package/dist/adapter/index.js.map +1 -0
- package/dist/adapter-kit/config.d.ts +163 -0
- package/dist/adapter-kit/config.d.ts.map +1 -0
- package/dist/adapter-kit/config.js +21 -0
- package/dist/adapter-kit/config.js.map +1 -0
- package/dist/adapter-kit/define.d.ts +47 -0
- package/dist/adapter-kit/define.d.ts.map +1 -0
- package/dist/adapter-kit/define.js +336 -0
- package/dist/adapter-kit/define.js.map +1 -0
- package/dist/adapter-kit/index.d.ts +34 -0
- package/dist/adapter-kit/index.d.ts.map +1 -0
- package/dist/adapter-kit/index.js +32 -0
- package/dist/adapter-kit/index.js.map +1 -0
- package/dist/adapter-kit/projection.d.ts +69 -0
- package/dist/adapter-kit/projection.d.ts.map +1 -0
- package/dist/adapter-kit/projection.js +85 -0
- package/dist/adapter-kit/projection.js.map +1 -0
- package/dist/adapter-kit/session.d.ts +105 -0
- package/dist/adapter-kit/session.d.ts.map +1 -0
- package/dist/adapter-kit/session.js +230 -0
- package/dist/adapter-kit/session.js.map +1 -0
- package/dist/client/witness-client.d.ts +193 -0
- package/dist/client/witness-client.d.ts.map +1 -0
- package/dist/client/witness-client.js +321 -0
- package/dist/client/witness-client.js.map +1 -0
- package/dist/constants.d.ts +189 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +197 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +16 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +21 -0
- package/dist/json.js.map +1 -0
- package/dist/queue/bullmq.d.ts +28 -0
- package/dist/queue/bullmq.d.ts.map +1 -0
- package/dist/queue/bullmq.js +210 -0
- package/dist/queue/bullmq.js.map +1 -0
- package/dist/queue/observer.d.ts +130 -0
- package/dist/queue/observer.d.ts.map +1 -0
- package/dist/queue/observer.js +179 -0
- package/dist/queue/observer.js.map +1 -0
- package/dist/surface.d.ts +211 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +268 -0
- package/dist/surface.js.map +1 -0
- package/dist/witness/adapter-registry.d.ts +92 -0
- package/dist/witness/adapter-registry.d.ts.map +1 -0
- package/dist/witness/adapter-registry.js +370 -0
- package/dist/witness/adapter-registry.js.map +1 -0
- package/dist/witness/behavior-request.d.ts +80 -0
- package/dist/witness/behavior-request.d.ts.map +1 -0
- package/dist/witness/behavior-request.js +335 -0
- package/dist/witness/behavior-request.js.map +1 -0
- package/dist/witness/behavior.d.ts +52 -0
- package/dist/witness/behavior.d.ts.map +1 -0
- package/dist/witness/behavior.js +118 -0
- package/dist/witness/behavior.js.map +1 -0
- package/dist/witness/bin.d.ts +21 -0
- package/dist/witness/bin.d.ts.map +1 -0
- package/dist/witness/bin.js +254 -0
- package/dist/witness/bin.js.map +1 -0
- package/dist/witness/browser.d.ts +221 -0
- package/dist/witness/browser.d.ts.map +1 -0
- package/dist/witness/browser.js +644 -0
- package/dist/witness/browser.js.map +1 -0
- package/dist/witness/chaos.d.ts +179 -0
- package/dist/witness/chaos.d.ts.map +1 -0
- package/dist/witness/chaos.js +270 -0
- package/dist/witness/chaos.js.map +1 -0
- package/dist/witness/classifications.d.ts +42 -0
- package/dist/witness/classifications.d.ts.map +1 -0
- package/dist/witness/classifications.js +87 -0
- package/dist/witness/classifications.js.map +1 -0
- package/dist/witness/env-attestation.d.ts +98 -0
- package/dist/witness/env-attestation.d.ts.map +1 -0
- package/dist/witness/env-attestation.js +202 -0
- package/dist/witness/env-attestation.js.map +1 -0
- package/dist/witness/fixture-provider.d.ts +89 -0
- package/dist/witness/fixture-provider.d.ts.map +1 -0
- package/dist/witness/fixture-provider.js +116 -0
- package/dist/witness/fixture-provider.js.map +1 -0
- package/dist/witness/loopback-pins.d.ts +79 -0
- package/dist/witness/loopback-pins.d.ts.map +1 -0
- package/dist/witness/loopback-pins.js +244 -0
- package/dist/witness/loopback-pins.js.map +1 -0
- package/dist/witness/run-options.d.ts +47 -0
- package/dist/witness/run-options.d.ts.map +1 -0
- package/dist/witness/run-options.js +179 -0
- package/dist/witness/run-options.js.map +1 -0
- package/dist/witness/server.d.ts +34 -0
- package/dist/witness/server.d.ts.map +1 -0
- package/dist/witness/server.js +4899 -0
- package/dist/witness/server.js.map +1 -0
- package/dist/witness/task.d.ts +77 -0
- package/dist/witness/task.d.ts.map +1 -0
- package/dist/witness/task.js +201 -0
- package/dist/witness/task.js.map +1 -0
- package/dist/witness/twin-shapes.d.ts +51 -0
- package/dist/witness/twin-shapes.d.ts.map +1 -0
- package/dist/witness/twin-shapes.js +127 -0
- package/dist/witness/twin-shapes.js.map +1 -0
- package/dist/witness/types.d.ts +1094 -0
- package/dist/witness/types.d.ts.map +1 -0
- package/dist/witness/types.js +2 -0
- package/dist/witness/types.js.map +1 -0
- package/package.json +98 -4
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BullMQ implementation of the witness queue channel: the engine reads
|
|
3
|
+
* the queue itself over one Redis
|
|
4
|
+
* connection, and produces its own deliveries so a task case grades
|
|
5
|
+
* state the ENGINE created, not state the suite claims.
|
|
6
|
+
*
|
|
7
|
+
* Everything the engine cannot normalize fails closed
|
|
8
|
+
* (`QueueObserverError`): an unrecognized job state, an unbounded
|
|
9
|
+
* field, an unbounded list. A partial read is a diagnostic, never a
|
|
10
|
+
* proof.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { Queue } from 'bullmq';
|
|
14
|
+
import { Redis } from 'ioredis';
|
|
15
|
+
import { DEFAULT_QUEUE_LIST_LIMIT, MAX_QUEUE_FIELD_CHARS, MAX_QUEUE_LIST_LIMIT, QUEUE_JOB_STATES, } from '@gate-forge/core';
|
|
16
|
+
import { QueueObserverError, boundedQueueField, } from './observer.js';
|
|
17
|
+
/** BullMQ job states, mapped onto the normalized vocabulary. */
|
|
18
|
+
const STATE_MAP = {
|
|
19
|
+
completed: 'completed',
|
|
20
|
+
failed: 'failed',
|
|
21
|
+
delayed: 'delayed',
|
|
22
|
+
active: 'active',
|
|
23
|
+
waiting: 'waiting',
|
|
24
|
+
'waiting-children': 'waiting',
|
|
25
|
+
prioritized: 'prioritized',
|
|
26
|
+
paused: 'paused',
|
|
27
|
+
'waiting-active': 'active',
|
|
28
|
+
};
|
|
29
|
+
/** The queue job payload key carrying the engine's delivery identity. */
|
|
30
|
+
export const DELIVERY_ID_KEY = 'gateforgeDeliveryId';
|
|
31
|
+
/** The queue job payload key carrying the engine's idempotency key. */
|
|
32
|
+
export const IDEMPOTENCY_KEY_KEY = 'gateforgeIdempotencyKey';
|
|
33
|
+
/**
|
|
34
|
+
* The BullMQ job types a bounded list read covers. A paused queue
|
|
35
|
+
* holds its jobs in BullMQ's paused list, which this read does not
|
|
36
|
+
* enumerate: a paused delivery is therefore invisible here. Every
|
|
37
|
+
* delivered job is read by id first, so a pause still shows up as a
|
|
38
|
+
* non-terminal state there.
|
|
39
|
+
*/
|
|
40
|
+
const LISTED_JOB_TYPES = [
|
|
41
|
+
'waiting',
|
|
42
|
+
'waiting-children',
|
|
43
|
+
'active',
|
|
44
|
+
'prioritized',
|
|
45
|
+
'delayed',
|
|
46
|
+
'completed',
|
|
47
|
+
'failed',
|
|
48
|
+
];
|
|
49
|
+
/** The BullMQ job name every engine-produced delivery carries. */
|
|
50
|
+
export const BULLMQ_DELIVERY_JOB_NAME = 'gateforge-delivery';
|
|
51
|
+
/**
|
|
52
|
+
* Normalizes one BullMQ state, failing closed on anything unknown.
|
|
53
|
+
*
|
|
54
|
+
* Args:
|
|
55
|
+
* state: the state BullMQ reported.
|
|
56
|
+
*
|
|
57
|
+
* Returns:
|
|
58
|
+
* QueueJobState: the normalized state.
|
|
59
|
+
*
|
|
60
|
+
* @throws QueueObserverError the state is not in the normalized vocabulary.
|
|
61
|
+
*/
|
|
62
|
+
function normalizeState(state) {
|
|
63
|
+
const mapped = STATE_MAP[state];
|
|
64
|
+
if (mapped === undefined) {
|
|
65
|
+
throw new QueueObserverError(`bullmq reported job state '${state}', which has no normalized meaning (fail closed)`);
|
|
66
|
+
}
|
|
67
|
+
return mapped;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Projects one BullMQ job into the bounded engine observation. Every
|
|
71
|
+
* field is read from the QUEUE, never from the producer's own report.
|
|
72
|
+
*
|
|
73
|
+
* Args:
|
|
74
|
+
* job: the BullMQ job handle.
|
|
75
|
+
* state: its normalized state.
|
|
76
|
+
*
|
|
77
|
+
* Returns:
|
|
78
|
+
* QueueJobObservation: the bounded observation.
|
|
79
|
+
*
|
|
80
|
+
* @throws QueueObserverError a field is unbounded or the wrong type.
|
|
81
|
+
*/
|
|
82
|
+
function observeJob(job, state) {
|
|
83
|
+
const attemptsMade = job.attemptsMade;
|
|
84
|
+
const maxAttempts = typeof job.opts.attempts === 'number' && job.opts.attempts > 0 ? job.opts.attempts : null;
|
|
85
|
+
return {
|
|
86
|
+
jobId: boundedQueueField(job.id, 'jobId') ?? '',
|
|
87
|
+
deliveryId: boundedQueueField(job.data[DELIVERY_ID_KEY] ?? null, 'deliveryId'),
|
|
88
|
+
idempotencyKey: boundedQueueField(job.data[IDEMPOTENCY_KEY_KEY] ?? null, 'idempotencyKey'),
|
|
89
|
+
state,
|
|
90
|
+
attemptsMade: typeof attemptsMade === 'number' && Number.isInteger(attemptsMade) && attemptsMade >= 0
|
|
91
|
+
? attemptsMade
|
|
92
|
+
: (() => {
|
|
93
|
+
throw new QueueObserverError(`bullmq job '${String(job.id)}' reported a non-integer attempt count (fail closed)`);
|
|
94
|
+
})(),
|
|
95
|
+
maxAttempts,
|
|
96
|
+
failedReason: boundedQueueField(job.failedReason, 'failedReason'),
|
|
97
|
+
processedAt: typeof job.processedOn === 'number' && Number.isInteger(job.processedOn) ? job.processedOn : null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Opens the BullMQ channel over one connection. One `Queue` handle per
|
|
102
|
+
* declared queue; every handle is closed exactly once.
|
|
103
|
+
*
|
|
104
|
+
* Args:
|
|
105
|
+
* config: the validated engine-owned configuration.
|
|
106
|
+
* connection: host/port or URL connection material.
|
|
107
|
+
*
|
|
108
|
+
* Returns:
|
|
109
|
+
* QueueChannel: the opened observer + deliverer.
|
|
110
|
+
*/
|
|
111
|
+
export function createBullmqChannel(config, connection) {
|
|
112
|
+
const clientsByName = new Map();
|
|
113
|
+
const queueByName = new Map();
|
|
114
|
+
const bindings = new Map();
|
|
115
|
+
for (const binding of config.queues) {
|
|
116
|
+
if (bindings.has(binding.taskResourceId)) {
|
|
117
|
+
throw new QueueObserverError(`task resource '${binding.taskResourceId}' is bound to more than one queue (fail closed)`);
|
|
118
|
+
}
|
|
119
|
+
bindings.set(binding.taskResourceId, binding.name);
|
|
120
|
+
}
|
|
121
|
+
const queueFor = (name) => {
|
|
122
|
+
const existing = queueByName.get(name);
|
|
123
|
+
if (existing !== undefined)
|
|
124
|
+
return existing;
|
|
125
|
+
// An explicit client instance: BullMQ's CJS dynamic require of
|
|
126
|
+
// ioredis does not resolve in this ESM package, and a constructed
|
|
127
|
+
// client is the documented ESM form.
|
|
128
|
+
// ioredis takes the URL as its first argument (a `{url}` option is
|
|
129
|
+
// ignored and silently falls back to localhost).
|
|
130
|
+
const client = 'url' in connection
|
|
131
|
+
? new Redis(connection.url, { maxRetriesPerRequest: null })
|
|
132
|
+
: new Redis({ ...connection, maxRetriesPerRequest: null });
|
|
133
|
+
clientsByName.set(name, client);
|
|
134
|
+
const created = new Queue(name, { connection: client });
|
|
135
|
+
queueByName.set(name, created);
|
|
136
|
+
return created;
|
|
137
|
+
};
|
|
138
|
+
const close = async () => {
|
|
139
|
+
const handles = [...queueByName.values()];
|
|
140
|
+
const clients = [...clientsByName.values()];
|
|
141
|
+
queueByName.clear();
|
|
142
|
+
clientsByName.clear();
|
|
143
|
+
await Promise.all(handles.map((handle) => handle.close()));
|
|
144
|
+
await Promise.all(clients.map((client) => client.quit()));
|
|
145
|
+
};
|
|
146
|
+
const observer = {
|
|
147
|
+
kind: 'bullmq',
|
|
148
|
+
queueFor: (taskResourceId) => {
|
|
149
|
+
const queue = bindings.get(taskResourceId);
|
|
150
|
+
if (queue === undefined) {
|
|
151
|
+
throw new QueueObserverError(`task resource '${taskResourceId}' has no declared queue binding (fail closed)`);
|
|
152
|
+
}
|
|
153
|
+
return queue;
|
|
154
|
+
},
|
|
155
|
+
readJob: async (queue, jobId) => {
|
|
156
|
+
const job = await queueFor(queue).getJob(jobId);
|
|
157
|
+
if (job === undefined)
|
|
158
|
+
return null;
|
|
159
|
+
return observeJob(job, normalizeState(await job.getState()));
|
|
160
|
+
},
|
|
161
|
+
listJobs: async (queue, limit = DEFAULT_QUEUE_LIST_LIMIT) => {
|
|
162
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_QUEUE_LIST_LIMIT) {
|
|
163
|
+
throw new QueueObserverError(`queue list limit ${String(limit)} is outside [1, ${String(MAX_QUEUE_LIST_LIMIT)}] (fail closed)`);
|
|
164
|
+
}
|
|
165
|
+
const jobs = await queueFor(queue).getJobs([...LISTED_JOB_TYPES], 0, limit, false);
|
|
166
|
+
if (jobs.length >= limit) {
|
|
167
|
+
// A full page may be exactly the queue's size, so the read is
|
|
168
|
+
// reported as incomplete rather than pretending it is exact.
|
|
169
|
+
return { jobs: [], complete: false, count: jobs.length };
|
|
170
|
+
}
|
|
171
|
+
const observed = [];
|
|
172
|
+
for (const job of jobs) {
|
|
173
|
+
observed.push(observeJob(job, normalizeState(await job.getState())));
|
|
174
|
+
}
|
|
175
|
+
return { jobs: observed, complete: true, count: observed.length };
|
|
176
|
+
},
|
|
177
|
+
close,
|
|
178
|
+
};
|
|
179
|
+
const deliverer = {
|
|
180
|
+
enqueue: async (request) => {
|
|
181
|
+
if (request.deliveryId.length === 0 || request.deliveryId.length > MAX_QUEUE_FIELD_CHARS) {
|
|
182
|
+
throw new QueueObserverError('the engine delivery identity is missing or over the bound (fail closed)');
|
|
183
|
+
}
|
|
184
|
+
if (request.idempotencyKey.length === 0 || request.idempotencyKey.length > MAX_QUEUE_FIELD_CHARS) {
|
|
185
|
+
throw new QueueObserverError('the engine idempotency key is missing or over the bound (fail closed)');
|
|
186
|
+
}
|
|
187
|
+
if (!Number.isInteger(request.maxAttempts) || request.maxAttempts < 1) {
|
|
188
|
+
throw new QueueObserverError('the declared attempt bound must be a positive integer (fail closed)');
|
|
189
|
+
}
|
|
190
|
+
// BullMQ forbids ':' in a custom job id, so the job id stays
|
|
191
|
+
// engine-generated and the owner's identity rides in the data.
|
|
192
|
+
const job = await queueFor(request.queue).add(BULLMQ_DELIVERY_JOB_NAME, {
|
|
193
|
+
...request.payload,
|
|
194
|
+
[DELIVERY_ID_KEY]: request.deliveryId,
|
|
195
|
+
[IDEMPOTENCY_KEY_KEY]: request.idempotencyKey,
|
|
196
|
+
}, {
|
|
197
|
+
attempts: request.maxAttempts,
|
|
198
|
+
jobId: `gf-${randomUUID()}`,
|
|
199
|
+
// Retained so the engine can read a finished job back: a
|
|
200
|
+
// removed job would leave the case unprovable.
|
|
201
|
+
removeOnComplete: false,
|
|
202
|
+
removeOnFail: false,
|
|
203
|
+
});
|
|
204
|
+
return { jobId: String(job.id), deliveryId: request.deliveryId, idempotencyKey: request.idempotencyKey };
|
|
205
|
+
},
|
|
206
|
+
close,
|
|
207
|
+
};
|
|
208
|
+
return { observer, deliverer };
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=bullmq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bullmq.js","sourceRoot":"","sources":["../../src/queue/bullmq.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { DEFAULT_QUEUE_LIST_LIMIT, DEFAULT_QUEUE_POLL_INTERVAL_MS, DEFAULT_QUEUE_TERMINAL_TIMEOUT_MS, MAX_QUEUE_FIELD_CHARS, MAX_QUEUE_LIST_LIMIT, QUEUE_OBSERVER_CONFIG_ENV, QUEUE_OBSERVER_ENV, type QueueBinding, type QueueJobObservation, type QueueJobState, type QueueObserverConfig } from '@gate-forge/core';
|
|
2
|
+
/** One bounded list read: the jobs plus whether the read was complete. */
|
|
3
|
+
export interface QueueJobList {
|
|
4
|
+
/** Observed jobs, in queue order. */
|
|
5
|
+
jobs: QueueJobObservation[];
|
|
6
|
+
/** False when the queue held more jobs than the bound (never proof). */
|
|
7
|
+
complete: boolean;
|
|
8
|
+
/** Engine-side count of jobs read. */
|
|
9
|
+
count: number;
|
|
10
|
+
}
|
|
11
|
+
/** Blocking queue-channel failure (always a diagnostic, never proof). */
|
|
12
|
+
export declare class QueueObserverError extends Error {
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
/** The engine's independent queue read. */
|
|
16
|
+
export interface QueueStateObserver {
|
|
17
|
+
/** Implementation kind (`bullmq` or a module's declared name). */
|
|
18
|
+
readonly kind: string;
|
|
19
|
+
/** Resolves the approved queue name for a task resource id. */
|
|
20
|
+
queueFor(taskResourceId: string): string;
|
|
21
|
+
/** Reads one job by id; null when the queue has no such job. */
|
|
22
|
+
readJob(queue: string, jobId: string): Promise<QueueJobObservation | null>;
|
|
23
|
+
/** Reads a bounded slice of one queue. */
|
|
24
|
+
listJobs(queue: string, limit?: number): Promise<QueueJobList>;
|
|
25
|
+
/** Releases connections (idempotent; never flips a verdict). */
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/** One engine-produced delivery. */
|
|
29
|
+
export interface QueueDeliveryRequest {
|
|
30
|
+
/** Approved queue name for the task resource. */
|
|
31
|
+
queue: string;
|
|
32
|
+
/** Engine-stamped delivery identity (groups the jobs of one delivery). */
|
|
33
|
+
deliveryId: string;
|
|
34
|
+
/** Engine-stamped idempotency key. */
|
|
35
|
+
idempotencyKey: string;
|
|
36
|
+
/** Bounded job payload. */
|
|
37
|
+
payload: Record<string, unknown>;
|
|
38
|
+
/** Declared attempt bound for the produced job. */
|
|
39
|
+
maxAttempts: number;
|
|
40
|
+
}
|
|
41
|
+
/** One produced delivery. */
|
|
42
|
+
export interface QueueDeliveryReceipt {
|
|
43
|
+
/** Queue-native job id of the produced job. */
|
|
44
|
+
jobId: string;
|
|
45
|
+
/** The delivery identity the engine stamped. */
|
|
46
|
+
deliveryId: string;
|
|
47
|
+
/** The idempotency key the engine stamped. */
|
|
48
|
+
idempotencyKey: string;
|
|
49
|
+
}
|
|
50
|
+
/** The engine's queue WRITE side (the delivery it will later grade). */
|
|
51
|
+
export interface QueueDeliverer {
|
|
52
|
+
/** Produces one delivery and returns its engine-observed receipt. */
|
|
53
|
+
enqueue(request: QueueDeliveryRequest): Promise<QueueDeliveryReceipt>;
|
|
54
|
+
/** Releases connections (idempotent; never flips a verdict). */
|
|
55
|
+
close(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/** Observer + deliverer over one configured queue backend. */
|
|
58
|
+
export interface QueueChannel {
|
|
59
|
+
/** The independent read. */
|
|
60
|
+
readonly observer: QueueStateObserver;
|
|
61
|
+
/** The engine-produced delivery. */
|
|
62
|
+
readonly deliverer: QueueDeliverer;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Bounds one observed string field (fail closed, never a truncated hash
|
|
66
|
+
* of an over-long value).
|
|
67
|
+
*
|
|
68
|
+
* Args:
|
|
69
|
+
* value: the observed value.
|
|
70
|
+
* what: field name for the diagnostic.
|
|
71
|
+
*
|
|
72
|
+
* Returns:
|
|
73
|
+
* string | null: the bounded string, or null when absent.
|
|
74
|
+
*
|
|
75
|
+
* @throws QueueObserverError the value is not a string or exceeds the bound.
|
|
76
|
+
*/
|
|
77
|
+
export declare function boundedQueueField(value: unknown, what: string): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Resolves a declared URL from the witness process environment.
|
|
80
|
+
*
|
|
81
|
+
* Args:
|
|
82
|
+
* config: the validated engine-owned configuration.
|
|
83
|
+
* env: the witness process environment.
|
|
84
|
+
*
|
|
85
|
+
* Returns:
|
|
86
|
+
* {host, port} | {url}: the connection material for the backend.
|
|
87
|
+
*
|
|
88
|
+
* @throws QueueObserverError the named environment variable is unset.
|
|
89
|
+
*/
|
|
90
|
+
export declare function resolveQueueConnection(config: QueueObserverConfig, env: NodeJS.ProcessEnv): {
|
|
91
|
+
host: string;
|
|
92
|
+
port: number;
|
|
93
|
+
} | {
|
|
94
|
+
url: string;
|
|
95
|
+
};
|
|
96
|
+
/** The factory shape a custom queue-observer module must export. */
|
|
97
|
+
export type QueueChannelFactory = (config: QueueObserverConfig) => Promise<QueueChannel> | QueueChannel;
|
|
98
|
+
/**
|
|
99
|
+
* Opens the configured queue channel. Built-in `bullmq` first; any
|
|
100
|
+
* other value is a module path whose default export is a factory.
|
|
101
|
+
*
|
|
102
|
+
* Args:
|
|
103
|
+
* config: the validated engine-owned configuration.
|
|
104
|
+
* env: the witness process environment (URL connection form only).
|
|
105
|
+
*
|
|
106
|
+
* Returns:
|
|
107
|
+
* Promise<QueueChannel>: the opened channel.
|
|
108
|
+
*
|
|
109
|
+
* @throws QueueObserverError on an unknown kind, an unloadable module,
|
|
110
|
+
* or a module that does not return a conforming channel (fail closed).
|
|
111
|
+
*/
|
|
112
|
+
export declare function openQueueChannel(config: QueueObserverConfig, env?: NodeJS.ProcessEnv): Promise<QueueChannel>;
|
|
113
|
+
/**
|
|
114
|
+
* Opens the channel named by the witness process environment, or null
|
|
115
|
+
* when no queue observer is configured (the fail-closed default: every
|
|
116
|
+
* `engine-task` case then blocks with a naming diagnostic).
|
|
117
|
+
*
|
|
118
|
+
* Args:
|
|
119
|
+
* env: the witness process environment.
|
|
120
|
+
*
|
|
121
|
+
* Returns:
|
|
122
|
+
* Promise<QueueChannel | null>: the configured channel, or null.
|
|
123
|
+
*
|
|
124
|
+
* @throws QueueObserverError the configuration is present but illegal.
|
|
125
|
+
*/
|
|
126
|
+
export declare function openConfiguredQueueChannel(env?: NodeJS.ProcessEnv): Promise<QueueChannel | null>;
|
|
127
|
+
export { DEFAULT_QUEUE_LIST_LIMIT, DEFAULT_QUEUE_POLL_INTERVAL_MS, DEFAULT_QUEUE_TERMINAL_TIMEOUT_MS, MAX_QUEUE_FIELD_CHARS, MAX_QUEUE_LIST_LIMIT, QUEUE_OBSERVER_CONFIG_ENV, QUEUE_OBSERVER_ENV, type QueueBinding, type QueueJobObservation, type QueueJobState, type QueueObserverConfig, };
|
|
128
|
+
/** The engine-owned queue-observer configuration. */
|
|
129
|
+
export type QueueObserverSpec = QueueObserverConfig;
|
|
130
|
+
//# sourceMappingURL=observer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observer.d.ts","sourceRoot":"","sources":["../../src/queue/observer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Witness-owned queue-state observer: the
|
|
3
|
+
* engine's INDEPENDENT trusted read of a background job's delivery
|
|
4
|
+
* state. A task contract is a claim about the queue, so the engine must
|
|
5
|
+
* read the queue itself — a suite-submitted "the job succeeded" is never
|
|
6
|
+
* proof, exactly like a suite-submitted HTTP status.
|
|
7
|
+
*
|
|
8
|
+
* The shape follows the DB/evidence adapters: a narrow read interface,
|
|
9
|
+
* one implementation per queue library, and a fail-closed rule for
|
|
10
|
+
* everything unexpected.
|
|
11
|
+
*
|
|
12
|
+
* - `readJob` reads ONE job by id (state, attempts, bounded fields);
|
|
13
|
+
* - `listJobs` reads a BOUNDED slice of one queue and reports whether
|
|
14
|
+
* the read was exhaustive (a truncated list can never prove a set);
|
|
15
|
+
* - unknown states, over-bound fields, and over-bound lists raise
|
|
16
|
+
* `QueueObserverError` — a blocking diagnostic, never a partial
|
|
17
|
+
* observation;
|
|
18
|
+
* - `enqueue` is the engine's WRITE side: the engine produces the
|
|
19
|
+
* delivery it will later grade, so a task case never grades the
|
|
20
|
+
* suite's own enqueue.
|
|
21
|
+
*
|
|
22
|
+
* The channel is opened ONLY from the engine-owned configuration
|
|
23
|
+
* (`GATEFORGE_QUEUE_OBSERVER` + `GATEFORGE_QUEUE_OBSERVER_CONFIG` in the
|
|
24
|
+
* witness process, supplied by the supervisor from the repository's
|
|
25
|
+
* `queueObserver` block). With no configuration there is no channel and
|
|
26
|
+
*/
|
|
27
|
+
import { pathToFileURL } from 'node:url';
|
|
28
|
+
import { DEFAULT_QUEUE_LIST_LIMIT, DEFAULT_QUEUE_POLL_INTERVAL_MS, DEFAULT_QUEUE_TERMINAL_TIMEOUT_MS, MAX_QUEUE_FIELD_CHARS, MAX_QUEUE_LIST_LIMIT, QUEUE_OBSERVER_CONFIG_ENV, QUEUE_OBSERVER_ENV, parseQueueObserverConfigJson, } from '@gate-forge/core';
|
|
29
|
+
/** Blocking queue-channel failure (always a diagnostic, never proof). */
|
|
30
|
+
export class QueueObserverError extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'QueueObserverError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Bounds one observed string field (fail closed, never a truncated hash
|
|
38
|
+
* of an over-long value).
|
|
39
|
+
*
|
|
40
|
+
* Args:
|
|
41
|
+
* value: the observed value.
|
|
42
|
+
* what: field name for the diagnostic.
|
|
43
|
+
*
|
|
44
|
+
* Returns:
|
|
45
|
+
* string | null: the bounded string, or null when absent.
|
|
46
|
+
*
|
|
47
|
+
* @throws QueueObserverError the value is not a string or exceeds the bound.
|
|
48
|
+
*/
|
|
49
|
+
export function boundedQueueField(value, what) {
|
|
50
|
+
if (value === null || value === undefined)
|
|
51
|
+
return null;
|
|
52
|
+
if (typeof value !== 'string') {
|
|
53
|
+
throw new QueueObserverError(`queue field '${what}' is not a string (fail closed)`);
|
|
54
|
+
}
|
|
55
|
+
if (value.length > MAX_QUEUE_FIELD_CHARS) {
|
|
56
|
+
throw new QueueObserverError(`queue field '${what}' exceeds the ${String(MAX_QUEUE_FIELD_CHARS)}-character bound (fail closed)`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolves a declared URL from the witness process environment.
|
|
62
|
+
*
|
|
63
|
+
* Args:
|
|
64
|
+
* config: the validated engine-owned configuration.
|
|
65
|
+
* env: the witness process environment.
|
|
66
|
+
*
|
|
67
|
+
* Returns:
|
|
68
|
+
* {host, port} | {url}: the connection material for the backend.
|
|
69
|
+
*
|
|
70
|
+
* @throws QueueObserverError the named environment variable is unset.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveQueueConnection(config, env) {
|
|
73
|
+
if ('urlEnv' in config.connection) {
|
|
74
|
+
const url = env[config.connection.urlEnv];
|
|
75
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
76
|
+
throw new QueueObserverError(`queue observer connection env '${config.connection.urlEnv}' is unset in the witness process (fail closed)`);
|
|
77
|
+
}
|
|
78
|
+
return { url };
|
|
79
|
+
}
|
|
80
|
+
return { host: config.connection.host, port: config.connection.port };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Loads the built-in BullMQ channel on first use. `bullmq` and `ioredis`
|
|
84
|
+
* are optional peers: a repository that declares no BullMQ observer
|
|
85
|
+
* never installs or loads them, so the witness itself never needs them.
|
|
86
|
+
*
|
|
87
|
+
* Returns:
|
|
88
|
+
* Promise<BullmqChannelModule>: the channel module.
|
|
89
|
+
*
|
|
90
|
+
* @throws QueueObserverError when the packages are not installed in the
|
|
91
|
+
* project (fail closed, naming the install command).
|
|
92
|
+
*/
|
|
93
|
+
async function loadBullmqChannelModule() {
|
|
94
|
+
try {
|
|
95
|
+
// Dynamic on purpose: `bullmq`/`ioredis` are optional peers that a
|
|
96
|
+
// project without a BullMQ observer never installs; a static import
|
|
97
|
+
// would make loading the witness itself require them.
|
|
98
|
+
return await import('./bullmq.js');
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const missing = error instanceof Error &&
|
|
102
|
+
'code' in error &&
|
|
103
|
+
error.code === 'ERR_MODULE_NOT_FOUND' &&
|
|
104
|
+
/'(bullmq|ioredis)'/.test(error.message);
|
|
105
|
+
if (!missing)
|
|
106
|
+
throw error;
|
|
107
|
+
throw new QueueObserverError("queueObserver kind 'bullmq' needs the 'bullmq' and 'ioredis' packages in this project — " +
|
|
108
|
+
'install them next to the app (npm install --save-dev bullmq ioredis) (fail closed)');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Opens the configured queue channel. Built-in `bullmq` first; any
|
|
113
|
+
* other value is a module path whose default export is a factory.
|
|
114
|
+
*
|
|
115
|
+
* Args:
|
|
116
|
+
* config: the validated engine-owned configuration.
|
|
117
|
+
* env: the witness process environment (URL connection form only).
|
|
118
|
+
*
|
|
119
|
+
* Returns:
|
|
120
|
+
* Promise<QueueChannel>: the opened channel.
|
|
121
|
+
*
|
|
122
|
+
* @throws QueueObserverError on an unknown kind, an unloadable module,
|
|
123
|
+
* or a module that does not return a conforming channel (fail closed).
|
|
124
|
+
*/
|
|
125
|
+
export async function openQueueChannel(config, env = process.env) {
|
|
126
|
+
if (config.kind === 'bullmq') {
|
|
127
|
+
const { createBullmqChannel } = await loadBullmqChannelModule();
|
|
128
|
+
return createBullmqChannel(config, resolveQueueConnection(config, env));
|
|
129
|
+
}
|
|
130
|
+
// The module path is operator-supplied at run time, so no static
|
|
131
|
+
// import specifier exists at author time.
|
|
132
|
+
const module = (await import(pathToFileURL(config.kind).href));
|
|
133
|
+
if (typeof module.default !== 'function') {
|
|
134
|
+
throw new QueueObserverError(`queue observer module '${config.kind}' has no default-exported factory (fail closed)`);
|
|
135
|
+
}
|
|
136
|
+
const channel = await module.default(config);
|
|
137
|
+
if (channel === null ||
|
|
138
|
+
typeof channel !== 'object' ||
|
|
139
|
+
channel.observer === null ||
|
|
140
|
+
typeof channel.observer !== 'object' ||
|
|
141
|
+
typeof channel.observer.readJob !== 'function' ||
|
|
142
|
+
typeof channel.observer.listJobs !== 'function' ||
|
|
143
|
+
typeof channel.observer.queueFor !== 'function' ||
|
|
144
|
+
channel.deliverer === null ||
|
|
145
|
+
typeof channel.deliverer !== 'object' ||
|
|
146
|
+
typeof channel.deliverer.enqueue !== 'function') {
|
|
147
|
+
throw new QueueObserverError(`queue observer module '${config.kind}' did not return a conforming channel (fail closed)`);
|
|
148
|
+
}
|
|
149
|
+
return channel;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Opens the channel named by the witness process environment, or null
|
|
153
|
+
* when no queue observer is configured (the fail-closed default: every
|
|
154
|
+
* `engine-task` case then blocks with a naming diagnostic).
|
|
155
|
+
*
|
|
156
|
+
* Args:
|
|
157
|
+
* env: the witness process environment.
|
|
158
|
+
*
|
|
159
|
+
* Returns:
|
|
160
|
+
* Promise<QueueChannel | null>: the configured channel, or null.
|
|
161
|
+
*
|
|
162
|
+
* @throws QueueObserverError the configuration is present but illegal.
|
|
163
|
+
*/
|
|
164
|
+
export async function openConfiguredQueueChannel(env = process.env) {
|
|
165
|
+
const kind = env[QUEUE_OBSERVER_ENV];
|
|
166
|
+
const raw = env[QUEUE_OBSERVER_CONFIG_ENV];
|
|
167
|
+
if (kind === undefined || kind === '')
|
|
168
|
+
return null;
|
|
169
|
+
if (raw === undefined || raw === '') {
|
|
170
|
+
throw new QueueObserverError(`${QUEUE_OBSERVER_CONFIG_ENV} is required when a queue observer is named (fail closed)`);
|
|
171
|
+
}
|
|
172
|
+
const config = parseQueueObserverConfigJson(raw);
|
|
173
|
+
if (config.kind !== kind) {
|
|
174
|
+
throw new QueueObserverError(`${QUEUE_OBSERVER_ENV} ('${kind}') does not match the configured observer kind ('${config.kind}') (fail closed)`);
|
|
175
|
+
}
|
|
176
|
+
return openQueueChannel(config, env);
|
|
177
|
+
}
|
|
178
|
+
export { DEFAULT_QUEUE_LIST_LIMIT, DEFAULT_QUEUE_POLL_INTERVAL_MS, DEFAULT_QUEUE_TERMINAL_TIMEOUT_MS, MAX_QUEUE_FIELD_CHARS, MAX_QUEUE_LIST_LIMIT, QUEUE_OBSERVER_CONFIG_ENV, QUEUE_OBSERVER_ENV, };
|
|
179
|
+
//# sourceMappingURL=observer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observer.js","sourceRoot":"","sources":["../../src/queue/observer.ts"],"names":[],"mappings":"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"}
|