effect-mq 0.1.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 +21 -0
- package/README.md +235 -0
- package/dist/Job.d.ts +222 -0
- package/dist/Job.d.ts.map +1 -0
- package/dist/Job.js +218 -0
- package/dist/Job.js.map +1 -0
- package/dist/JobStore.d.ts +401 -0
- package/dist/JobStore.d.ts.map +1 -0
- package/dist/JobStore.js +89 -0
- package/dist/JobStore.js.map +1 -0
- package/dist/MemoryJobStore.d.ts +34 -0
- package/dist/MemoryJobStore.d.ts.map +1 -0
- package/dist/MemoryJobStore.js +381 -0
- package/dist/MemoryJobStore.js.map +1 -0
- package/dist/Worker.d.ts +127 -0
- package/dist/Worker.d.ts.map +1 -0
- package/dist/Worker.js +274 -0
- package/dist/Worker.js.map +1 -0
- package/dist/drizzle/DrizzleJobStore.d.ts +59 -0
- package/dist/drizzle/DrizzleJobStore.d.ts.map +1 -0
- package/dist/drizzle/DrizzleJobStore.js +426 -0
- package/dist/drizzle/DrizzleJobStore.js.map +1 -0
- package/dist/drizzle/index.d.ts +19 -0
- package/dist/drizzle/index.d.ts.map +1 -0
- package/dist/drizzle/index.js +19 -0
- package/dist/drizzle/index.js.map +1 -0
- package/dist/drizzle/schema.d.ts +464 -0
- package/dist/drizzle/schema.d.ts.map +1 -0
- package/dist/drizzle/schema.js +68 -0
- package/dist/drizzle/schema.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/testing/conformance.d.ts +27 -0
- package/dist/testing/conformance.d.ts.map +1 -0
- package/dist/testing/conformance.js +451 -0
- package/dist/testing/conformance.js.map +1 -0
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +8 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +71 -0
- package/src/Job.ts +606 -0
- package/src/JobStore.ts +446 -0
- package/src/MemoryJobStore.ts +467 -0
- package/src/Worker.ts +514 -0
- package/src/drizzle/DrizzleJobStore.ts +599 -0
- package/src/drizzle/index.ts +20 -0
- package/src/drizzle/schema.ts +116 -0
- package/src/index.ts +33 -0
- package/src/testing/conformance.ts +654 -0
- package/src/testing/index.ts +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam Rankin
|
|
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,235 @@
|
|
|
1
|
+
# effect-mq
|
|
2
|
+
|
|
3
|
+
Effect-native background jobs. Schema-first job definitions, a storage-agnostic
|
|
4
|
+
queue core, a worker runtime, and a Postgres store that lives inside your
|
|
5
|
+
drizzle schema — inspired by BullMQ's semantics and `effect/workflow`'s DX.
|
|
6
|
+
Built on Effect v4.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
bun add effect-mq effect # or npm / pnpm / yarn
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
One package, tree-shakeable modules:
|
|
13
|
+
|
|
14
|
+
| Import | Contents | Extra peers |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `effect-mq` | `Job`, `JobStore`, `MemoryJobStore`, `Worker` | — |
|
|
17
|
+
| `effect-mq/drizzle` | drizzle schema factories + the Postgres `JobStore` | `drizzle-orm` (v1), `@effect/sql-pg` |
|
|
18
|
+
| `effect-mq/testing` | the `JobStore` conformance suite for driver authors | `@effect/vitest` |
|
|
19
|
+
|
|
20
|
+
## Five-minute tour
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Job, MemoryJobStore, Worker } from "effect-mq"
|
|
24
|
+
import { Effect, Layer, Schema } from "effect"
|
|
25
|
+
|
|
26
|
+
// 1. Define a job — shared by producers and runners.
|
|
27
|
+
class SendEmail extends Job.make("SendEmail", {
|
|
28
|
+
payload: { to: Schema.String, subject: Schema.String },
|
|
29
|
+
success: Schema.String,
|
|
30
|
+
idempotencyKey: ({ to, subject }) => `${to}:${subject}`,
|
|
31
|
+
metadata: ({ to }) => ({ to }), // indexed, queryable
|
|
32
|
+
queue: "email",
|
|
33
|
+
defaults: { attempts: 3, backoff: { type: "exponential", delay: "1 second" } }
|
|
34
|
+
}) {}
|
|
35
|
+
|
|
36
|
+
// 2. Produce. Only the store is required — no worker anywhere in sight.
|
|
37
|
+
const producer = Effect.gen(function*() {
|
|
38
|
+
const jobId = yield* SendEmail.enqueue({ to: "ada@example.com", subject: "hi" })
|
|
39
|
+
// ^ JobId — deterministic here, thanks to idempotencyKey
|
|
40
|
+
|
|
41
|
+
// ...or enqueue-and-wait for the typed result:
|
|
42
|
+
const messageId = yield* SendEmail.execute(
|
|
43
|
+
{ to: "grace@example.com", subject: "now" },
|
|
44
|
+
{ delay: "5 seconds", priority: 2 }
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// 3. Run. Workers are layers — deploy them in the same process or across
|
|
49
|
+
// machines against shared storage.
|
|
50
|
+
const RunnerLive = SendEmail.toLayer(
|
|
51
|
+
(payload, ctx) => Effect.succeed(`message-${ctx.jobId}`),
|
|
52
|
+
{ concurrency: 5 }
|
|
53
|
+
).pipe(
|
|
54
|
+
Layer.provideMerge(Worker.layer()),
|
|
55
|
+
Layer.provideMerge(MemoryJobStore.layer) // swap for Postgres below
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
What you get out of the box:
|
|
60
|
+
|
|
61
|
+
- **At-least-once execution** — token-guarded locks, heartbeat renewal, and a
|
|
62
|
+
stalled-job sweeper that recovers work from crashed workers.
|
|
63
|
+
- **Durable retries** — a failed attempt is written back to the store and
|
|
64
|
+
re-claimed after its backoff by *any* worker; no lock or worker slot is held
|
|
65
|
+
while waiting.
|
|
66
|
+
- **A full run ledger** — every run (success, retry, failure, stall) persists
|
|
67
|
+
as an `AttemptRecord`; `Job.attempts(id)` decodes them back to typed exits.
|
|
68
|
+
- **Idempotency** — `idempotencyKey` makes enqueue a no-op while a job with
|
|
69
|
+
that key exists, and makes the job id *computable from business data* — the
|
|
70
|
+
natural join key between the queue and your own domain tables.
|
|
71
|
+
- **A dashboard data layer** — `store.list({ name, states, metadata, cursor })`,
|
|
72
|
+
`Job.poll`, `Job.retry(id)` (failed → fresh attempt budget, ledger intact),
|
|
73
|
+
and per-job retention via `keep: { count, age }`.
|
|
74
|
+
- **Graceful shutdown** — interrupting a worker releases in-flight jobs back
|
|
75
|
+
to `waiting` without consuming an attempt.
|
|
76
|
+
|
|
77
|
+
## Postgres through drizzle
|
|
78
|
+
|
|
79
|
+
The Postgres store runs on drizzle v1's Effect driver
|
|
80
|
+
(`drizzle-orm/effect-postgres`, built on `@effect/sql-pg` — works on Node and
|
|
81
|
+
Bun). Claims use `FOR UPDATE SKIP LOCKED`; wake-ups use LISTEN/NOTIFY, so
|
|
82
|
+
workers in other processes pick jobs up promptly.
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
bun add drizzle-orm@rc @effect/sql-pg
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 1. Put the tables in your drizzle schema
|
|
89
|
+
|
|
90
|
+
The factories are the single source of truth for the table layout. Re-export
|
|
91
|
+
them from your schema file and **your drizzle-kit pipeline owns the
|
|
92
|
+
migrations** — no library-run DDL, no parallel migration system:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// db/schema.ts
|
|
96
|
+
import { mqJobAttempts, mqJobs } from "effect-mq/drizzle"
|
|
97
|
+
|
|
98
|
+
// The `name` column is typed to your job tags (derived, not hand-written):
|
|
99
|
+
type JobNames = typeof SyncBenefits._tag | typeof GenerateReport._tag
|
|
100
|
+
|
|
101
|
+
export const jobs = mqJobs<JobNames>() // default table: effect_mq_jobs
|
|
102
|
+
export const jobAttempts = mqJobAttempts(jobs) // default: effect_mq_job_attempts
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
drizzle-kit generate # emits the CREATE TABLE migration next to your others
|
|
107
|
+
drizzle-kit migrate
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
When a future effect-mq version changes the layout, the factory changes and
|
|
111
|
+
`drizzle-kit generate` diffs it — you get a normal, reviewable migration.
|
|
112
|
+
|
|
113
|
+
### 2. Provide the store layer
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { DrizzleJobStore } from "effect-mq/drizzle"
|
|
117
|
+
import { PgClient } from "@effect/sql-pg"
|
|
118
|
+
import { Layer, Redacted } from "effect"
|
|
119
|
+
import { jobAttempts, jobs } from "./db/schema.ts"
|
|
120
|
+
|
|
121
|
+
const JobStoreLive = DrizzleJobStore.layer({ jobs, attempts: jobAttempts }).pipe(
|
|
122
|
+
Layer.provide(PgClient.layer({ url: Redacted.make(process.env.DATABASE_URL!) }))
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The layer probes the tables at startup and fails fast with a clear message if
|
|
127
|
+
migrations haven't run (`validate: false` defers that to first use).
|
|
128
|
+
|
|
129
|
+
### 3. Query it like any other table
|
|
130
|
+
|
|
131
|
+
Product UIs read the tables directly with drizzle — fully typed:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
db.select().from(jobs).where(and(
|
|
135
|
+
eq(jobs.name, "sync-benefits"), // a typo is a compile error
|
|
136
|
+
sql`${jobs.metadata} @> ${{ employerId }}::jsonb` // GIN-indexed containment
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
db.select().from(jobAttempts)
|
|
140
|
+
.innerJoin(jobs, eq(jobAttempts.jobId, jobs.id))
|
|
141
|
+
.where(eq(jobAttempts.outcome, "failed"))
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Reads yes, writes no.** Mutations must go through the store —
|
|
145
|
+
`MyJob.retry(id)`, `store.remove(id)` — so lock tokens, attempt accounting,
|
|
146
|
+
and wake-up notifications stay coherent.
|
|
147
|
+
|
|
148
|
+
Worker tip: `awaitWake` is LISTEN/NOTIFY-driven, and the worker's
|
|
149
|
+
`pollInterval` is the fallback — `Worker.layer({ pollInterval: "500 millis" })`
|
|
150
|
+
is a good Postgres setting.
|
|
151
|
+
|
|
152
|
+
## Multiple stores on different infrastructure
|
|
153
|
+
|
|
154
|
+
Bind jobs to *named stores* so business-critical runs live in Postgres while
|
|
155
|
+
disposable ones live elsewhere — enforced by the type system:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { Job, JobStore, MemoryJobStore, Worker } from "effect-mq"
|
|
159
|
+
|
|
160
|
+
const Durable = JobStore.named("durable") // -> Postgres in prod
|
|
161
|
+
const Ephemeral = JobStore.named("ephemeral") // -> memory/Redis
|
|
162
|
+
|
|
163
|
+
class SyncBenefits extends Job.make("sync-benefits", {
|
|
164
|
+
payload: { employerId: Schema.String },
|
|
165
|
+
idempotencyKey: ({ employerId }) => employerId,
|
|
166
|
+
store: Durable
|
|
167
|
+
}) {}
|
|
168
|
+
|
|
169
|
+
// Forgetting the Durable layer is now a COMPILE error at every enqueue site.
|
|
170
|
+
// Workers bind to one store:
|
|
171
|
+
const durableWorkers = SyncBenefits.toLayer(handler).pipe(
|
|
172
|
+
Layer.provide(Worker.layer({ store: Durable })) // local provide: several
|
|
173
|
+
) // workers can coexist
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A **queue** is an ordering/concurrency domain *within* a store
|
|
177
|
+
(`Job.make({ queue })`, `Worker.layer({ queues: { email: { concurrency: 5 } } })`);
|
|
178
|
+
a **store** is an infrastructure/durability domain hosting many queues.
|
|
179
|
+
|
|
180
|
+
## Metadata vs. your own tables
|
|
181
|
+
|
|
182
|
+
Two kinds of "business context", two homes:
|
|
183
|
+
|
|
184
|
+
- **Ops UI** ("list sync runs for employer X, retry that one"): use the
|
|
185
|
+
`metadata` projection — a flat `Record<string, string>` derived from the
|
|
186
|
+
payload, indexed by every driver, filterable via `store.list` or raw SQL.
|
|
187
|
+
- **Domain history** ("what did this sync actually change"): your own table,
|
|
188
|
+
joined by the *deterministic* job id from `idempotencyKey`. The queue's
|
|
189
|
+
retention (`keep`) can then prune freely while your business history lives
|
|
190
|
+
forever. Don't let queue infrastructure own business data lifecycles.
|
|
191
|
+
|
|
192
|
+
## Secrets and redaction
|
|
193
|
+
|
|
194
|
+
Payloads and results are persisted **schema-encoded** — nothing reaches a
|
|
195
|
+
store un-encoded. For `Schema.Redacted` fields, Effect's semantics apply:
|
|
196
|
+
|
|
197
|
+
- `Schema.Redacted(Schema.String)` **round-trips**: handlers receive a real
|
|
198
|
+
`Redacted` value (safe to log — it prints `<redacted>`), but the underlying
|
|
199
|
+
value *is* stored in the payload/exit JSON. Redaction protects logs and
|
|
200
|
+
inspection, not the database at rest.
|
|
201
|
+
- `Schema.Redacted(inner, { disallowJsonEncode: true })` **refuses
|
|
202
|
+
persistence**: enqueueing such a payload dies with `Cannot serialize
|
|
203
|
+
Redacted` before anything reaches the store. Use it for values that must
|
|
204
|
+
never be written down; pass them to handlers via context/services instead.
|
|
205
|
+
|
|
206
|
+
Both behaviors are pinned by tests.
|
|
207
|
+
|
|
208
|
+
## Writing a storage driver
|
|
209
|
+
|
|
210
|
+
Implement the `JobStore` service (one atomic seam: `enqueue`, `claim`, `ack`,
|
|
211
|
+
`release`, `extendLocks`, `recoverStalled`, `awaitWake`, `getJob`,
|
|
212
|
+
`getAttempts`, `list`, `retry`, `counts`, `remove`) and run the conformance
|
|
213
|
+
suite against it:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { jobStoreConformance } from "effect-mq/testing"
|
|
217
|
+
|
|
218
|
+
jobStoreConformance("MyDriver", () => MyDriver.layer)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Two rules make the suite work against real storage: derive **all time from
|
|
222
|
+
the Effect `Clock`** (pass `now` as a bind parameter — never SQL `now()`), so
|
|
223
|
+
tests run under `TestClock`; and keep every operation atomic. The in-memory
|
|
224
|
+
driver (`MemoryJobStore`) is the reference implementation, and the Postgres
|
|
225
|
+
suite in this repo runs the same conformance tests against a real database.
|
|
226
|
+
|
|
227
|
+
## Roadmap
|
|
228
|
+
|
|
229
|
+
- `effect-mq/redis` (Bun.redis / ioredis-free) behind the same conformance suite
|
|
230
|
+
- A standalone non-drizzle Postgres driver on plain `@effect/sql-pg`
|
|
231
|
+
- Repeatable/cron jobs, rate limiting, a reference dashboard
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
package/dist/Job.d.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-first background job definitions.
|
|
3
|
+
*
|
|
4
|
+
* A `Job` is defined once (name, payload/success/error schemas, defaults) and
|
|
5
|
+
* used from both sides:
|
|
6
|
+
*
|
|
7
|
+
* - producers call `MyJob.enqueue(payload, options)` (requires the job's store)
|
|
8
|
+
* - runners provide `MyJob.toLayer(handler)` on top of a `Worker.layer`
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { Job, JobStore } from "effect-mq"
|
|
12
|
+
* import { Effect, Schema } from "effect"
|
|
13
|
+
*
|
|
14
|
+
* const Durable = JobStore.named("durable")
|
|
15
|
+
*
|
|
16
|
+
* class SendEmail extends Job.make("SendEmail", {
|
|
17
|
+
* payload: { to: Schema.String, body: Schema.String },
|
|
18
|
+
* queue: "email",
|
|
19
|
+
* store: Durable,
|
|
20
|
+
* metadata: ({ to }) => ({ to }),
|
|
21
|
+
* defaults: { attempts: 5, backoff: { type: "exponential", delay: "1 second" } }
|
|
22
|
+
* }) {}
|
|
23
|
+
*
|
|
24
|
+
* // producer — requires the Durable store in context, enforced at compile time
|
|
25
|
+
* const jobId = yield* SendEmail.enqueue({ to: "a@b.c", body: "hi" }, { delay: "5 seconds" })
|
|
26
|
+
*
|
|
27
|
+
* // runner
|
|
28
|
+
* const SendEmailWorker = SendEmail.toLayer((payload) => Effect.log(`sending to ${payload.to}`))
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @since 0.1.0
|
|
32
|
+
*/
|
|
33
|
+
import { type Context, Duration, Effect, type Exit, Layer, Option, Schedule, Schema } from "effect";
|
|
34
|
+
import { type BackoffPolicy, JobId, JobNotFoundError, type JobNotRetryableError, type JobState, JobStore, type KeepPolicy, QueueName, type Service as StoreService } from "./JobStore.ts";
|
|
35
|
+
import { type JobContext, type RegisterOptions, Worker } from "./Worker.ts";
|
|
36
|
+
declare const TypeId: "~effect-mq/Job";
|
|
37
|
+
/**
|
|
38
|
+
* A struct schema (or anything with struct fields).
|
|
39
|
+
*
|
|
40
|
+
* @since 0.1.0
|
|
41
|
+
*/
|
|
42
|
+
export interface AnyStructSchema extends Schema.Top {
|
|
43
|
+
readonly fields: Schema.Struct.Fields;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* User-facing backoff configuration.
|
|
47
|
+
*
|
|
48
|
+
* @since 0.1.0
|
|
49
|
+
*/
|
|
50
|
+
export interface BackoffInput {
|
|
51
|
+
readonly type: "fixed" | "exponential";
|
|
52
|
+
readonly delay: Duration.Input;
|
|
53
|
+
/** Exponential growth factor (default 2). */
|
|
54
|
+
readonly factor?: number | undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* User-facing retention configuration for terminal jobs.
|
|
58
|
+
*
|
|
59
|
+
* @since 0.1.0
|
|
60
|
+
*/
|
|
61
|
+
export interface KeepInput {
|
|
62
|
+
/** Keep at most this many terminal records (per name + state). */
|
|
63
|
+
readonly count?: number | undefined;
|
|
64
|
+
/** Remove terminal records older than this. */
|
|
65
|
+
readonly age?: Duration.Input | undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Options shared between job defaults and per-enqueue overrides.
|
|
69
|
+
*
|
|
70
|
+
* @since 0.1.0
|
|
71
|
+
*/
|
|
72
|
+
export interface JobOptions {
|
|
73
|
+
/** Do not run before this long from enqueue time. */
|
|
74
|
+
readonly delay?: Duration.Input | undefined;
|
|
75
|
+
/** Higher runs first; ties are FIFO. Default 0. */
|
|
76
|
+
readonly priority?: number | undefined;
|
|
77
|
+
/** Total attempts including the first run. Default 1 (no retries). */
|
|
78
|
+
readonly attempts?: number | undefined;
|
|
79
|
+
readonly backoff?: BackoffInput | undefined;
|
|
80
|
+
/** Retention for terminal records. Default: keep forever. */
|
|
81
|
+
readonly keep?: KeepInput | undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* @since 0.1.0
|
|
85
|
+
*/
|
|
86
|
+
export interface EnqueueOptions extends JobOptions {
|
|
87
|
+
/**
|
|
88
|
+
* Explicit job id. Enqueueing an id that already exists is a no-op that
|
|
89
|
+
* returns the existing id (idempotency). Overrides the definition's
|
|
90
|
+
* `idempotencyKey`.
|
|
91
|
+
*/
|
|
92
|
+
readonly jobId?: string | undefined;
|
|
93
|
+
/** Send to a different queue than the definition's. */
|
|
94
|
+
readonly queue?: string | undefined;
|
|
95
|
+
/** Queryable business context, merged over the definition's `metadata`. */
|
|
96
|
+
readonly metadata?: Readonly<Record<string, string>> | undefined;
|
|
97
|
+
}
|
|
98
|
+
interface ResolvedDefaults {
|
|
99
|
+
readonly delayMs: number;
|
|
100
|
+
readonly priority: number;
|
|
101
|
+
readonly attempts: number;
|
|
102
|
+
readonly backoff: BackoffPolicy | undefined;
|
|
103
|
+
readonly keep: KeepPolicy | undefined;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The status of a job as seen by `poll`. Fetch the full run ledger with
|
|
107
|
+
* `Job.attempts`.
|
|
108
|
+
*
|
|
109
|
+
* @since 0.1.0
|
|
110
|
+
*/
|
|
111
|
+
export interface JobStatus<A, E> {
|
|
112
|
+
readonly state: JobState;
|
|
113
|
+
readonly attemptsMade: number;
|
|
114
|
+
readonly metadata: Readonly<Record<string, string>>;
|
|
115
|
+
/** Present for completed/failed jobs (except store-side failures like stalling). */
|
|
116
|
+
readonly exit: Option.Option<Exit.Exit<A, E>>;
|
|
117
|
+
readonly failedReason: string | undefined;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* One decoded entry of a job's run ledger.
|
|
121
|
+
*
|
|
122
|
+
* @since 0.1.0
|
|
123
|
+
*/
|
|
124
|
+
export interface JobAttempt<A, E> {
|
|
125
|
+
readonly attempt: number;
|
|
126
|
+
readonly startedAt: number | undefined;
|
|
127
|
+
readonly finishedAt: number;
|
|
128
|
+
readonly outcome: "completed" | "retried" | "failed" | "stalled";
|
|
129
|
+
/** Absent for `stalled` entries. */
|
|
130
|
+
readonly exit: Option.Option<Exit.Exit<A, E>>;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* @since 0.1.0
|
|
134
|
+
*/
|
|
135
|
+
export interface Job<Name extends string, Payload extends AnyStructSchema, Success extends Schema.Top, Error extends Schema.Top, StoreId = JobStore> {
|
|
136
|
+
new (_: never): {};
|
|
137
|
+
readonly [TypeId]: typeof TypeId;
|
|
138
|
+
/**
|
|
139
|
+
* The job's unique name. (Named `_tag` rather than `name` because a
|
|
140
|
+
* `class X extends Job.make(...) {}` subclass shadows `Function.name`.)
|
|
141
|
+
*/
|
|
142
|
+
readonly _tag: Name;
|
|
143
|
+
readonly queue: QueueName;
|
|
144
|
+
/** The store this job's runs live on. */
|
|
145
|
+
readonly store: Context.Key<StoreId, StoreService>;
|
|
146
|
+
readonly payloadSchema: Payload;
|
|
147
|
+
readonly successSchema: Success;
|
|
148
|
+
readonly errorSchema: Error;
|
|
149
|
+
/** JSON codec for the payload — what is actually persisted. */
|
|
150
|
+
readonly payloadJsonSchema: Schema.toCodecJson<Payload>;
|
|
151
|
+
/** JSON codec for handler exits — what is actually persisted. */
|
|
152
|
+
readonly exitSchema: Schema.toCodecJson<Schema.Exit<Schema.toCodecJson<Success>, Schema.toCodecJson<Error>, Schema.Defect>>;
|
|
153
|
+
readonly idempotencyKey: ((payload: Payload["Type"]) => string) | undefined;
|
|
154
|
+
readonly metadata: ((payload: Payload["Type"]) => Readonly<Record<string, string>>) | undefined;
|
|
155
|
+
readonly defaults: ResolvedDefaults;
|
|
156
|
+
/**
|
|
157
|
+
* Queue this job. Returns the job id. Duplicate ids (via `jobId` or
|
|
158
|
+
* `idempotencyKey`) are a silent no-op returning the existing id.
|
|
159
|
+
*/
|
|
160
|
+
readonly enqueue: (payload: Payload["~type.make.in"], options?: EnqueueOptions | undefined) => Effect.Effect<JobId, never, StoreId | Payload["EncodingServices"]>;
|
|
161
|
+
/** Read the current status of a previously enqueued job. */
|
|
162
|
+
readonly poll: (jobId: JobId) => Effect.Effect<Option.Option<JobStatus<Success["Type"], Error["Type"]>>, never, StoreId | Success["DecodingServices"] | Error["DecodingServices"]>;
|
|
163
|
+
/** The job's decoded run ledger, oldest first. */
|
|
164
|
+
readonly attempts: (jobId: JobId) => Effect.Effect<ReadonlyArray<JobAttempt<Success["Type"], Error["Type"]>>, never, StoreId | Success["DecodingServices"] | Error["DecodingServices"]>;
|
|
165
|
+
/**
|
|
166
|
+
* Wait (by polling) until the job finishes, then return its result. Dies
|
|
167
|
+
* if the job id does not exist or the job was failed by the store itself.
|
|
168
|
+
*/
|
|
169
|
+
readonly awaitResult: (jobId: JobId, options?: {
|
|
170
|
+
readonly pollSchedule?: Schedule.Schedule<unknown> | undefined;
|
|
171
|
+
} | undefined) => Effect.Effect<Success["Type"], Error["Type"], StoreId | Success["DecodingServices"] | Error["DecodingServices"]>;
|
|
172
|
+
/** `enqueue` + `awaitResult` in one call. */
|
|
173
|
+
readonly execute: (payload: Payload["~type.make.in"], options?: EnqueueOptions | undefined) => Effect.Effect<Success["Type"], Error["Type"], StoreId | Payload["EncodingServices"] | Success["DecodingServices"] | Error["DecodingServices"]>;
|
|
174
|
+
/**
|
|
175
|
+
* Re-run a failed job with a fresh attempt budget. The run ledger is
|
|
176
|
+
* preserved. (The admin op behind a dashboard's "retry" button.)
|
|
177
|
+
*/
|
|
178
|
+
readonly retry: (jobId: JobId) => Effect.Effect<void, JobNotFoundError | JobNotRetryableError, StoreId>;
|
|
179
|
+
/**
|
|
180
|
+
* Attach the handler that processes this job, as a layer to provide on top
|
|
181
|
+
* of `Worker.layer` (bound to the same store).
|
|
182
|
+
*/
|
|
183
|
+
readonly toLayer: <R>(handler: (payload: Payload["Type"], context: JobContext) => Effect.Effect<Success["Type"], Error["Type"], R>, options?: RegisterOptions | undefined) => Layer.Layer<never, never, Worker | R | Payload["DecodingServices"] | Success["EncodingServices"] | Error["EncodingServices"]>;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* @since 0.1.0
|
|
187
|
+
*/
|
|
188
|
+
export interface Any {
|
|
189
|
+
readonly [TypeId]: typeof TypeId;
|
|
190
|
+
readonly _tag: string;
|
|
191
|
+
readonly queue: QueueName;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Define a job.
|
|
195
|
+
*
|
|
196
|
+
* @since 0.1.0
|
|
197
|
+
*/
|
|
198
|
+
export declare const make: <const Name extends string, Payload extends Schema.Struct.Fields | AnyStructSchema, Success extends Schema.Top = Schema.Void, Error extends Schema.Top = Schema.Never, StoreId = JobStore>(name: Name, options: {
|
|
199
|
+
readonly payload: Payload;
|
|
200
|
+
readonly success?: Success | undefined;
|
|
201
|
+
readonly error?: Error | undefined;
|
|
202
|
+
/**
|
|
203
|
+
* Derive a stable job id from the payload. Enqueueing the same key twice
|
|
204
|
+
* while the first job still exists is a no-op (returns the existing id).
|
|
205
|
+
*/
|
|
206
|
+
readonly idempotencyKey?: ((payload: Payload extends Schema.Struct.Fields ? Schema.Struct.Type<Payload> : Payload["Type"]) => string) | undefined;
|
|
207
|
+
/**
|
|
208
|
+
* Derive queryable business context from the payload (flat string map, so
|
|
209
|
+
* every driver can index it). Merged with per-enqueue `metadata`.
|
|
210
|
+
*/
|
|
211
|
+
readonly metadata?: ((payload: Payload extends Schema.Struct.Fields ? Schema.Struct.Type<Payload> : Payload["Type"]) => Readonly<Record<string, string>>) | undefined;
|
|
212
|
+
/** The queue this job runs on. Default `"default"`. */
|
|
213
|
+
readonly queue?: string | undefined;
|
|
214
|
+
/**
|
|
215
|
+
* The store this job's runs live on (a `JobStore.named(...)` key).
|
|
216
|
+
* Default: the default `JobStore`.
|
|
217
|
+
*/
|
|
218
|
+
readonly store?: Context.Key<StoreId, StoreService> | undefined;
|
|
219
|
+
readonly defaults?: JobOptions | undefined;
|
|
220
|
+
}) => Job<Name, Payload extends Schema.Struct.Fields ? Schema.Struct<Payload> : Payload, Success, Error, StoreId>;
|
|
221
|
+
export {};
|
|
222
|
+
//# sourceMappingURL=Job.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Job.d.ts","sourceRoot":"","sources":["../src/Job.ts"],"names":[],"mappings":"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"}
|