@owlmeans/queue 0.1.18-rc.2 → 0.1.18-rc.20

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.
Files changed (54) hide show
  1. package/README.md +237 -8
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/queue/SKILL.md +198 -12
  4. package/build/bridge.d.ts +30 -0
  5. package/build/bridge.d.ts.map +1 -0
  6. package/build/bridge.js +108 -0
  7. package/build/bridge.js.map +1 -0
  8. package/build/config.d.ts +22 -0
  9. package/build/config.d.ts.map +1 -0
  10. package/build/config.js +41 -0
  11. package/build/config.js.map +1 -0
  12. package/build/consts.d.ts +33 -0
  13. package/build/consts.d.ts.map +1 -0
  14. package/build/consts.js +35 -0
  15. package/build/consts.js.map +1 -0
  16. package/build/errors.d.ts +59 -0
  17. package/build/errors.d.ts.map +1 -0
  18. package/build/errors.js +90 -0
  19. package/build/errors.js.map +1 -0
  20. package/build/index.d.ts +8 -0
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js +8 -1
  23. package/build/index.js.map +1 -1
  24. package/build/protocol.d.ts +11 -0
  25. package/build/protocol.d.ts.map +1 -0
  26. package/build/protocol.js +59 -0
  27. package/build/protocol.js.map +1 -0
  28. package/build/transport.d.ts +15 -0
  29. package/build/transport.d.ts.map +1 -0
  30. package/build/transport.js +62 -0
  31. package/build/transport.js.map +1 -0
  32. package/build/types.d.ts +248 -0
  33. package/build/types.d.ts.map +1 -0
  34. package/build/types.js +2 -0
  35. package/build/types.js.map +1 -0
  36. package/build/worker.d.ts +27 -0
  37. package/build/worker.d.ts.map +1 -0
  38. package/build/worker.js +58 -0
  39. package/build/worker.js.map +1 -0
  40. package/package.json +12 -2
  41. package/src/bridge.ts +126 -0
  42. package/src/config.ts +56 -0
  43. package/src/consts.ts +39 -0
  44. package/src/errors.ts +106 -0
  45. package/src/index.ts +8 -0
  46. package/src/protocol.ts +81 -0
  47. package/src/transport.ts +82 -0
  48. package/src/types.ts +260 -0
  49. package/src/worker.ts +72 -0
  50. package/tests/bridge.spec.ts +64 -0
  51. package/tests/config.spec.ts +66 -0
  52. package/tests/protocol.spec.ts +58 -0
  53. package/tsconfig.json +1 -0
  54. package/build/.gitkeep +0 -0
