@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.7
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/NOTICE +4 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-ZOEPKBVF.js → chunk-4MWG3B7G.js} +1347 -428
- package/dist/chunk-4MWG3B7G.js.map +1 -0
- package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
- package/dist/chunk-7ZQZYDOC.js.map +1 -0
- package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
- package/dist/chunk-GVNLZZTN.js.map +1 -0
- package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
- package/dist/chunk-VOYW3R5J.js.map +1 -0
- package/dist/contracts.d.ts +515 -17
- package/dist/contracts.js +1 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +964 -152
- package/dist/index.js +4279 -866
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +3 -3
- package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/docs/authentication.md +487 -0
- package/docs/authorization.md +435 -0
- package/docs/cache.md +647 -0
- package/docs/collections.md +2270 -0
- package/docs/configuration.md +334 -0
- package/docs/console.md +651 -0
- package/docs/container.md +430 -0
- package/docs/controllers.md +406 -0
- package/docs/csrf.md +126 -0
- package/docs/database-testing.md +144 -0
- package/docs/database.md +236 -0
- package/docs/deployment.md +276 -0
- package/docs/eloquent-collections.md +218 -0
- package/docs/eloquent-factories.md +255 -0
- package/docs/eloquent-mutators.md +254 -0
- package/docs/eloquent-observers.md +145 -0
- package/docs/eloquent-relationships.md +557 -0
- package/docs/eloquent.md +494 -0
- package/docs/encryption.md +104 -0
- package/docs/errors.md +318 -0
- package/docs/events.md +336 -0
- package/docs/facades.md +278 -0
- package/docs/hashing.md +118 -0
- package/docs/helpers.md +1281 -0
- package/docs/http-tests.md +386 -0
- package/docs/inertia.md +485 -0
- package/docs/installation.md +209 -0
- package/docs/lifecycle.md +234 -0
- package/docs/live-tests.md +226 -0
- package/docs/live.md +910 -0
- package/docs/logging.md +255 -0
- package/docs/mail.md +619 -0
- package/docs/middleware.md +384 -0
- package/docs/migrations.md +416 -0
- package/docs/mocking.md +264 -0
- package/docs/navigation.json +273 -0
- package/docs/notifications.md +225 -0
- package/docs/pagination.md +247 -0
- package/docs/passwords.md +207 -0
- package/docs/providers.md +187 -0
- package/docs/queries.md +600 -0
- package/docs/queues.md +330 -0
- package/docs/requests.md +533 -0
- package/docs/responses.md +444 -0
- package/docs/routing.md +738 -0
- package/docs/scheduling.md +277 -0
- package/docs/seeding.md +122 -0
- package/docs/session.md +409 -0
- package/docs/starter-kits.md +396 -0
- package/docs/storage.md +192 -0
- package/docs/structure.md +264 -0
- package/docs/testing.md +165 -0
- package/docs/time-travel.md +138 -0
- package/docs/validation.md +1792 -0
- package/docs/verification.md +152 -0
- package/docs/views.md +566 -0
- package/docs/vite.md +322 -0
- package/package.json +34 -32
- package/dist/chunk-I63ZFMX6.js.map +0 -1
- package/dist/chunk-MFTZQMZM.js.map +0 -1
- package/dist/chunk-POEP4B2M.js.map +0 -1
- package/dist/chunk-ZOEPKBVF.js.map +0 -1
- package/dist/index.d-BJyJx6U1.d.ts +0 -319
package/docs/queues.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Queues
|
|
2
|
+
|
|
3
|
+
Some work is too slow to do while a user waits: sending mail, resizing an image, calling another service. Push it onto
|
|
4
|
+
a queue instead, and a worker process runs it in the background while your app answers at once.
|
|
5
|
+
|
|
6
|
+
## Connections and queues
|
|
7
|
+
|
|
8
|
+
`config/queue.ts` lists your queue **connections**: where jobs are stored. A new app uses the `database` connection,
|
|
9
|
+
whose `jobs` table a new app's migrations already created:
|
|
10
|
+
|
|
11
|
+
```ts title="config/queue.ts"
|
|
12
|
+
import { env } from '@orieljs/framework'
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
default: env('QUEUE_CONNECTION', 'database'),
|
|
16
|
+
connections: {
|
|
17
|
+
sync: { driver: 'sync' },
|
|
18
|
+
database: { driver: 'database', table: 'jobs', queue: 'default', retry_after: 90, after_commit: false },
|
|
19
|
+
redis: { driver: 'redis', connection: 'default', queue: 'default', retry_after: 90, block_for: null, after_commit: false },
|
|
20
|
+
},
|
|
21
|
+
batching: { database: env('DB_CONNECTION', 'sqlite'), table: 'job_batches' },
|
|
22
|
+
failed: { driver: 'database-uuids', database: env('DB_CONNECTION', 'sqlite'), table: 'failed_jobs' },
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The `sync` driver runs each job straight away, in the same process, which is handy in tests; `redis` reads its server
|
|
27
|
+
from `database.redis` in `config/database.ts`. Within a connection, jobs go onto named **queues**, so a worker can run
|
|
28
|
+
the urgent ones first. Without a name, a job goes onto the connection's `queue`, here `default`.
|
|
29
|
+
|
|
30
|
+
## Creating jobs
|
|
31
|
+
|
|
32
|
+
`make:job` writes a job class in `app/Jobs`:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
node oriel make:job ProcessPodcast
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A job's constructor takes what the job needs, and `handle()` does the work. Everything the job stores in its fields
|
|
39
|
+
travels with it to the worker:
|
|
40
|
+
|
|
41
|
+
```ts title="app/Jobs/ProcessPodcast.ts"
|
|
42
|
+
import { Log, Queueable } from '@orieljs/framework'
|
|
43
|
+
import type { Podcast } from '../Models/Podcast'
|
|
44
|
+
|
|
45
|
+
export class ProcessPodcast extends Queueable {
|
|
46
|
+
constructor(public podcast: Podcast) {
|
|
47
|
+
super()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async handle(): Promise<void> {
|
|
51
|
+
Log.info('Processing a podcast', { file: this.podcast.audio_path })
|
|
52
|
+
this.podcast.published_at = new Date()
|
|
53
|
+
await this.podcast.save()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
A model in a job is stored as its class and key, not its attributes, and the worker reads it again from the database,
|
|
59
|
+
with the relations it had loaded, before `handle()` runs. Dates, maps, sets, plain objects and arrays travel as they
|
|
60
|
+
are. A model that has been deleted in the meantime fails the job; set `deleteWhenMissingModels = true` to drop the job
|
|
61
|
+
quietly instead.
|
|
62
|
+
|
|
63
|
+
> [!NOTE]
|
|
64
|
+
> Extending `Queueable` is what makes a job queued, as Laravel's `ShouldQueue` does. Laravel looks for the interface
|
|
65
|
+
> when your app runs; TypeScript interfaces are gone by then, so mail, listeners and notifications that should be
|
|
66
|
+
> queued say so with a field: `readonly shouldQueue = true`. `make:job --sync` writes a job that runs at once instead.
|
|
67
|
+
|
|
68
|
+
The worker finds your job classes in `app/Jobs` (and your events, listeners, mail and notifications in their folders)
|
|
69
|
+
by their class names, so give each its own name.
|
|
70
|
+
|
|
71
|
+
## Dispatching jobs
|
|
72
|
+
|
|
73
|
+
Dispatch a job with its static `dispatch()`, passing what its constructor takes:
|
|
74
|
+
|
|
75
|
+
```ts title="app/Http/Controllers/PodcastController.ts"
|
|
76
|
+
import { redirect } from '@orieljs/framework'
|
|
77
|
+
import { ProcessPodcast } from '../../Jobs/ProcessPodcast'
|
|
78
|
+
import { Podcast } from '../../Models/Podcast'
|
|
79
|
+
import { Controller } from './Controller'
|
|
80
|
+
|
|
81
|
+
export class PodcastController extends Controller {
|
|
82
|
+
async store() {
|
|
83
|
+
const podcast = await Podcast.create({ title: 'Episode 1', audio_path: 'podcasts/1.mp3' })
|
|
84
|
+
|
|
85
|
+
await ProcessPodcast.dispatch(podcast)
|
|
86
|
+
|
|
87
|
+
return redirect().back()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`dispatchIf()` and `dispatchUnless()` take a condition first:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
declare const podcast: Podcast
|
|
96
|
+
declare const accountActive: boolean
|
|
97
|
+
// ---cut---
|
|
98
|
+
await ProcessPodcast.dispatchIf(accountActive, podcast)
|
|
99
|
+
await ProcessPodcast.dispatchUnless(podcast.published_at !== null, podcast)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Delayed dispatching
|
|
103
|
+
|
|
104
|
+
`delay()` keeps a job off the worker for a number of seconds, or until a date:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
declare const podcast: Podcast
|
|
108
|
+
// ---cut---
|
|
109
|
+
await ProcessPodcast.dispatch(podcast).delay(10 * 60)
|
|
110
|
+
await ProcessPodcast.dispatch(podcast).delay(new Date('2026-12-24T09:00:00Z'))
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> [!WARNING]
|
|
114
|
+
> A dispatched job is sent at the end of the current step of your code (or when you `await` it), so chain its options
|
|
115
|
+
> in the same statement: `ProcessPodcast.dispatch(podcast).onQueue('audio').delay(60)`. Setting one later throws.
|
|
116
|
+
|
|
117
|
+
### Running a job now
|
|
118
|
+
|
|
119
|
+
`dispatchSync()` runs the job in the current process and waits for it, and `dispatchAfterResponse()` runs it once the
|
|
120
|
+
response has been sent to the browser:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
declare const podcast: Podcast
|
|
124
|
+
// ---cut---
|
|
125
|
+
await ProcessPodcast.dispatchSync(podcast)
|
|
126
|
+
ProcessPodcast.dispatchAfterResponse(podcast)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Choosing the queue and the connection
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
declare const podcast: Podcast
|
|
133
|
+
// ---cut---
|
|
134
|
+
await ProcessPodcast.dispatch(podcast).onQueue('processing')
|
|
135
|
+
await ProcessPodcast.dispatch(podcast).onConnection('redis')
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A job can also name them for itself in its `queue` and `connection` fields.
|
|
139
|
+
|
|
140
|
+
### Job chaining
|
|
141
|
+
|
|
142
|
+
A chain runs its jobs one after the other, and stops when one fails. `catch()` takes a job to run when that happens:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
declare const podcast: Podcast
|
|
146
|
+
// ---cut---
|
|
147
|
+
await Bus.chain([new ProcessPodcast(podcast), new OptimizePodcast(podcast), new PublishPodcast(podcast)])
|
|
148
|
+
.onQueue('processing')
|
|
149
|
+
.catch(new ReportFailure(podcast.id))
|
|
150
|
+
.dispatch()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
> [!NOTE]
|
|
154
|
+
> Laravel's chains and batches take closures for their callbacks. A closure can't be stored on a queue in
|
|
155
|
+
> JavaScript, so Oriel's take jobs.
|
|
156
|
+
|
|
157
|
+
## Retries and timeouts
|
|
158
|
+
|
|
159
|
+
A job is attempted once unless it, or the worker, says otherwise. A job's fields set its own limits:
|
|
160
|
+
|
|
161
|
+
```ts title="app/Jobs/ProcessPodcast.ts"
|
|
162
|
+
import { Queueable } from '@orieljs/framework'
|
|
163
|
+
import type { Podcast } from '../Models/Podcast'
|
|
164
|
+
|
|
165
|
+
export class ProcessPodcast extends Queueable {
|
|
166
|
+
tries = 3
|
|
167
|
+
backoff = [10, 60]
|
|
168
|
+
timeout = 120
|
|
169
|
+
|
|
170
|
+
constructor(public podcast: Podcast) {
|
|
171
|
+
super()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async handle(): Promise<void> {
|
|
175
|
+
if (this.attempts() > 1) {
|
|
176
|
+
// a retry: the first attempt failed
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async failed(error: unknown): Promise<void> {
|
|
181
|
+
// every attempt failed: tell someone
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`backoff` is the number of seconds to wait before each retry, one number for all of them or one per retry. `timeout`
|
|
187
|
+
is how many seconds an attempt may run before the worker gives up on it. Inside `handle()`, `this.release(seconds)`
|
|
188
|
+
puts the job back on the queue for later, and `this.fail(error)` fails it at once, with no more retries.
|
|
189
|
+
|
|
190
|
+
## Job middleware
|
|
191
|
+
|
|
192
|
+
A job's `middleware()` wraps its `handle()`. `WithoutOverlapping` keeps two jobs of the same class with the same key
|
|
193
|
+
from running at the same time (`shared()` makes the key count across classes); one that finds the key taken goes back on
|
|
194
|
+
the queue:
|
|
195
|
+
|
|
196
|
+
```ts title="app/Jobs/ProcessPodcast.ts"
|
|
197
|
+
import { Queueable, WithoutOverlapping } from '@orieljs/framework'
|
|
198
|
+
import type { Podcast } from '../Models/Podcast'
|
|
199
|
+
|
|
200
|
+
export class ProcessPodcast extends Queueable {
|
|
201
|
+
constructor(public podcast: Podcast) {
|
|
202
|
+
super()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
middleware() {
|
|
206
|
+
return [new WithoutOverlapping(this.podcast.id).releaseAfterSeconds(30)]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async handle(): Promise<void> {}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Unique jobs
|
|
214
|
+
|
|
215
|
+
A job with a `uniqueId()` isn't queued while another job of its class with the same ID is waiting or running: the
|
|
216
|
+
second dispatch is dropped. Uniqueness uses a cache lock, so it needs a cache store with locks, such as a new app's
|
|
217
|
+
`database` store:
|
|
218
|
+
|
|
219
|
+
```ts title="app/Jobs/UpdateSearchIndex.ts"
|
|
220
|
+
import { Queueable } from '@orieljs/framework'
|
|
221
|
+
|
|
222
|
+
export class UpdateSearchIndex extends Queueable {
|
|
223
|
+
uniqueFor = 3600
|
|
224
|
+
|
|
225
|
+
constructor(public productId: number) {
|
|
226
|
+
super()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
uniqueId(): string {
|
|
230
|
+
return String(this.productId)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async handle(): Promise<void> {}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The lock is released when the job has run, or has failed for good; a job released for a retry keeps it. `uniqueFor`
|
|
238
|
+
is how many seconds the lock lasts at most, in case a job never finishes; without it, the lock lasts until then. With
|
|
239
|
+
`uniqueUntilProcessing = true`, the lock is released as the job starts, so the next one can be queued while it runs.
|
|
240
|
+
`uniqueVia()` returns the name of the cache store that holds the lock, in place of your default store. Any of these
|
|
241
|
+
members makes a job unique, with or without a `uniqueId()`: without one, the class alone is the ID.
|
|
242
|
+
|
|
243
|
+
Queued mail, notifications and listeners can be unique in the same way, with these members on the mailable, the
|
|
244
|
+
notification or the listener. A duplicate is:
|
|
245
|
+
|
|
246
|
+
- a mailable of the same class, with the same `uniqueId()`, sent to the same recipients with `Mail.to()`, `cc()` and
|
|
247
|
+
`bcc()` (see [Unique mailables](mail.md#unique-mailables));
|
|
248
|
+
- a notification of the same class, with the same `uniqueId()`, to the same notifiable on the same channel (see
|
|
249
|
+
[Unique notifications](notifications.md#unique-notifications));
|
|
250
|
+
- a listener of the same class, with the same `uniqueId()`, which receives the event: a listener has one lock for all
|
|
251
|
+
its methods and events (see [Unique listeners](events.md#unique-listeners)).
|
|
252
|
+
|
|
253
|
+
## Job batching
|
|
254
|
+
|
|
255
|
+
A batch runs many jobs side by side and tells you when they have all finished. The batch's own jobs say what happens
|
|
256
|
+
then:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
declare const files: string[]
|
|
260
|
+
// ---cut---
|
|
261
|
+
const batch = await Bus.batch(files.map((file) => new ImportCsv(file)))
|
|
262
|
+
.name('Import CSV')
|
|
263
|
+
.then(new ImportFinished())
|
|
264
|
+
.catch(new ImportFailed())
|
|
265
|
+
.allowFailures()
|
|
266
|
+
.dispatch()
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`then()` runs when every job has succeeded, `catch()` when the first one fails, and `finally()` when the batch is done
|
|
270
|
+
either way. A failure marks the batch cancelled, unless it `allowFailures()`, but its other jobs still run: a job that
|
|
271
|
+
should stop checks `(await this.batch())?.cancelled()` first, as in Laravel. `Bus.findBatch(id)` reads a batch's
|
|
272
|
+
progress later: its `totalJobs`, `pendingJobs`, `failedJobs`, `progress()` (a percentage) and `finished()`. Inside a
|
|
273
|
+
job, `await this.batch()` returns its batch. A job in a batch keeps its own `delay`, except on the `sync` connection,
|
|
274
|
+
which runs every job at once.
|
|
275
|
+
|
|
276
|
+
## Running the queue worker
|
|
277
|
+
|
|
278
|
+
The worker takes jobs off the queue and runs them until you stop it:
|
|
279
|
+
|
|
280
|
+
```sh
|
|
281
|
+
node oriel queue:work
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
It runs the default connection's default queue. Name another connection, and list the queues to work in order of
|
|
285
|
+
priority:
|
|
286
|
+
|
|
287
|
+
```sh
|
|
288
|
+
node oriel queue:work redis --queue=high,default
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
| Option | Meaning |
|
|
292
|
+
| --- | --- |
|
|
293
|
+
| `--once` | Run one job, then stop |
|
|
294
|
+
| `--stop-when-empty` | Stop when the queue is empty |
|
|
295
|
+
| `--tries=1` | Attempts for a job that doesn't set `tries` |
|
|
296
|
+
| `--backoff=0` | Seconds before retrying a job that doesn't set `backoff` |
|
|
297
|
+
| `--timeout=60` | Seconds a job may run, unless it sets `timeout` |
|
|
298
|
+
| `--memory=512` | Stop when the worker's JavaScript heap passes this many megabytes |
|
|
299
|
+
| `--max-jobs=0`, `--max-time=0` | Stop after this many jobs, or seconds (0: never) |
|
|
300
|
+
| `--sleep=3` | Seconds to wait when there is no job |
|
|
301
|
+
| `--rest=0` | Seconds to wait between jobs |
|
|
302
|
+
|
|
303
|
+
A worker loads your code once, so it doesn't see your changes until it restarts. While you work, run
|
|
304
|
+
`node oriel queue:listen` instead: it starts a fresh process for every job. In production, keep workers running with a
|
|
305
|
+
process manager such as systemd or Supervisor, and after each deploy run `node oriel queue:restart`, which tells every
|
|
306
|
+
worker to stop after its current job so the process manager starts it again on the new code
|
|
307
|
+
([Deployment](deployment.md#queue-workers) shows a Supervisor program).
|
|
308
|
+
|
|
309
|
+
`queue:restart` leaves its signal in your default cache store, which each worker reads after every job. While the
|
|
310
|
+
cache can't be read, a worker goes on working and reports the error once per outage; it sees the next signal once
|
|
311
|
+
the cache is back.
|
|
312
|
+
|
|
313
|
+
> [!TIP]
|
|
314
|
+
> A worker that stops on a timeout or its memory limit exits with an error code on purpose: the process manager
|
|
315
|
+
> restarts it with a clean slate.
|
|
316
|
+
|
|
317
|
+
## Failed jobs
|
|
318
|
+
|
|
319
|
+
A job that has used up its attempts is stored in the `failed_jobs` table, and its `failed()` method runs. The queue
|
|
320
|
+
commands manage them:
|
|
321
|
+
|
|
322
|
+
```sh
|
|
323
|
+
node oriel queue:failed
|
|
324
|
+
node oriel queue:retry 5a6b8c1e-9d2f-4e3a-8b7c-1f2e3d4c5b6a
|
|
325
|
+
node oriel queue:retry all
|
|
326
|
+
node oriel queue:forget 5a6b8c1e-9d2f-4e3a-8b7c-1f2e3d4c5b6a
|
|
327
|
+
node oriel queue:flush
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
`queue:clear` deletes the jobs still waiting on a queue: `node oriel queue:clear redis --queue=emails`.
|