@ultimat3/jobs 1.0.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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/package.json +38 -0
  4. package/src/clock.d.ts +7 -0
  5. package/src/clock.d.ts.map +1 -0
  6. package/src/clock.js +21 -0
  7. package/src/clock.js.map +1 -0
  8. package/src/clock.ts +23 -0
  9. package/src/describe.ts +61 -0
  10. package/src/driver-memory.d.ts +9 -0
  11. package/src/driver-memory.d.ts.map +1 -0
  12. package/src/driver-memory.js +189 -0
  13. package/src/driver-memory.js.map +1 -0
  14. package/src/driver-memory.ts +216 -0
  15. package/src/driver-nats.d.ts +7 -0
  16. package/src/driver-nats.d.ts.map +1 -0
  17. package/src/driver-nats.js +51 -0
  18. package/src/driver-nats.js.map +1 -0
  19. package/src/driver-nats.ts +73 -0
  20. package/src/driver-pg-sql.d.ts +19 -0
  21. package/src/driver-pg-sql.d.ts.map +1 -0
  22. package/src/driver-pg-sql.js +160 -0
  23. package/src/driver-pg-sql.js.map +1 -0
  24. package/src/driver-pg-sql.ts +170 -0
  25. package/src/driver-pg.d.ts +17 -0
  26. package/src/driver-pg.d.ts.map +1 -0
  27. package/src/driver-pg.js +246 -0
  28. package/src/driver-pg.js.map +1 -0
  29. package/src/driver-pg.ts +356 -0
  30. package/src/driver-redis.d.ts +7 -0
  31. package/src/driver-redis.d.ts.map +1 -0
  32. package/src/driver-redis.js +54 -0
  33. package/src/driver-redis.js.map +1 -0
  34. package/src/driver-redis.ts +76 -0
  35. package/src/driver.d.ts +114 -0
  36. package/src/driver.d.ts.map +1 -0
  37. package/src/driver.js +14 -0
  38. package/src/driver.js.map +1 -0
  39. package/src/driver.ts +143 -0
  40. package/src/errors.d.ts +63 -0
  41. package/src/errors.d.ts.map +1 -0
  42. package/src/errors.js +105 -0
  43. package/src/errors.js.map +1 -0
  44. package/src/errors.ts +165 -0
  45. package/src/events.d.ts +35 -0
  46. package/src/events.d.ts.map +1 -0
  47. package/src/events.js +92 -0
  48. package/src/events.js.map +1 -0
  49. package/src/events.ts +134 -0
  50. package/src/index.d.ts +32 -0
  51. package/src/index.d.ts.map +1 -0
  52. package/src/index.js +18 -0
  53. package/src/index.js.map +1 -0
  54. package/src/index.ts +172 -0
  55. package/src/inspect.d.ts +82 -0
  56. package/src/inspect.d.ts.map +1 -0
  57. package/src/inspect.js +113 -0
  58. package/src/inspect.js.map +1 -0
  59. package/src/inspect.ts +213 -0
  60. package/src/job.d.ts +71 -0
  61. package/src/job.d.ts.map +1 -0
  62. package/src/job.js +99 -0
  63. package/src/job.js.map +1 -0
  64. package/src/job.ts +261 -0
  65. package/src/limits.d.ts +47 -0
  66. package/src/limits.d.ts.map +1 -0
  67. package/src/limits.js +0 -0
  68. package/src/limits.js.map +1 -0
  69. package/src/limits.ts +0 -0
  70. package/src/outbox.d.ts +81 -0
  71. package/src/outbox.d.ts.map +1 -0
  72. package/src/outbox.js +202 -0
  73. package/src/outbox.js.map +1 -0
  74. package/src/outbox.ts +336 -0
  75. package/src/register.ts +40 -0
  76. package/src/retry.d.ts +40 -0
  77. package/src/retry.d.ts.map +1 -0
  78. package/src/retry.js +59 -0
  79. package/src/retry.js.map +1 -0
  80. package/src/retry.ts +90 -0
  81. package/src/scheduler.d.ts +79 -0
  82. package/src/scheduler.d.ts.map +1 -0
  83. package/src/scheduler.js +183 -0
  84. package/src/scheduler.js.map +1 -0
  85. package/src/scheduler.ts +417 -0
  86. package/src/steps.d.ts +86 -0
  87. package/src/steps.d.ts.map +1 -0
  88. package/src/steps.js +227 -0
  89. package/src/steps.js.map +1 -0
  90. package/src/steps.ts +339 -0
  91. package/src/worker.d.ts +68 -0
  92. package/src/worker.d.ts.map +1 -0
  93. package/src/worker.js +273 -0
  94. package/src/worker.js.map +1 -0
  95. package/src/worker.ts +356 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 developerz.ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # @ultimat3/jobs ⚙️