@@ -0,0 +1,248 @@
1
+ import type { BasicConfig, BasicContext, InitializedService } from '@owlmeans/context';
2
+ import type { PubSubResource, Resource, ResourceRecord } from '@owlmeans/resource';
3
+ import type { AbstractRequest, AbstractResponse } from '@owlmeans/entrypoint';
4
+ import type { JobEventType, JobState } from './consts.js';
5
+ /**
6
+ * Everything the monorepo knows about its queues, declared in the shared backend package so that
7
+ * producer and consumer read the same list. A process becomes a worker by naming queues in
8
+ * `listen` — nothing in a declaration decides where it runs.
9
+ */
10
+ export interface QueueConfig {
11
+ queues?: QueueDeclaration[];
12
+ /** The queues THIS process consumes. Absent or empty means producer only. */
13
+ listen?: string[];
14
+ /** Which db service carries the queues. Defaults to the queue service's own alias. */
15
+ db?: string;
16
+ defaults?: JobOptions;
17
+ /**
18
+ * How long a signed job envelope stays acceptable, in seconds. It bounds both the freshness
19
+ * check and the nonce cache, so a job delayed behind a long backlog is still admitted.
20
+ */
21
+ envelopeTtl?: number;
22
+ }
23
+ export interface QueueDeclaration {
24
+ name: string;
25
+ /**
26
+ * The job names this queue accepts. A job the queue does not declare is refused at enqueue
27
+ * time rather than becoming a job nothing can process.
28
+ */
29
+ jobs: string[];
30
+ worker?: QueueWorkerOptions;
31
+ defaults?: JobOptions;
32
+ }
33
+ export interface QueueWorkerOptions {
34
+ concurrency?: number;
35
+ /**
36
+ * How long a job may hold its lock without reporting progress. A processor that runs longer
37
+ * than this between `touch()` calls is treated as stalled and its job is handed to someone else.
38
+ */
39
+ lockDuration?: number;
40
+ stalledInterval?: number;
41
+ maxStalledCount?: number;
42
+ }
43
+ export interface JobOptions {
44
+ attempts?: number;
45
+ backoff?: {
46
+ type: 'fixed' | 'exponential';
47
+ delay: number;
48
+ };
49
+ delay?: number;
50
+ priority?: number;
51
+ /**
52
+ * A caller-chosen identity for the job. Enqueueing the same id twice while the first is still
53
+ * around is a no-op, which is what makes an admission step safe to retry.
54
+ */
55
+ id?: string;
56
+ removeOnComplete?: boolean | number;
57
+ removeOnFail?: boolean | number;
58
+ }
59
+ /**
60
+ * A job as a RECORD — the same shape the resource contract reads and writes, so inspecting a
61
+ * queue is `list`/`get`/`count` rather than a broker-specific API.
62
+ */
63
+ export interface JobRecord<D = unknown, R = unknown> extends ResourceRecord {
64
+ queue: string;
65
+ name: string;
66
+ data: D;
67
+ state?: JobState;
68
+ result?: R;
69
+ error?: string;
70
+ attempts?: number;
71
+ progress?: unknown;
72
+ parentId?: string;
73
+ createdAt?: string;
74
+ startedAt?: string;
75
+ finishedAt?: string;
76
+ opts?: JobOptions;
77
+ }
78
+ export interface JobEvent<R = unknown> {
79
+ type: JobEventType;
80
+ id: string;
81
+ queue: string;
82
+ /**
83
+ * Empty when the job is already gone. A broker's completion event carries only an id, so the
84
+ * name is read back from the job — and a queue that removes jobs on completion has nothing left
85
+ * to read. Subscribe for the id and the outcome; do not branch on the name.
86
+ */
87
+ name: string;
88
+ result?: R;
89
+ error?: string;
90
+ progress?: unknown;
91
+ }
92
+ /** A node of a job graph: children run first, siblings run together. */
93
+ export interface FlowSpec<D = unknown> {
94
+ name: string;
95
+ queue?: string;
96
+ data: D;
97
+ opts?: JobOptions;
98
+ children?: FlowSpec[];
99
+ }
100
+ /**
101
+ * One queue, addressed as a resource.
102
+ *
103
+ * `create` enqueues, `get`/`load`/`list`/`count` inspect, `delete`/`take` cancel. What the base
104
+ * contract cannot express — waiting for an answer, submitting a graph, releasing the connection —
105
+ * is added here.
106
+ *
107
+ * Three places where the base `Resource` contract reads differently against a broker, because a
108
+ * job is not a row:
109
+ * - `create` does NOT throw `RecordExists` for a repeated `opts.id`. Re-enqueueing an id that is
110
+ * still around returns the existing job, which is the idempotency that makes an admission step
111
+ * safe to retry.
112
+ * - `WriteOptions.ttl` is refused. A job's lifetime is `removeOnComplete`/`removeOnFail` —
113
+ * explicit deletion once it has finished, not expiry while it waits.
114
+ * - `update`/`save` reach only the job's `data`. Everything else about a job belongs to the
115
+ * broker, and an implementation refuses rather than pretending to write it.
116
+ */
117
+ export interface QueueResource<D = unknown, R = unknown> extends Resource<JobRecord<D, R>>, PubSubResource<JobEvent<R>> {
118
+ queue: string;
119
+ /**
120
+ * Release the connections this queue holds.
121
+ *
122
+ * Watching for completions means a blocking connection, and a process that never releases it
123
+ * does not exit. Nothing else in the resource contract can express that, so it lives here and a
124
+ * producer calls it on shutdown.
125
+ */
126
+ close: () => Promise<void>;
127
+ /**
128
+ * Wait for a job's return value.
129
+ * @throws {QueueTimeout} when the wait elapses — the job itself is left alone.
130
+ * @throws the processor's own error, rebuilt as its original class.
131
+ */
132
+ wait: (id: string, opts?: {
133
+ timeout?: number;
134
+ }) => Promise<R>;
135
+ /** Submit a graph and get its root back. Children complete before their parent starts. */
136
+ flow: (root: FlowSpec<D>) => Promise<JobRecord<D, R>>;
137
+ counts: () => Promise<Record<JobState, number>>;
138
+ }
139
+ /**
140
+ * What a processor is handed. `data` is the job's payload; everything else is the means to stay
141
+ * alive and to read what the children reported.
142
+ */
143
+ export interface JobContext<D = unknown> {
144
+ id: string;
145
+ name: string;
146
+ queue: string;
147
+ attempt: number;
148
+ data: D;
149
+ signal: AbortSignal;
150
+ /**
151
+ * Renew the lock. A processor that runs a long loop MUST call it as it goes: the broker judges
152
+ * liveness by the lock, so silence for longer than `lockDuration` is indistinguishable from a
153
+ * dead worker, and the job is re-run somewhere else while this one is still working on it.
154
+ */
155
+ touch: () => Promise<void>;
156
+ progress: (value: unknown) => Promise<void>;
157
+ /**
158
+ * What the children returned, keyed by child job id.
159
+ *
160
+ * A broker keys them by its own fully qualified job key; an implementation re-keys to the bare
161
+ * id, so two children of one parent that live in DIFFERENT queues and happen to share an id
162
+ * collide here. Give the children of a cross-queue flow distinct ids.
163
+ */
164
+ children: <T = unknown>() => Promise<Record<string, T>>;
165
+ failedChildren: () => Promise<Record<string, string>>;
166
+ }
167
+ export interface JobProcessor<D = unknown, R = unknown> {
168
+ (job: JobContext<D>): Promise<R>;
169
+ }
170
+ /**
171
+ * The consuming half. It is registered only in processes that consume: `start()` binds the
172
+ * queues named in `cfg.listen` and dispatches by job name, to a processor registered here or to
173
+ * an entrypoint served in this process.
174
+ */
175
+ export interface QueueWorkerService extends InitializedService {
176
+ process: <D, R>(queue: string, name: string, processor: JobProcessor<D, R>) => void;
177
+ start: () => Promise<void>;
178
+ stop: () => Promise<void>;
179
+ /** The queues this process actually consumes. Empty in a producer. */
180
+ listening: () => string[];
181
+ /**
182
+ * Bind the application's reactions to job lifecycle events. Registering more than once merges,
183
+ * so an application composed of several parts can each contribute what it cares about.
184
+ *
185
+ * It belongs on the contract rather than on a driver because `onJobDead` is where an
186
+ * application COMPENSATES — releases the lock the admission step took, marks the record failed —
187
+ * and that behaviour must not have to be rewritten when the broker changes.
188
+ */
189
+ hooks: (hooks: QueueHooks) => void;
190
+ }
191
+ /**
192
+ * The envelope a queued entrypoint call travels in. It is the request, minus everything that only
193
+ * means something to HTTP — the transport rebuilds a request from it on the far side.
194
+ */
195
+ export interface JobEnvelope {
196
+ alias: string;
197
+ params?: Record<string, unknown>;
198
+ body?: unknown;
199
+ query?: Record<string, unknown>;
200
+ headers?: Record<string, string | undefined>;
201
+ /** When the producer enqueued it — freshness is judged here, not at processing time. */
202
+ enqueuedAt?: string;
203
+ }
204
+ export interface QueueJobMeta {
205
+ id: string;
206
+ name: string;
207
+ queue: string;
208
+ attempt: number;
209
+ /** Renew the broker lock for protocol handlers whose work can outlive one lock duration. */
210
+ touch: () => Promise<void>;
211
+ }
212
+ /** What a processor returns for an entrypoint job, so the caller can rebuild the reply. */
213
+ export interface JobReply<T = unknown> {
214
+ value?: T;
215
+ outcome?: string;
216
+ error?: unknown;
217
+ }
218
+ export interface QueueHooks {
219
+ wrapHandler?: (job: JobContext, next: () => Promise<unknown>) => Promise<unknown>;
220
+ onJobResult?: (event: JobEvent) => void | Promise<void>;
221
+ onJobStalled?: (meta: {
222
+ id: string;
223
+ queue: string;
224
+ name: string;
225
+ }, phase: 'stalled' | 'failed') => void | Promise<void>;
226
+ /**
227
+ * The job is finished for good — retries exhausted, or it was dropped. This is where an
228
+ * application compensates: release the lock the admission step took, mark the record failed.
229
+ */
230
+ onJobDead?: (job: JobRecord, reason: string) => void | Promise<void>;
231
+ }
232
+ export interface QueueDriver {
233
+ resource: <D, R>(queue: string) => QueueResource<D, R>;
234
+ worker: () => QueueWorkerService;
235
+ }
236
+ export interface Config extends BasicConfig {
237
+ queue?: QueueConfig;
238
+ }
239
+ export interface Context<C extends Config = Config> extends BasicContext<C> {
240
+ }
241
+ export interface QueueAppend {
242
+ jobs: <D = unknown, R = unknown>(queue?: string) => QueueResource<D, R>;
243
+ }
244
+ export interface QueueTransportRequest extends AbstractRequest {
245
+ }
246
+ export interface QueueTransportResponse extends AbstractResponse<unknown> {
247
+ }
248
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/build/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ import type { Middleware } from '@owlmeans/context';
2
+ import type { CommonEntrypoint } from '@owlmeans/entrypoint';
3
+ import type { Config, Context, JobEnvelope, JobProcessor } from './types.js';
4
+ /**
5
+ * Every queued entrypoint this process both SERVES and LISTENS to, grouped by queue.
6
+ *
7
+ * Both halves are required and they answer different questions. Serving is about code — the alias
8
+ * was bound here, so a handler exists. Listening is about deployment — this process was
9
+ * configured to consume that queue. A worker that bound queues by what it can serve would make
10
+ * every deployment of the same binary a worker for everything it happens to import.
11
+ */
12
+ export declare const servedJobs: <C extends Config, T extends Context<C>>(context: T) => Map<string, CommonEntrypoint[]>;
13
+ /**
14
+ * A processor that runs a queued entrypoint call. The driver dispatches by job name, and an
15
+ * entrypoint job's name IS its alias — which is what lets one worker carry both entrypoint jobs
16
+ * and the internal steps an application registers with `process()`.
17
+ */
18
+ export declare const entrypointProcessor: <C extends Config, T extends Context<C>>(context: T) => JobProcessor<JobEnvelope, unknown>;
19
+ /**
20
+ * Start the worker once the context is ready.
21
+ *
22
+ * Ready stage, not Loading: processors are registered while the application wires itself up, and
23
+ * binding the queues before that finished would take jobs this process cannot yet run. A process
24
+ * that listens to nothing registers no worker at all, so a producer pays nothing for this.
25
+ */
26
+ export declare const queueWorkerMiddleware: (alias?: string) => Middleware;
27
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAE5D,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAc,WAAW,EAAE,YAAY,EAAsB,MAAM,YAAY,CAAA;AAI5G;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WACtD,CAAC,KACT,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAoBhC,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WAC/D,CAAC,KACT,YAAY,CAAC,WAAW,EAAE,OAAO,CACmC,CAAA;AAEvE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,WAAW,MAAM,KAAmB,UAcpE,CAAA"}
@@ -0,0 +1,58 @@
1
+ import { MiddlewareStage, MiddlewareType, AppType } from '@owlmeans/context';
2
+ import { RouteProtocols } from '@owlmeans/route';
3
+ import { handleJob } from './bridge.js';
4
+ import { DEFAULT_ALIAS } from './consts.js';
5
+ /**
6
+ * Every queued entrypoint this process both SERVES and LISTENS to, grouped by queue.
7
+ *
8
+ * Both halves are required and they answer different questions. Serving is about code — the alias
9
+ * was bound here, so a handler exists. Listening is about deployment — this process was
10
+ * configured to consume that queue. A worker that bound queues by what it can serve would make
11
+ * every deployment of the same binary a worker for everything it happens to import.
12
+ */
13
+ export const servedJobs = (context) => {
14
+ const listen = context.cfg.queue?.listen ?? [];
15
+ const served = new Map();
16
+ context.entrypoints().forEach(entrypoint => {
17
+ const route = entrypoint.route.route;
18
+ if (route.type !== AppType.Backend || route.protocol !== RouteProtocols.QUEUE) {
19
+ return;
20
+ }
21
+ if (route.service != null && route.service !== context.cfg.service) {
22
+ return;
23
+ }
24
+ if (entrypoint.handle == null || route.queue == null || !listen.includes(route.queue)) {
25
+ return;
26
+ }
27
+ served.set(route.queue, [...(served.get(route.queue) ?? []), entrypoint]);
28
+ });
29
+ return served;
30
+ };
31
+ /**
32
+ * A processor that runs a queued entrypoint call. The driver dispatches by job name, and an
33
+ * entrypoint job's name IS its alias — which is what lets one worker carry both entrypoint jobs
34
+ * and the internal steps an application registers with `process()`.
35
+ */
36
+ export const entrypointProcessor = (context) => async (job) => await handleJob(context, job);
37
+ /**
38
+ * Start the worker once the context is ready.
39
+ *
40
+ * Ready stage, not Loading: processors are registered while the application wires itself up, and
41
+ * binding the queues before that finished would take jobs this process cannot yet run. A process
42
+ * that listens to nothing registers no worker at all, so a producer pays nothing for this.
43
+ */
44
+ export const queueWorkerMiddleware = (alias = DEFAULT_ALIAS) => ({
45
+ type: MiddlewareType.Context,
46
+ stage: MiddlewareStage.Ready,
47
+ apply: async (context) => {
48
+ const ctx = context;
49
+ if ((ctx.cfg.queue?.listen ?? []).length === 0) {
50
+ return;
51
+ }
52
+ if (!ctx.hasService(alias)) {
53
+ return;
54
+ }
55
+ await ctx.service(alias).start();
56
+ }
57
+ });
58
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAE5E,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CACxB,OAAU,EACuB,EAAE;IACnC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,IAAI,EAAE,CAAA;IAC9C,MAAM,MAAM,GAAG,IAAI,GAAG,EAA8B,CAAA;IAEpD,OAAO,CAAC,WAAW,EAAoB,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE;QAC3D,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,KAAK,CAAA;QACpC,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,OAAO,IAAI,KAAK,CAAC,QAAQ,KAAK,cAAc,CAAC,KAAK,EAAE,CAAC;YAC9E,OAAM;QACR,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,IAAI,IAAI,IAAI,KAAK,CAAC,OAAO,KAAK,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;YACnE,OAAM;QACR,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,IAAI,IAAI,IAAI,KAAK,CAAC,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YACtF,OAAM;QACR,CAAC;QAED,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,UAAU,CAAC,CAAC,CAAA;IAC3E,CAAC,CAAC,CAAA;IAEF,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,OAAU,EAC0B,EAAE,CACtC,KAAK,EAAE,GAA4B,EAAE,EAAE,CAAC,MAAM,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAEvE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,KAAK,GAAW,aAAa,EAAc,EAAE,CAAC,CAAC;IACnF,IAAI,EAAE,cAAc,CAAC,OAAO;IAC5B,KAAK,EAAE,eAAe,CAAC,KAAK;IAC5B,KAAK,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;QACrB,MAAM,GAAG,GAAG,OAAqC,CAAA;QACjD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/C,OAAM;QACR,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAM;QACR,CAAC;QAED,MAAM,GAAG,CAAC,OAAO,CAAqB,KAAK,CAAC,CAAC,KAAK,EAAE,CAAA;IACtD,CAAC;CACF,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@owlmeans/queue",
3
- "version": "0.1.18-rc.2",
3
+ "version": "0.1.18-rc.20",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsc -b",
8
8
  "dev": "sleep 234 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
