@ultimat3/jobs 18.0.0 → 19.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/README.md +62 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -42,6 +42,68 @@ nightlyDigest.describe(); // { kind: 'task', cron, tz, j
|
|
|
42
42
|
| `enqueue(options?)` | `TaskHandle` | fires every declared entry now, one result per entry |
|
|
43
43
|
| `describe()` | `TaskHandle` | `TaskDescriptor` — cron, tz, catch-up, jobs in declaration order |
|
|
44
44
|
|
|
45
|
+
## Fan-out per row is a job, not a task
|
|
46
|
+
|
|
47
|
+
A task's `enqueue` is **synchronous by design** — `(occurrenceMs) => [[job, input], …]` — and it
|
|
48
|
+
stays that way, `As of 2026-09-05`, for two reasons that are the same reason. `describe()` reads
|
|
49
|
+
`entries()` to project the task's job names into `x.manifest.json`, `/_x` and `x tasks show`, so an
|
|
50
|
+
`enqueue` that awaited a database would make the manifest an I/O operation; and a task that reads
|
|
51
|
+
the hosts table inside its tick is a task doing work, which the primitive's own header rules out.
|
|
52
|
+
So "one job per row" cannot be written in the task. It is written as **one fan-out job**:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { job, t, task } from '@ultimat3/jobs';
|
|
56
|
+
|
|
57
|
+
declare const listHosts: () => Promise<readonly { readonly id: string }[]>;
|
|
58
|
+
declare const probe: (hostId: string) => Promise<void>;
|
|
59
|
+
|
|
60
|
+
// jobs/poll-host.ts — the unit of retry: one row
|
|
61
|
+
export const pollHost = job({
|
|
62
|
+
input: t.object({ hostId: t.uuid, occurrenceMs: t.number.int() }),
|
|
63
|
+
idempotencyKey: ({ hostId, occurrenceMs }) => `poll-host:${hostId}:${occurrenceMs}`,
|
|
64
|
+
tenant: 'none',
|
|
65
|
+
retry: { attempts: 3, backoff: 'exponential' },
|
|
66
|
+
async run({ input }) {
|
|
67
|
+
await probe(input.hostId);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// jobs/poll-hosts.ts — the fan-out: list the rows, enqueue one child per row
|
|
72
|
+
export const pollHosts = job({
|
|
73
|
+
input: t.object({ occurrenceMs: t.number.int() }),
|
|
74
|
+
idempotencyKey: ({ occurrenceMs }) => `poll-hosts:${occurrenceMs}`,
|
|
75
|
+
tenant: 'none',
|
|
76
|
+
retry: { attempts: 3, backoff: 'exponential' },
|
|
77
|
+
async run({ input, step }) {
|
|
78
|
+
const hosts = await step.run('list', () => listHosts());
|
|
79
|
+
for (const host of hosts) {
|
|
80
|
+
// One step per child, keyed by the row: a replayed attempt re-enqueues nothing it already did.
|
|
81
|
+
await step.run(`enqueue:${host.id}`, () =>
|
|
82
|
+
pollHost.enqueue({ hostId: host.id, occurrenceMs: input.occurrenceMs }),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// tasks/poll-fleet.ts — the tick hands its instant to ONE job
|
|
89
|
+
export const pollFleet = task({
|
|
90
|
+
cron: '*/5 * * * *',
|
|
91
|
+
tz: 'UTC',
|
|
92
|
+
enqueue: (occurrenceMs) => [[pollHosts, { occurrenceMs }]],
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| Piece | Why it is this piece |
|
|
97
|
+
|---|---|
|
|
98
|
+
| the task enqueues one job | the tick stays cheap and deterministic; `describe()` names `pollHosts` and nothing else |
|
|
99
|
+
| the fan-out is a `job` | it reads the database, so it gets a queue row, retries, a trace and `x jobs show` — a task gets none of those |
|
|
100
|
+
| one `step.run` per child | at-least-once delivery would otherwise enqueue every child again on a replay; the step's name is the row's id, so the second attempt skips the ones that landed |
|
|
101
|
+
| the child is its own `job` | retry, concurrency and the dead-letter path are per row — one host being down must not retry the other forty. `webhook()` is the same shape: one event, one endpoint, one job |
|
|
102
|
+
| the child's `idempotencyKey` carries `occurrenceMs` | the same row for the same tick is one row in the queue however many times the fan-out replays |
|
|
103
|
+
|
|
104
|
+
`webhook()` below is this pattern shipped as a factory; a fleet poll, a per-tenant digest and a
|
|
105
|
+
per-subscriber notification are the same three files with different names.
|
|
106
|
+
|
|
45
107
|
## The export name is the job's name
|
|
46
108
|
|
|
47
109
|
```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/jobs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "19.1.0",
|
|
4
4
|
"description": "Durable background work: steps, transactional outbox, cron tasks, one driver interface",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"test": "bun test"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@ultimat3/core": "
|
|
36
|
-
"@ultimat3/entity": "
|
|
37
|
-
"@ultimat3/schema": "
|
|
38
|
-
"@ultimat3/time": "
|
|
35
|
+
"@ultimat3/core": "19.1.0",
|
|
36
|
+
"@ultimat3/entity": "19.1.0",
|
|
37
|
+
"@ultimat3/schema": "19.1.0",
|
|
38
|
+
"@ultimat3/time": "19.1.0"
|
|
39
39
|
}
|
|
40
40
|
}
|