2
+
3
+ Durable background work. Steps that replay, a transactional outbox that is on by default,
4
+ and one driver interface so Postgres → Redis → NATS is a config line.
5
+
6
+ ```ts
7
+ import { job, t } from '@ultimat3/jobs';
8
+
9
+ export const onboardOrg = job({
10
+ input: t.object({ orgId: t.uuid }),
11
+ idempotencyKey: ({ orgId }) => `onboard:${orgId}`, // REQUIRED by the type
12
+ retry: { attempts: 5, backoff: 'exponential' },
13
+ async run({ input, step, ctx }) {
14
+ const org = await step.run('provision', () => ctx.orgs.provision(input.orgId));
15
+ await step.run('welcome-email', () => ctx.mail.send(welcomeEmail, org));
16
+ await step.sleep('3d');
17
+ await step.run('nudge', () => ctx.mail.send(nudgeEmail, org));
18
+ },
19
+ });
20
+ ```
21
+
22
+ `welcome-email` fails? Attempt 2 replays `provision` **from storage** and re-sends only the
23
+ email. The 3-day sleep does not hold a process: the run suspends and the queue redelivers it.
24
+
25
+ ## The handle is the surface
26
+
27
+ ```ts
28
+ await onboardOrg.enqueue({ orgId }); // joins the ambient transaction
29
+ await onboardOrg.as(ctx.actor, { orgId }); // same, tenantId stamped from the actor
30
+ onboardOrg.describe(); // the manifest row for this job
31
+ await nightlyDigest.enqueue(); // fire a task's entries now
32
+ nightlyDigest.describe(); // { kind: 'task', cron, tz, jobs, … }
33
+ ```
34
+
35
+ | Member | On | Behaviour |
36
+ |---|---|---|
37
+ | `enqueue(input, options?)` | `JobHandle` | queues through `jobsFacade()`; joins the caller's `tx` when the outbox is installed |
38
+ | `as(actor, input, options?)` | `JobHandle` | enqueue on behalf of an actor — fills `tenantId` from `actor.orgId` so per-tenant limits apply |
39
+ | `describe()` | `JobHandle` | the `JobDescriptor` the manifest, `/_x` and MCP read |
40
+ | `entries()` | `TaskHandle` | the declared `[job, input]` pairs |
41
+ | `enqueue(options?)` | `TaskHandle` | fires every declared entry now, one result per entry |
42
+ | `describe()` | `TaskHandle` | `TaskDescriptor` — cron, tz, catch-up, jobs in declaration order |
43
+
44
+ ## The export name is the job's name
45
+
46
+ ```ts
47
+ export const api = defineApi({
48
+ jobs: [postJobs, orgJobs], // module namespaces, never a list of name strings
49
+ tasks: [scheduledTasks],
50
+ });
51
+ ```
52
+
53
+ `defineApi` hands each module to `registerJobs`/`registerTasks`, which stamp the export name
54
+ onto **the handle the module exported** and re-key the registry — `import { onboardOrg }` is the
55
+ object `enqueue()` routes through after boot, not a renamed copy of it. Hand nothing over and a
56
+ job keeps the positional name `job()` gave it: `anonymous-job-2` on the queue row, in
57
+ `x.manifest.json` and in every dead-letter trace, appearing in no source file.
58
+
59
+ A definition that sets `name:` keeps it. A job name is the durable queue key that queued,
60
+ retrying and dead-lettered rows already carry, so renaming an export must never move where they
61
+ are delivered — `@ultimat3/mail`'s `mail.send` relies on exactly that.
62
+
63
+ Registering the same handle twice is one registration seen twice, because `defineApi` and the
64
+ framework's module scan both reach the same declaration file. Everything else that puts two
65
+ things on one durable name is `X_JOB_DUPLICATE`, refused at the earliest point it is decidable:
66
+
67
+ | Collision | Refused at |
68
+ |---|---|
69
+ | two definitions setting the same `name:` | `job()` / `task()`, before either can seat the other out |
70
+ | two different handles under one export name | `defineApi` |
71
+ | one handle exported twice (`export { notify as a, notify as b }`) | `defineApi` — the second alias would move the queue key |
72
+
73
+ `registerJobs`/`registerTasks` are internal: `defineApi` reaches them through core's registrar
74
+ table, and they are not part of this package's public API. There is one way to register.
75
+
76
+ `.as()` **queues**; it never runs the handler inline. A job's execution surface is the queue,
77
+ so an inline run would be a second execution path next to the worker's — with no retry, no
78
+ dedupe and no dead letter. Its idempotency key is the job's own, so a manual `task.enqueue()`
79
+ and the scheduler's occurrence-scoped fire stay distinct rows on purpose.
80
+
81
+ With no facade installed, `enqueue` publishes straight to `jobDriver()`; with none of those
82
+ either, it is `X_DRIVER_UNAVAILABLE` at the call, not a silently dropped job.
83
+
84
+ ## `idempotencyKey` is required by the type
85
+
86
+ Not a convention, not a lint rule — a non-optional field on `JobDefinition`. Queues deliver
87
+ at least once (partition, lease expiry, outbox replay), so every job is asked "have you
88
+ already run?" whether or not its author thought about it. Optional means the answer is
89
+ usually "nobody thought about it", and the bug — two charges, two welcome emails, two
90
+ provisioned orgs — shows up in production under load and never in a test. There is no way
91
+ to define a job in Ultimate that cannot be deduped.
92
+
93
+ ## Durable steps
94
+
95
+ | Call | Behaviour |
96
+ |---|---|
97
+ | `step.run(name, fn)` | runs once ever; result persisted before the next step starts |
98
+ | `step.sleep(name, '3d')` | suspends the run, requeues it for the wake time |
99
+ | `step.sleep('3d')` | same, step name derived from the duration |
100
+ | `step.waitForEvent(name, event, { match, timeout })` | suspends until `publishEvent()` matches |
101
+
102
+ Step names are the replay key, so they must be deterministic and unique in a run — a
103
+ duplicate is `X_STEP_DUPLICATE`, not a silent overwrite. Suspension is control flow
104
+ (`StepSuspension`), never a failure: it does not burn a retry attempt.
105
+
106
+ ## The transactional outbox (on by default)
107
+
108
+ ```ts
109
+ await ctx.tx(async (tx) => {
110
+ const post = await ctx.posts.publish(input.postId, tx);
111
+ await notifySubscribers.enqueue({ postId: post.id }); // joins `tx`
112
+ });
113
+ ```
114
+
115
+ The job row is written in the **same transaction** as the business rows; a relay publishes
116
+ it after commit. The bug class this removes:
117
+
118
+ | Without an outbox | Result |
119
+ |---|---|
120
+ | enqueue, then the transaction rolls back | the job runs against rows that never existed |
121
+ | commit, then the process dies before enqueue | the job is lost, silently, forever |
122
+
123
+ Both are load-dependent, both pass every test you would write, and both produce "the email
124
+ went out but the order isn't in the database". Joining the transaction closes the window.
125
+ The relay publishes *then* marks published, so a crash re-publishes — collapsed by the
126
+ idempotency key. Set `mode: 'required'` to make an enqueue outside a transaction an
127
+ `X_OUTBOX_NO_TX` error instead of a direct publish.
128
+
129
+ ## Drivers
130
+
131
+ One interface: `enqueue`, `claim` (visibility timeout), `ack`, `nack` (backoff),
132
+ `heartbeat`, `stats`. Zero job-code change between them.
133
+
134
+ | Driver | Status | Backing | Use |
135
+ |---|---|---|---|
136
+ | `pg` | **default** | `SELECT ... FOR UPDATE SKIP LOCKED`, partial unique index, advisory-lock leader | zero-infra start, most apps |
137
+ | `memory` | complete | in-process maps | `x dev`, tests |
138
+ | `redis` | interface-complete, `X_NOT_IMPLEMENTED` | Streams + consumer groups | planned |
139
+ | `nats` | interface-complete, `X_NOT_IMPLEMENTED` | JetStream work queue | planned |
140
+
141
+ The pg SQL is exported verbatim (`SQL_CLAIM`, `SQL_ENQUEUE`, `SQL_NACK`, …) so an agent
142
+ debugging a stuck queue can read and run the exact statement.
143
+
144
+ ## Roles
145
+
146
+ | Role | Entry | Behaviour |
147
+ |---|---|---|
148
+ | `worker` | `createWorker({ driver, queues, concurrency })` | per-queue pools, heartbeat, SIGTERM drain: stop claiming → finish in-flight → close |
149
+ | `scheduler` | `createScheduler({ driver, leader })` | advisory-lock leader, one dispatcher per tick, catch-up policy |
150
+
151
+ ```ts
152
+ export const nightlyDigest = task({
153
+ cron: '0 3 * * *',
154
+ tz: 'UTC', // REQUIRED — a cron without a timezone is a bug
155
+ enqueue: () => [[sendDigest, {}]],
156
+ });
157
+ ```
158
+
159
+ `tz` is required by the type *and* validated against the runtime's IANA database, because a
160
+ non-empty string is not a timezone: `tz: 'Bogota'` would resolve every occurrence in UTC and
161
+ run five hours off, silently, forever. `0 3 * * *` in a DST zone runs twice or zero times on
162
+ the switch day. Catch-up after downtime is explicit: `skip` (default), `run-once`, `run-all`.
163
+
164
+ ## Retries
165
+
166
+ `{ attempts, backoff: 'exponential' | 'linear' | 'fixed', delay, maxDelay, jitter }`.
167
+ Equal jitter is on by default so a burst of failures does not retry in lockstep.
168
+ Exhausted jobs are dead-lettered, never dropped: `x jobs retry <id>`.
169
+
170
+ ```
171
+ retrySchedule({ attempts: 5, backoff: 'exponential', delay: 1000 })
172
+ // => [1000, 2000, 4000, 8000]
173
+ ```
174
+
175
+ ## Limits
176
+
177
+ Per-tenant concurrency (`tenantId` = the actor's `orgId`, carried on the queue row), a
178
+ per-queue cap and a global cap. Over a cap, the claim is handed straight back without
179
+ burning an attempt — one org's 50k-row import cannot starve the fleet.
180
+
181
+ ## Introspection
182
+
183
+ `inspectQueues`, `inspectJob` (per-step trace), `inspectDeadLetters`, `retryFromStep`,
184
+ `inspectManifest` — all `--json`-shaped, shared by `/_x`, the CLI and the MCP tools.
185
+
186
+ ## Errors
187
+
188
+ | Code | Cause |
189
+ |---|---|
190
+ | `X_IDEMPOTENCY_REQUIRED` | job defined without an idempotency key (JS callers) |
191
+ | `X_STEP_DUPLICATE` | two steps share a name in one run |
192
+ | `X_JOB_DUPLICATE` | enqueue collided with a live key under `onConflict: 'error'`; or two different job/task handles registered under one name |
193
+ | `X_JOB_TIMEOUT` | job or required `waitForEvent` exceeded its timeout |
194
+ | `X_JOB_MAX_ATTEMPTS` | retries exhausted, job dead-lettered |
195
+ | `X_OUTBOX_NO_TX` | enqueue outside a transaction with `mode: 'required'` |
196
+ | `X_DRIVER_UNAVAILABLE` | no `DATABASE_URL` / executor for the pg driver |
197
+ | `X_NOT_IMPLEMENTED` | redis / nats driver |
198
+
199
+ ## Boundary
200
+
201
+ Tier 3. Imports `@ultimat3/core`, `@ultimat3/schema`, `@ultimat3/entity`,
202
+ `@ultimat3/policy`, `@ultimat3/cache`, `@ultimat3/time`. Never HTTP, render or UI.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@ultimat3/jobs",
3
+ "version": "1.0.0",
4
+ "description": "Durable background work: steps, transactional outbox, cron tasks, one driver interface",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/developerz-ai/ultimate.git",
10
+ "directory": "packages/jobs"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "provenance": true
15
+ },
16
+ "exports": {
17
+ ".": "./src/index.ts"
18
+ },
19
+ "files": [
20
+ "src",
21
+ "!src/**/*.test.ts",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "engines": {
26
+ "bun": ">=1.3.0"
27
+ },
28
+ "scripts": {
29
+ "typecheck": "tsc --noEmit -p tsconfig.json",
30
+ "test": "bun test"
31
+ },
32
+ "dependencies": {
33
+ "@ultimat3/core": "1.0.0",
34
+ "@ultimat3/entity": "1.0.0",
35
+ "@ultimat3/schema": "1.0.0",
36
+ "@ultimat3/time": "1.0.0"
37
+ }
38
+ }
package/src/clock.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { Clock } from '@ultimat3/core';
2
+ export type DurationInput = string | number;
3
+ /** A Clock may hand back a `Date` or epoch ms; scheduling needs one comparable number. */
4
+ export declare function nowMs(clock?: Clock): number;
5
+ /** `'3d'` | `'30s'` | `1500` -> ms. Numbers pass through so callers may stay explicit. */
6
+ export declare function toMs(duration: DurationInput): number;
7
+ //# sourceMappingURL=clock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["clock.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAI5C,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,0FAA0F;AAC1F,wBAAgB,KAAK,CAAC,KAAK,GAAE,KAAmB,GAAG,MAAM,CAIxD;AAED,0FAA0F;AAC1F,wBAAgB,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAKpD"}
package/src/clock.js ADDED
@@ -0,0 +1,21 @@
1
+ // Internal: every scheduling decision in this package is made in epoch ms against an
2
+ // injected Clock, so tests never sleep and a frozen clock cannot be bypassed.
3
+ import { systemClock } from '@ultimat3/core';
4
+ import { parseDuration } from '@ultimat3/time';
5
+ /** A Clock may hand back a `Date` or epoch ms; scheduling needs one comparable number. */
6
+ export function nowMs(clock = systemClock) {
7
+ const reading = clock.now();
8
+ if (reading instanceof Date)
9
+ return reading.getTime();
10
+ return Number(reading);
11
+ }
12
+ /** `'3d'` | `'30s'` | `1500` -> ms. Numbers pass through so callers may stay explicit. */
13
+ export function toMs(duration) {
14
+ if (typeof duration === 'number')
15
+ return duration;
16
+ const parsed = parseDuration(duration);
17
+ if (parsed instanceof Date)
18
+ return parsed.getTime();
19
+ return Number(parsed);
20
+ }
21
+ //# sourceMappingURL=clock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.js","sourceRoot":"","sources":["clock.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,8EAA8E;AAG9E,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAI/C,0FAA0F;AAC1F,MAAM,UAAU,KAAK,CAAC,KAAK,GAAU,WAAW;IAC9C,MAAM,OAAO,GAAY,KAAK,CAAC,GAAG,EAAE,CAAC;IACrC,IAAI,OAAO,YAAY,IAAI;QAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IACtD,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC;AACzB,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,IAAI,CAAC,QAAuB;IAC1C,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAClD,MAAM,MAAM,GAAY,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChD,IAAI,MAAM,YAAY,IAAI;QAAE,OAAO,MAAM,CAAC,OAAO,EAAE,CAAC;IACpD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC;AACxB,CAAC"}
package/src/clock.ts ADDED
@@ -0,0 +1,23 @@
1
+ // Internal: every scheduling decision in this package is made in epoch ms against an
2
+ // injected Clock, so tests never sleep and a frozen clock cannot be bypassed.
3
+
4
+ import type { Clock } from '@ultimat3/core';
5
+ import { systemClock } from '@ultimat3/core';
6
+ import { parseDuration } from '@ultimat3/time';
7
+
8
+ export type DurationInput = string | number;
9
+
10
+ /** A Clock may hand back a `Date` or epoch ms; scheduling needs one comparable number. */
11
+ export function nowMs(clock: Clock = systemClock): number {
12
+ const reading: unknown = clock.now();
13
+ if (reading instanceof Date) return reading.getTime();
14
+ return Number(reading);
15
+ }
16
+
17
+ /** `'3d'` | `'30s'` | `1500` -> ms. Numbers pass through so callers may stay explicit. */
18
+ export function toMs(duration: DurationInput): number {
19
+ if (typeof duration === 'number') return duration;
20
+ const parsed: unknown = parseDuration(duration);
21
+ if (parsed instanceof Date) return parsed.getTime();
22
+ return Number(parsed);
23
+ }
@@ -0,0 +1,61 @@
1
+ // The JSON projection of one job handle, shared by `handle.describe()` and `describeJobs()`.
2
+ // Byte-stability is the whole point: `x.manifest.json` is committed and diffed, so anything
3
+ // clock- or iteration-order-dependent in this shape is a spurious change on every build.
4
+
5
+ import { toJsonSchema } from '@ultimat3/schema';
6
+ import type { BackoffStrategy, RetryPolicy } from './retry';
7
+ import { DEFAULT_RETRY } from './retry';
8
+
9
+ /** A registered job as the manifest, the `/_x` jobs panel and the MCP dev server read it. */
10
+ export interface JobDescriptor {
11
+ readonly name: string;
12
+ /** JSON Schema, exactly as an action's descriptor publishes one — never the schema object. */
13
+ readonly input: Record<string, unknown>;
14
+ readonly queue: string;
15
+ readonly retry: { readonly attempts: number; readonly backoff: BackoffStrategy };
16
+ readonly steps: readonly string[];
17
+ }
18
+
19
+ /**
20
+ * Narrower than `JobHandle` on purpose: the projection reads four declared fields, so keeping
21
+ * it structural means it never has to carry — or vary with — the handle's input generic.
22
+ */
23
+ export interface DescribableJob {
24
+ readonly name: string;
25
+ readonly queue: string;
26
+ readonly retry: RetryPolicy;
27
+ readonly input: unknown;
28
+ }
29
+
30
+ export function describeJob(handle: DescribableJob): JobDescriptor {
31
+ return {
32
+ name: handle.name,
33
+ input: describeSchema(handle.input),
34
+ queue: handle.queue,
35
+ retry: {
36
+ attempts: handle.retry.attempts,
37
+ backoff: handle.retry.backoff ?? DEFAULT_RETRY.backoff,
38
+ },
39
+ // Empty by design: step names are chosen inside `run()` at execution time, so they are
40
+ // not statically knowable. `inspect(name)` reports the steps an actual run recorded.
41
+ steps: [],
42
+ };
43
+ }
44
+
45
+ /**
46
+ * The job's input as JSON Schema. Naming the vendor — which is all this used to publish — told a
47
+ * reader of `x.manifest.json` who validated the payload and nothing about its shape, so the one
48
+ * fact the manifest exists to carry was missing. Never throws: a schema the active provider cannot
49
+ * introspect degrades to a permissive object node, because a missing manifest detail must not
50
+ * break a build.
51
+ */
52
+ function describeSchema(schema: unknown): Record<string, unknown> {
53
+ try {
54
+ const converted: unknown = toJsonSchema(schema);
55
+ if (typeof converted === 'object' && converted !== null && !Array.isArray(converted))
56
+ return converted as Record<string, unknown>;
57
+ } catch {
58
+ // fall through to the permissive node
59
+ }
60
+ return { type: 'object', additionalProperties: true };
61
+ }
@@ -0,0 +1,9 @@
1
+ import type { Clock } from '@ultimat3/core';
2
+ import type { JobDriver } from './driver';
3
+ import type { StepStore } from './steps';
4
+ export interface MemoryDriverOptions {
5
+ readonly clock?: Clock;
6
+ readonly steps?: StepStore;
7
+ }
8
+ export declare function createMemoryDriver(options?: MemoryDriverOptions): JobDriver;
9
+ //# sourceMappingURL=driver-memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driver-memory.d.ts","sourceRoot":"","sources":["driver-memory.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAG5C,OAAO,KAAK,EAKV,SAAS,EAMV,MAAM,UAAU,CAAC;AAGlB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGzC,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;CAC5B;AAID,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB,GAAG,SAAS,CAwL/E"}
@@ -0,0 +1,189 @@
1
+ // In-process driver for `x dev` and tests: same semantics as pg (visibility timeout,
2
+ // idempotency dedupe, dead-letter) with zero infrastructure, so a test suite exercises the
3
+ // real claim/ack/nack paths rather than a mock that always succeeds.
4
+ import { assert, systemClock, uuid } from '@ultimat3/core';
5
+ import { nowMs } from './clock';
6
+ import { DEFAULT_QUEUE } from './driver';
7
+ import { JobDuplicateError } from './errors';
8
+ import { createMemoryStepStore } from './steps';
9
+ const LIVE_STATES = new Set(['ready', 'delayed', 'running', 'suspended']);
10
+ export function createMemoryDriver(options = {}) {
11
+ const clock = options.clock ?? systemClock;
12
+ const steps = options.steps ?? createMemoryStepStore();
13
+ const jobs = new Map();
14
+ const liveByKey = (key) => {
15
+ for (const record of jobs.values()) {
16
+ if (record.idempotencyKey === key && LIVE_STATES.has(record.state))
17
+ return record;
18
+ }
19
+ return undefined;
20
+ };
21
+ const update = (id, patch) => {
22
+ const existing = jobs.get(id);
23
+ if (existing === undefined)
24
+ return;
25
+ jobs.set(id, { ...existing, ...patch, updatedAt: nowMs(clock) });
26
+ };
27
+ const introspect = {
28
+ job(jobId) {
29
+ return Promise.resolve(jobs.get(jobId));
30
+ },
31
+ list(filter = {}) {
32
+ const rows = [...jobs.values()]
33
+ .filter((record) => filter.queue === undefined || record.queue === filter.queue)
34
+ .filter((record) => filter.name === undefined || record.name === filter.name)
35
+ .filter((record) => filter.state === undefined || record.state === filter.state)
36
+ .sort((a, b) => a.createdAt - b.createdAt)
37
+ .slice(0, filter.limit ?? 100);
38
+ return Promise.resolve(rows);
39
+ },
40
+ deadLetters(limit = 100) {
41
+ const rows = [...jobs.values()]
42
+ .filter((record) => record.state === 'dead')
43
+ .sort((a, b) => b.updatedAt - a.updatedAt)
44
+ .slice(0, limit);
45
+ return Promise.resolve(rows);
46
+ },
47
+ async requeue(jobId, requeueOptions) {
48
+ const existing = jobs.get(jobId);
49
+ assert(existing !== undefined, `no job ${jobId} in the memory driver`, 'requeue a job id returned by enqueue() or stats() — the memory driver holds no state across processes, so an id from another run will not resolve');
50
+ const record = existing;
51
+ if (requeueOptions?.fromStep !== undefined) {
52
+ // Drop the target step so it re-executes; earlier steps stay memoized.
53
+ await steps.del(record.runId, requeueOptions.fromStep);
54
+ }
55
+ update(jobId, { state: 'ready', attempt: 0, runAt: nowMs(clock) });
56
+ const next = jobs.get(jobId);
57
+ return next ?? record;
58
+ },
59
+ };
60
+ return {
61
+ name: 'memory',
62
+ steps,
63
+ introspect,
64
+ enqueue(request) {
65
+ const existing = liveByKey(request.idempotencyKey);
66
+ if (existing !== undefined) {
67
+ if (request.onConflict === 'error') {
68
+ throw new JobDuplicateError({
69
+ job: request.name,
70
+ idempotencyKey: request.idempotencyKey,
71
+ existingId: existing.id,
72
+ });
73
+ }
74
+ return Promise.resolve({ id: existing.id, runId: existing.runId, deduped: true });
75
+ }
76
+ const at = nowMs(clock);
77
+ const runAt = request.runAt ?? at;
78
+ const record = {
79
+ id: uuid(),
80
+ name: request.name,
81
+ queue: request.queue || DEFAULT_QUEUE,
82
+ input: request.input,
83
+ idempotencyKey: request.idempotencyKey,
84
+ runId: request.runId ?? uuid(),
85
+ attempt: 0,
86
+ maxAttempts: request.maxAttempts,
87
+ state: runAt > at ? 'delayed' : 'ready',
88
+ runAt,
89
+ createdAt: at,
90
+ updatedAt: at,
91
+ ...(request.tenantId === undefined ? {} : { tenantId: request.tenantId }),
92
+ };
93
+ jobs.set(record.id, record);
94
+ return Promise.resolve({ id: record.id, runId: record.runId, deduped: false });
95
+ },
96
+ claim(claimOptions) {
97
+ const at = nowMs(clock);
98
+ const wanted = new Set(claimOptions.queues);
99
+ const claimable = [...jobs.values()]
100
+ .filter((record) => wanted.size === 0 || wanted.has(record.queue))
101
+ .filter((record) => {
102
+ if (record.runAt > at)
103
+ return false;
104
+ if (record.state === 'ready' || record.state === 'delayed')
105
+ return true;
106
+ if (record.state === 'suspended')
107
+ return true;
108
+ // Lease expiry: a worker that died without ack releases its job here.
109
+ return record.state === 'running' && (record.visibleAt ?? 0) <= at;
110
+ })
111
+ .sort((a, b) => a.runAt - b.runAt)
112
+ .slice(0, claimOptions.limit);
113
+ const out = [];
114
+ for (const record of claimable) {
115
+ const claimed = {
116
+ ...record,
117
+ state: 'running',
118
+ attempt: record.attempt + 1,
119
+ claimedBy: claimOptions.workerId,
120
+ claimedAt: at,
121
+ visibleAt: at + claimOptions.visibilityTimeoutMs,
122
+ updatedAt: at,
123
+ };
124
+ jobs.set(record.id, claimed);
125
+ out.push(claimed);
126
+ }
127
+ return Promise.resolve(out);
128
+ },
129
+ ack(jobId) {
130
+ update(jobId, { state: 'done' });
131
+ return Promise.resolve();
132
+ },
133
+ nack(jobId, nackOptions) {
134
+ const record = jobs.get(jobId);
135
+ if (record === undefined)
136
+ return Promise.resolve();
137
+ const at = nowMs(clock);
138
+ const counts = nackOptions.countsAsAttempt !== false;
139
+ const patch = {
140
+ state: nackOptions.deadLetter === true ? 'dead' : counts ? 'ready' : 'suspended',
141
+ runAt: at + nackOptions.delayMs,
142
+ // A suspension must not burn an attempt, or a 3-day sleep dead-letters the run.
143
+ attempt: counts ? record.attempt : record.attempt - 1,
144
+ ...(nackOptions.error === undefined ? {} : { lastError: nackOptions.error }),
145
+ };
146
+ update(jobId, patch);
147
+ return Promise.resolve();
148
+ },
149
+ heartbeat(jobId, heartbeatOptions) {
150
+ update(jobId, { visibleAt: nowMs(clock) + heartbeatOptions.visibilityTimeoutMs });
151
+ return Promise.resolve();
152
+ },
153
+ stats() {
154
+ const at = nowMs(clock);
155
+ const byQueue = new Map();
156
+ for (const record of jobs.values()) {
157
+ const current = byQueue.get(record.queue) ?? {
158
+ queue: record.queue,
159
+ ready: 0,
160
+ delayed: 0,
161
+ running: 0,
162
+ suspended: 0,
163
+ dead: 0,
164
+ oldestReadyMs: 0,
165
+ };
166
+ const next = { ...current };
167
+ if (record.state === 'ready' && record.runAt <= at) {
168
+ next.ready += 1;
169
+ next.oldestReadyMs = Math.max(next.oldestReadyMs, at - record.runAt);
170
+ }
171
+ else if (record.state === 'ready' || record.state === 'delayed')
172
+ next.delayed += 1;
173
+ else if (record.state === 'running')
174
+ next.running += 1;
175
+ else if (record.state === 'suspended')
176
+ next.suspended += 1;
177
+ else if (record.state === 'dead')
178
+ next.dead += 1;
179
+ byQueue.set(record.queue, next);
180
+ }
181
+ return Promise.resolve([...byQueue.values()].sort((a, b) => a.queue.localeCompare(b.queue)));
182
+ },
183
+ close() {
184
+ jobs.clear();
185
+ return Promise.resolve();
186
+ },
187
+ };
188
+ }
189
+ //# sourceMappingURL=driver-memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driver-memory.js","sourceRoot":"","sources":["driver-memory.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,2FAA2F;AAC3F,qEAAqE;AAGrE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAahC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAOhD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC;AAE1E,MAAM,UAAU,kBAAkB,CAAC,OAAO,GAAwB,EAAE;IAClE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,WAAW,CAAC;IAC3C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,qBAAqB,EAAE,CAAC;IACvD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAqB,CAAC;IAE1C,MAAM,SAAS,GAAG,CAAC,GAAW,EAAyB,EAAE;QACvD,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YACnC,IAAI,MAAM,CAAC,cAAc,KAAK,GAAG,IAAI,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC;gBAAE,OAAO,MAAM,CAAC;QACpF,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,CAAC,EAAU,EAAE,KAAyB,EAAQ,EAAE;QAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9B,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO;QACnC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,GAAG,QAAQ,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACnE,CAAC,CAAC;IAEF,MAAM,UAAU,GAAqB;QACnC,GAAG,CAAC,KAAK;YACP,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC,MAAM,GAAc,EAAE;YACzB,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;iBAC5B,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC;iBAC/E,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC;iBAC5E,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC;iBAC/E,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC;iBACzC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC;YACjC,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC/B,CAAC;QACD,WAAW,CAAC,KAAK,GAAG,GAAG;YACrB,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;iBAC5B,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC;iBAC3C,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC;iBACzC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;YACnB,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC/B,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,cAAc;YACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACjC,MAAM,CACJ,QAAQ,KAAK,SAAS,EACtB,UAAU,KAAK,uBAAuB,EACtC,mJAAmJ,CACpJ,CAAC;YACF,MAAM,MAAM,GAAG,QAAQ,CAAC;YACxB,IAAI,cAAc,EAAE,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3C,uEAAuE;gBACvE,MAAM,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;YACzD,CAAC;YACD,MAAM,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACnE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC7B,OAAO,IAAI,IAAI,MAAM,CAAC;QACxB,CAAC;KACF,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,KAAK;QACL,UAAU;QAEV,OAAO,CAAC,OAAuB;YAC7B,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;YACnD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,IAAI,OAAO,CAAC,UAAU,KAAK,OAAO,EAAE,CAAC;oBACnC,MAAM,IAAI,iBAAiB,CAAC;wBAC1B,GAAG,EAAE,OAAO,CAAC,IAAI;wBACjB,cAAc,EAAE,OAAO,CAAC,cAAc;wBACtC,UAAU,EAAE,QAAQ,CAAC,EAAE;qBACxB,CAAC,CAAC;gBACL,CAAC;gBACD,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACpF,CAAC;YAED,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;YACxB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;YAClC,MAAM,MAAM,GAAc;gBACxB,EAAE,EAAE,IAAI,EAAE;gBACV,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,aAAa;gBACrC,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,cAAc,EAAE,OAAO,CAAC,cAAc;gBACtC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,IAAI,EAAE;gBAC9B,OAAO,EAAE,CAAC;gBACV,WAAW,EAAE,OAAO,CAAC,WAAW;gBAChC,KAAK,EAAE,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO;gBACvC,KAAK;gBACL,SAAS,EAAE,EAAE;gBACb,SAAS,EAAE,EAAE;gBACb,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;aAC1E,CAAC;YACF,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;YAC5B,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACjF,CAAC;QAED,KAAK,CAAC,YAA0B;YAC9B,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;YACxB,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YAC5C,MAAM,SAAS,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;iBACjC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;iBACjE,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE;gBACjB,IAAI,MAAM,CAAC,KAAK,GAAG,EAAE;oBAAE,OAAO,KAAK,CAAC;gBACpC,IAAI,MAAM,CAAC,KAAK,KAAK,OAAO,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;oBAAE,OAAO,IAAI,CAAC;gBACxE,IAAI,MAAM,CAAC,KAAK,KAAK,WAAW;oBAAE,OAAO,IAAI,CAAC;gBAC9C,sEAAsE;gBACtE,OAAO,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;YACrE,CAAC,CAAC;iBACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;iBACjC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;YAEhC,MAAM,GAAG,GAAiB,EAAE,CAAC;YAC7B,KAAK,MAAM,MAAM,IAAI,SAAS,EAAE,CAAC;gBAC/B,MAAM,OAAO,GAAe;oBAC1B,GAAG,MAAM;oBACT,KAAK,EAAE,SAAS;oBAChB,OAAO,EAAE,MAAM,CAAC,OAAO,GAAG,CAAC;oBAC3B,SAAS,EAAE,YAAY,CAAC,QAAQ;oBAChC,SAAS,EAAE,EAAE;oBACb,SAAS,EAAE,EAAE,GAAG,YAAY,CAAC,mBAAmB;oBAChD,SAAS,EAAE,EAAE;iBACd,CAAC;gBACF,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;gBAC7B,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACpB,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;QAED,GAAG,CAAC,KAAa;YACf,MAAM,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;YACjC,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC;QAED,IAAI,CAAC,KAAa,EAAE,WAAwB;YAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC/B,IAAI,MAAM,KAAK,SAAS;gBAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;YACnD,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;YACxB,MAAM,MAAM,GAAG,WAAW,CAAC,eAAe,KAAK,KAAK,CAAC;YACrD,MAAM,KAAK,GAAuB;gBAChC,KAAK,EAAE,WAAW,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW;gBAChF,KAAK,EAAE,EAAE,GAAG,WAAW,CAAC,OAAO;gBAC/B,gFAAgF;gBAChF,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,GAAG,CAAC;gBACrD,GAAG,CAAC,WAAW,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,CAAC,KAAK,EAAE,CAAC;aAC7E,CAAC;YACF,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACrB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC;QAED,SAAS,CAAC,KAAa,EAAE,gBAAgB;YACvC,MAAM,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,gBAAgB,CAAC,mBAAmB,EAAE,CAAC,CAAC;YAClF,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC;QAED,KAAK;YACH,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;YACxB,MAAM,OAAO,GAAG,IAAI,GAAG,EAAsB,CAAC;YAC9C,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;gBACnC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI;oBAC3C,KAAK,EAAE,MAAM,CAAC,KAAK;oBACnB,KAAK,EAAE,CAAC;oBACR,OAAO,EAAE,CAAC;oBACV,OAAO,EAAE,CAAC;oBACV,SAAS,EAAE,CAAC;oBACZ,IAAI,EAAE,CAAC;oBACP,aAAa,EAAE,CAAC;iBACjB,CAAC;gBACF,MAAM,IAAI,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC;gBAC5B,IAAI,MAAM,CAAC,KAAK,KAAK,OAAO,IAAI,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;oBACnD,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;oBAChB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;gBACvE,CAAC;qBAAM,IAAI,MAAM,CAAC,KAAK,KAAK,OAAO,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;oBAAE,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;qBAChF,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;oBAAE,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;qBAClD,IAAI,MAAM,CAAC,KAAK,KAAK,WAAW;oBAAE,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;qBACtD,IAAI,MAAM,CAAC,KAAK,KAAK,MAAM;oBAAE,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC;gBACjD,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAClC,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC/F,CAAC;QAED,KAAK;YACH,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC;KACF,CAAC;AACJ,CAAC"}