9
- "watch": "tsc -b -w --preserveWatchOutput --pretty"
9
+ "watch": "tsc -b -w --preserveWatchOutput --pretty",
10
+ "test": "bun test ./tests"
10
11
  },
11
12
  "main": "build/index.js",
12
13
  "module": "build/index.js",
@@ -28,5 +29,14 @@
28
29
  },
29
30
  "publishConfig": {
30
31
  "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@owlmeans/auth": "^0.1.18-rc.20",
35
+ "@owlmeans/auth-common": "^0.1.18-rc.24",
36
+ "@owlmeans/context": "^0.1.18-rc.19",
37
+ "@owlmeans/entrypoint": "^0.1.18-rc.22",
38
+ "@owlmeans/error": "^0.1.18-rc.19",
39
+ "@owlmeans/resource": "^0.1.18-rc.20",
40
+ "@owlmeans/route": "^0.1.18-rc.20"
31
41
  }
32
42
  }
package/src/bridge.ts ADDED
@@ -0,0 +1,126 @@
1
+ import type { AbstractRequest, CommonEntrypoint, GuardService } from '@owlmeans/entrypoint'
2
+ import type { BasicContext } from '@owlmeans/context'
3
+ import { EntrypointOutcome, provideResponse } from '@owlmeans/entrypoint'
4
+ import type { Auth } from '@owlmeans/auth'
5
+ import { AuthorizationError } from '@owlmeans/auth'
6
+ import { attachEntity } from '@owlmeans/auth-common'
7
+ import { ResilientError } from '@owlmeans/error'
8
+ import type { Config, Context, JobContext, JobEnvelope, JobReply, QueueJobMeta } from './types.js'
9
+ import { EnvelopeExpired, JobNotServed } from './errors.js'
10
+
11
+ /**
12
+ * Rebuild the request the producer described. A queued call is still a call: the same guards, the
13
+ * same filter, the same handler run against it — only the wire is different.
14
+ */
15
+ export const requestOf = (
16
+ envelope: JobEnvelope, path: string, context?: BasicContext<any>, job?: QueueJobMeta,
17
+ ): AbstractRequest => ({
18
+ alias: envelope.alias,
19
+ params: (envelope.params ?? {}) as AbstractRequest['params'],
20
+ body: envelope.body as AbstractRequest['body'],
21
+ headers: (envelope.headers ?? {}) as AbstractRequest['headers'],
22
+ query: (envelope.query ?? {}) as AbstractRequest['query'],
23
+ path,
24
+ // Handlers reach their context through `original._ctx`, which the HTTP boundary sets on the raw
25
+ // Fastify request. There is no raw request here, so the shape is supplied deliberately — a
26
+ // handler must not have to know which transport delivered it.
27
+ original: context != null || job != null ? { _ctx: context, _job: job } : undefined,
28
+ })
29
+
30
+ /** Read broker identity only where an entrypoint truly needs to bind work to an admission claim. */
31
+ export const queueJobOf = (request: unknown): QueueJobMeta | null => {
32
+ if (request == null || typeof request !== 'object' || !('original' in request)) return null
33
+ const original = (request as { original?: unknown }).original
34
+ if (original == null || typeof original !== 'object' || !('_job' in original)) return null
35
+ return (original as { _job?: QueueJobMeta })._job ?? null
36
+ }
37
+
38
+ /**
39
+ * Freshness is judged from when the job was ENQUEUED, not from now.
40
+ *
41
+ * A signed envelope proves who produced it, and a replay window keeps a captured one from being
42
+ * useful forever. But a job can legitimately sit behind a long backlog, and judging it on pickup
43
+ * would reject exactly the work that a busy queue delayed — so the producer's timestamp is what
44
+ * the window applies to.
45
+ *
46
+ * @throws {EnvelopeExpired}
47
+ */
48
+ export const assertFresh = (envelope: JobEnvelope, ttl?: number): void => {
49
+ if (ttl == null || envelope.enqueuedAt == null) {
50
+ return
51
+ }
52
+ const age = (Date.now() - new Date(envelope.enqueuedAt).getTime()) / 1000
53
+ if (age > ttl) {
54
+ throw new EnvelopeExpired(`${envelope.alias}: ${Math.round(age)}s > ${ttl}s`)
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Run one queued entrypoint call and describe the outcome.
60
+ *
61
+ * It never throws for a DOMAIN failure — the error is marshalled into the returned reply so the
62
+ * producer can rebuild it as its own class, and so the broker does not count a legitimate refusal
63
+ * as a job to retry. Infrastructure failures are left to propagate: those are what retries are for.
64
+ */
65
+ export const handleJob = async <C extends Config, T extends Context<C>>(
66
+ context: T, job: JobContext<JobEnvelope>
67
+ ): Promise<JobReply> => {
68
+ const envelope = job.data
69
+
70
+ try {
71
+ assertFresh(envelope, context.cfg.queue?.envelopeTtl)
72
+
73
+ const entrypoint = context.entrypoint<CommonEntrypoint>(envelope.alias)
74
+ if (entrypoint.handle == null) {
75
+ throw new JobNotServed(envelope.alias)
76
+ }
77
+
78
+ const request = requestOf(envelope, entrypoint.path(), context, {
79
+ id: job.id, name: job.name, queue: job.queue, attempt: job.attempt, touch: job.touch,
80
+ })
81
+ const response = provideResponse<unknown>()
82
+
83
+ const guards = entrypoint.getGuards()
84
+ if (guards.length > 0) {
85
+ let matched: GuardService | undefined
86
+ for (const alias of guards) {
87
+ const guard: GuardService = context.service(alias)
88
+ if (await guard.match(request, response)) {
89
+ matched = guard
90
+ break
91
+ }
92
+ }
93
+ if (matched == null) {
94
+ throw new AuthorizationError(`queue:${envelope.alias}`)
95
+ }
96
+
97
+ const auth = provideResponse<Auth>()
98
+ if (!await matched.handle<boolean>(request, auth)) {
99
+ throw new AuthorizationError(`queue:${envelope.alias}:${matched.alias}`)
100
+ }
101
+ request.auth = auth.value
102
+
103
+ const entity = await attachEntity(context, request)
104
+ if (entity != null) {
105
+ request.entity = entity
106
+ }
107
+ }
108
+
109
+ await entrypoint.handle(request, response)
110
+
111
+ if (response.error != null) {
112
+ return { error: ResilientError.marshal(response.error).message }
113
+ }
114
+
115
+ return { value: response.value, outcome: response.outcome ?? EntrypointOutcome.Ok }
116
+ } catch (e) {
117
+ // A refusal the caller must see as its own class — `ContentRefused`, `AgentLocked` — travels
118
+ // in the reply. Only a broker- or connection-level fault should ever reach the retry logic:
119
+ // retrying a refusal just spends the work again to be refused identically.
120
+ if (e instanceof ResilientError) {
121
+ return { error: ResilientError.marshal(e).message }
122
+ }
123
+
124
+ throw e
125
+ }
126
+ }
package/src/config.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { Config, JobOptions, QueueDeclaration, QueueWorkerOptions } from './types.js'
2
+ import { UnknownQueue } from './errors.js'
3
+
4
+ /**
5
+ * Add a queue to the shared configuration. Every process in the monorepo loads the same
6
+ * declarations — a producer needs them to know a queue exists and what it accepts, a worker needs
7
+ * them to bind. Declaring twice replaces, so a helper that runs per app is safe to call.
8
+ */
9
+ export const declareQueue = <C extends Config>(
10
+ cfg: C, name: string, jobs: string[], opts?: { worker?: QueueWorkerOptions, defaults?: JobOptions }
11
+ ): C => {
12
+ const queue: QueueDeclaration = { name, jobs, ...opts }
13
+ const queues = cfg.queue?.queues ?? []
14
+ const existing = queues.findIndex(declared => declared.name === name)
15
+
16
+ if (existing < 0) {
17
+ queues.push(queue)
18
+ } else {
19
+ queues[existing] = queue
20
+ }
21
+
22
+ cfg.queue = { ...cfg.queue, queues }
23
+
24
+ return cfg
25
+ }
26
+
27
+ /**
28
+ * Name the queues this process consumes. It is what turns an otherwise identical binary into a
29
+ * worker, which is why it lives in the process's own config and never in a declaration.
30
+ */
31
+ export const listenQueues = <C extends Config>(cfg: C, ...names: string[]): C => {
32
+ const listen = new Set([...(cfg.queue?.listen ?? []), ...names])
33
+
34
+ cfg.queue = { ...cfg.queue, listen: [...listen] }
35
+
36
+ return cfg
37
+ }
38
+
39
+ /**
40
+ * @throws {UnknownQueue}
41
+ */
42
+ export const queueOf = <C extends Config>(cfg: C, name: string): QueueDeclaration => {
43
+ const queue = cfg.queue?.queues?.find(declared => declared.name === name)
44
+
45
+ if (queue == null) {
46
+ throw new UnknownQueue(name)
47
+ }
48
+
49
+ return queue
50
+ }
51
+
52
+ export const queueOfJob = <C extends Config>(cfg: C, job: string): QueueDeclaration | undefined =>
53
+ cfg.queue?.queues?.find(declared => declared.jobs.includes(job))
54
+
55
+ export const isListening = <C extends Config>(cfg: C, name: string): boolean =>
56
+ cfg.queue?.listen?.includes(name) === true
package/src/consts.ts ADDED
@@ -0,0 +1,39 @@
1
+
2
+ export const DEFAULT_ALIAS = 'queue'
3
+
4
+ /**
5
+ * The state a job is in, as the broker reports it. `Waiting` covers both a job queued normally
6
+ * and one held by an unfinished parent — the difference matters to the broker, never to a caller
7
+ * asking whether an answer has arrived.
8
+ */
9
+ export enum JobState {
10
+ Waiting = 'waiting',
11
+ Delayed = 'delayed',
12
+ Active = 'active',
13
+ Completed = 'completed',
14
+ Failed = 'failed',
15
+ Unknown = 'unknown',
16
+ }
17
+
18
+ /** A job has reached a state it will not leave on its own. */
19
+ export const isSettled = (state?: JobState): boolean =>
20
+ state === JobState.Completed || state === JobState.Failed
21
+
22
+ export enum JobEventType {
23
+ Completed = 'completed',
24
+ Failed = 'failed',
25
+ Progress = 'progress',
26
+ }
27
+
28
+ /**
29
+ * How long a caller waits for a reply when neither the route nor the request says. Long enough
30
+ * for work that talks to a model, short enough that a lost job surfaces as an error rather than a
31
+ * hung request.
32
+ */
33
+ export const DEFAULT_JOB_TIMEOUT = 60_000
34
+
35
+ /**
36
+ * Retries default to one attempt: the jobs here are mostly LLM work, and a blind retry re-spends
37
+ * the tokens that just failed. A queue whose jobs are cheap and idempotent raises it per queue.
38
+ */
39
+ export const DEFAULT_ATTEMPTS = 1