@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.
Files changed (86) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-ZOEPKBVF.js → chunk-4MWG3B7G.js} +1347 -428
  5. package/dist/chunk-4MWG3B7G.js.map +1 -0
  6. package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
  7. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  8. package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
  9. package/dist/chunk-GVNLZZTN.js.map +1 -0
  10. package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +515 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +964 -152
  16. package/dist/index.js +4279 -866
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +3 -3
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +651 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +276 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/facades.md +278 -0
  46. package/docs/hashing.md +118 -0
  47. package/docs/helpers.md +1281 -0
  48. package/docs/http-tests.md +386 -0
  49. package/docs/inertia.md +485 -0
  50. package/docs/installation.md +209 -0
  51. package/docs/lifecycle.md +234 -0
  52. package/docs/live-tests.md +226 -0
  53. package/docs/live.md +910 -0
  54. package/docs/logging.md +255 -0
  55. package/docs/mail.md +619 -0
  56. package/docs/middleware.md +384 -0
  57. package/docs/migrations.md +416 -0
  58. package/docs/mocking.md +264 -0
  59. package/docs/navigation.json +273 -0
  60. package/docs/notifications.md +225 -0
  61. package/docs/pagination.md +247 -0
  62. package/docs/passwords.md +207 -0
  63. package/docs/providers.md +187 -0
  64. package/docs/queries.md +600 -0
  65. package/docs/queues.md +330 -0
  66. package/docs/requests.md +533 -0
  67. package/docs/responses.md +444 -0
  68. package/docs/routing.md +738 -0
  69. package/docs/scheduling.md +277 -0
  70. package/docs/seeding.md +122 -0
  71. package/docs/session.md +409 -0
  72. package/docs/starter-kits.md +396 -0
  73. package/docs/storage.md +192 -0
  74. package/docs/structure.md +264 -0
  75. package/docs/testing.md +165 -0
  76. package/docs/time-travel.md +138 -0
  77. package/docs/validation.md +1792 -0
  78. package/docs/verification.md +152 -0
  79. package/docs/views.md +566 -0
  80. package/docs/vite.md +322 -0
  81. package/package.json +34 -32
  82. package/dist/chunk-I63ZFMX6.js.map +0 -1
  83. package/dist/chunk-MFTZQMZM.js.map +0 -1
  84. package/dist/chunk-POEP4B2M.js.map +0 -1
  85. package/dist/chunk-ZOEPKBVF.js.map +0 -1
  86. 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`.