@orieljs/framework 0.1.0-alpha.5 → 0.1.0-alpha.6

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/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  renderToString,
23
23
  styleString,
24
24
  view
25
- } from "./chunk-A5XRLEWM.js";
25
+ } from "./chunk-MFTZQMZM.js";
26
26
  import {
27
27
  API_ROUTES_STUB,
28
28
  Application as Application2,
@@ -131,7 +131,7 @@ import {
131
131
  vite,
132
132
  viteReactRefresh,
133
133
  writeResponse
134
- } from "./chunk-WNJP46B7.js";
134
+ } from "./chunk-ZOEPKBVF.js";
135
135
  import {
136
136
  Arr,
137
137
  BindingResolutionError,
@@ -143,6 +143,7 @@ import {
143
143
  LazyCollection,
144
144
  ServiceProvider,
145
145
  Str,
146
+ Stringable,
146
147
  __,
147
148
  app,
148
149
  collect,
@@ -155,7 +156,7 @@ import {
155
156
  retry,
156
157
  tap,
157
158
  value
158
- } from "./chunk-PM5YTG57.js";
159
+ } from "./chunk-POEP4B2M.js";
159
160
  import {
160
161
  Application,
161
162
  AuthFactory,
@@ -197,7 +198,7 @@ import {
197
198
  ValidationFactory,
198
199
  ViewFactory,
199
200
  token
200
- } from "./chunk-BSQKOLSK.js";
201
+ } from "./chunk-I63ZFMX6.js";
201
202
 
202
203
  // ../Auth/dist/index.js
203
204
  import { createHash } from "crypto";
@@ -3789,6 +3790,34 @@ var LISTENER_STUB_PLAIN = `export class {{ class }} {
3789
3790
  }
3790
3791
  }
3791
3792
  `;
3793
+ var LISTENER_STUB_QUEUED = `import { {{ event }} } from '{{ eventImport }}'
3794
+ import type { ShouldQueue } from '@orieljs/framework/contracts'
3795
+
3796
+ export class {{ class }} implements ShouldQueue {
3797
+ static listensTo = {{ event }}
3798
+
3799
+ /** Run by the queue worker (node oriel queue:work): remove this line and \`implements ShouldQueue\` to run it right away. */
3800
+ readonly shouldQueue = true
3801
+
3802
+ async handle(_event: {{ event }}): Promise<void> {
3803
+ //
3804
+ }
3805
+ }
3806
+ `;
3807
+ var LISTENER_STUB_QUEUED_PLAIN = `import type { ShouldQueue } from '@orieljs/framework/contracts'
3808
+
3809
+ export class {{ class }} implements ShouldQueue {
3810
+ // Name the event class (or event name) this listener handles, so app/Listeners discovery registers it:
3811
+ // static listensTo = OrderShipped
3812
+
3813
+ /** Run by the queue worker (node oriel queue:work): remove this line and \`implements ShouldQueue\` to run it right away. */
3814
+ readonly shouldQueue = true
3815
+
3816
+ async handle(_event: unknown): Promise<void> {
3817
+ //
3818
+ }
3819
+ }
3820
+ `;
3792
3821
  var JOB_STUB = `import { Queueable } from '@orieljs/framework'
3793
3822
 
3794
3823
  export class {{ class }} extends Queueable {
@@ -3964,15 +3993,12 @@ var MakeCastCommand = class extends GeneratorCommand {
3964
3993
  }
3965
3994
  };
3966
3995
  var MakeCommandCommand = class extends GeneratorCommand {
3967
- /** --command sets the generated signature; without it, Laravel's `app:` plus the kebab-cased name is used. */
3996
+ /** --command sets the generated signature; without it, Laravel's `app:` plus the kebab-cased class name is used. */
3968
3997
  signature = "make:command {name} {--command=} {--force}";
3969
3998
  description = "Create a new Artisan command";
3970
3999
  type() {
3971
4000
  return "Console/Commands";
3972
4001
  }
3973
- suffix() {
3974
- return "Command";
3975
- }
3976
4002
  stub() {
3977
4003
  return COMMAND_STUB;
3978
4004
  }
@@ -3980,7 +4006,7 @@ var MakeCommandCommand = class extends GeneratorCommand {
3980
4006
  const option = this.option("command");
3981
4007
  const className = this.qualifyClass(rawName).className;
3982
4008
  return {
3983
- signature: typeof option === "string" ? option : `app:${Str.kebab(className.replace(/Command$/, ""))}`
4009
+ signature: typeof option === "string" ? option : `app:${Str.kebab(className)}`
3984
4010
  };
3985
4011
  }
3986
4012
  };
@@ -4053,13 +4079,15 @@ var MakeJobCommand = class extends GeneratorCommand {
4053
4079
  }
4054
4080
  };
4055
4081
  var MakeListenerCommand = class extends GeneratorCommand {
4056
- signature = "make:listener {name} {--e|event= : The event class being listened for} {--force}";
4082
+ signature = "make:listener {name} {--e|event= : The event class being listened for} {--queued : Indicates the event listener should be queued} {--force}";
4057
4083
  description = "Create a new event listener class";
4058
4084
  type() {
4059
4085
  return "Listeners";
4060
4086
  }
4061
4087
  stub() {
4062
- return typeof this.option("event") === "string" ? LISTENER_STUB : LISTENER_STUB_PLAIN;
4088
+ const typed = typeof this.option("event") === "string";
4089
+ if (this.option("queued") === true) return typed ? LISTENER_STUB_QUEUED : LISTENER_STUB_QUEUED_PLAIN;
4090
+ return typed ? LISTENER_STUB : LISTENER_STUB_PLAIN;
4063
4091
  }
4064
4092
  extraPlaceholders(rawName) {
4065
4093
  const event2 = this.option("event");
@@ -4247,6 +4275,29 @@ var MakeTestCommand = class extends GeneratorCommand {
4247
4275
  return `${className.replace(/(?<=.)Test$/, "")}.test.ts`;
4248
4276
  }
4249
4277
  };
4278
+ var ScheduleClearCacheCommand = class extends Command {
4279
+ signature = "schedule:clear-cache";
4280
+ description = "Delete the cached mutex files created by scheduler";
4281
+ async handle() {
4282
+ let cleared = false;
4283
+ if (currentContainer()?.bound(CacheManager)) {
4284
+ const cache2 = inject(CacheManager);
4285
+ for (const event2 of inject(Schedule).events()) {
4286
+ if (!event2.preventsOverlapping) continue;
4287
+ const lock = cache2.lock(event2.mutexName(), event2.expiresAt * 60);
4288
+ if (await lock.get()) {
4289
+ await lock.release();
4290
+ continue;
4291
+ }
4292
+ this.info(`Deleting mutex for [${event2.command ?? event2.getSummaryForDisplay()}]`);
4293
+ await lock.forceRelease();
4294
+ cleared = true;
4295
+ }
4296
+ }
4297
+ if (!cleared) this.info("No mutex files were found.");
4298
+ return 0;
4299
+ }
4300
+ };
4250
4301
  var ScheduleListCommand = class extends Command {
4251
4302
  signature = "schedule:list";
4252
4303
  description = "List the scheduled commands";
@@ -4258,18 +4309,15 @@ var ScheduleListCommand = class extends Command {
4258
4309
  }
4259
4310
  this.table(
4260
4311
  ["Command", "Expression"],
4261
- events.map((event2) => [event2.getSummary(), event2.expression])
4312
+ events.map((event2) => [event2.getSummaryForDisplay(), event2.expression])
4262
4313
  );
4263
4314
  return 0;
4264
4315
  }
4265
4316
  };
4266
- function summaryOf(event2) {
4267
- return event2.getSummary();
4268
- }
4269
4317
  async function runDueEvents(events, io, now5) {
4270
4318
  const backgroundRuns = [];
4271
4319
  for (const event2 of events) {
4272
- io.info(`Running scheduled task: ${summaryOf(event2)}`);
4320
+ io.info(`Running scheduled task: ${event2.getSummaryForDisplay()}`);
4273
4321
  const attempt = async () => {
4274
4322
  try {
4275
4323
  await event2.run(now5);
@@ -4617,6 +4665,15 @@ var ScheduledEventImpl = class {
4617
4665
  get expression() {
4618
4666
  return this.cronExpression;
4619
4667
  }
4668
+ get command() {
4669
+ return this.commandString;
4670
+ }
4671
+ get preventsOverlapping() {
4672
+ return this.overlapExpiryMinutes !== void 0;
4673
+ }
4674
+ get expiresAt() {
4675
+ return this.overlapExpiryMinutes ?? 1440;
4676
+ }
4620
4677
  cron(expression) {
4621
4678
  this.cronExpression = expression;
4622
4679
  return this;
@@ -4710,15 +4767,19 @@ var ScheduledEventImpl = class {
4710
4767
  this.eventDescription = description;
4711
4768
  return this;
4712
4769
  }
4713
- /** Accessors beyond the contract, read back by schedule:list and the schedule:run/schedule:work loop. */
4770
+ getSummaryForDisplay() {
4771
+ return this.eventName ?? this.eventDescription ?? this.commandString ?? "Closure";
4772
+ }
4773
+ /** Accessors beyond the contract, read back by the schedule:run/schedule:work loop. */
4714
4774
  getName() {
4715
4775
  return this.eventName;
4716
4776
  }
4717
4777
  getDescription() {
4718
4778
  return this.eventDescription;
4719
4779
  }
4780
+ /** The same as getSummaryForDisplay(). */
4720
4781
  getSummary() {
4721
- return this.eventName ?? this.eventDescription ?? "(closure)";
4782
+ return this.getSummaryForDisplay();
4722
4783
  }
4723
4784
  isBackground() {
4724
4785
  return this.background;
@@ -4774,11 +4835,11 @@ var ScheduledEventImpl = class {
4774
4835
  * Follows Laravel: a command event's mutex is `sha1(expression + full command
4775
4836
  * string incl. parameters)` (Laravel's `Event::mutexName()`) — no `name()` required. A closure event (`call()`/
4776
4837
  * `job()`) has no command string, so its mutex is `sha1(name())` (Laravel's `CallbackEvent::mutexName()`, which
4777
- * reads the description `name()` also writes). `requireLockableName()` guarantees `eventName` is set by the time
4778
- * this runs for a closure event.
4838
+ * reads the description `name()` also writes), and `sha1('')` while it has no name, as Laravel's
4839
+ * (`requireLockableName()` keeps an unnamed closure from ever taking the lock).
4779
4840
  */
4780
4841
  mutexName() {
4781
- const key = this.commandString !== void 0 ? this.cronExpression + this.commandString : this.eventName;
4842
+ const key = this.commandString !== void 0 ? this.cronExpression + this.commandString : this.eventName ?? "";
4782
4843
  return `framework/schedule-${sha1(key)}`;
4783
4844
  }
4784
4845
  /**
@@ -4855,6 +4916,7 @@ var ConsoleServiceProvider = class extends ServiceProvider {
4855
4916
  ScheduleRunCommand,
4856
4917
  ScheduleWorkCommand,
4857
4918
  ScheduleListCommand,
4919
+ ScheduleClearCacheCommand,
4858
4920
  MakeControllerCommand,
4859
4921
  MakeModelCommand,
4860
4922
  MakeMiddlewareCommand,
@@ -4884,7 +4946,7 @@ var ConsoleServiceProvider = class extends ServiceProvider {
4884
4946
  }
4885
4947
  };
4886
4948
  var Artisan = facade(ConsoleKernel);
4887
- var Schedule4 = facade(Schedule);
4949
+ var Schedule5 = facade(Schedule);
4888
4950
  async function runConsole(app2, argv = process.argv.slice(2)) {
4889
4951
  app2.setRunningInConsole(true);
4890
4952
  try {
@@ -4895,7 +4957,17 @@ async function runConsole(app2, argv = process.argv.slice(2)) {
4895
4957
  }
4896
4958
  }
4897
4959
  async function runConsoleAndExit(app2, argv = process.argv.slice(2), exit = (code) => process.exit(code)) {
4898
- exit(await runConsole(app2, argv));
4960
+ let code;
4961
+ try {
4962
+ code = await runConsole(app2, argv);
4963
+ } finally {
4964
+ await flushLogs(app2);
4965
+ }
4966
+ exit(code);
4967
+ }
4968
+ async function flushLogs(app2) {
4969
+ if (!app2.bound(LogManager)) return;
4970
+ await app2.make(LogManager).flush?.();
4899
4971
  }
4900
4972
 
4901
4973
  // ../Cookie/dist/index.js
@@ -5499,7 +5571,10 @@ var AbstractPaginator = class _AbstractPaginator {
5499
5571
  this.collection = items;
5500
5572
  return this;
5501
5573
  }
5502
- /** Transforms each item in place, typically for serialization: paginator.through(post => ({ id: post.id })). */
5574
+ /**
5575
+ * Transforms each item in place, typically for serialization, and returns this paginator typed with the new items:
5576
+ * `posts.through((post) => ({ id: post.id }))`. Each paginator narrows the return type to its own class.
5577
+ */
5503
5578
  through(callback) {
5504
5579
  this.collection = this.collection.map(callback);
5505
5580
  return this;
@@ -5599,6 +5674,9 @@ var CursorPaginator = class extends AbstractPaginator {
5599
5674
  getCursorName() {
5600
5675
  return this.cursorName;
5601
5676
  }
5677
+ through(callback) {
5678
+ return super.through(callback);
5679
+ }
5602
5680
  hasMorePages() {
5603
5681
  return this.cursorValue === void 0 && this.hasMore || this.cursorValue !== void 0 && this.cursorValue.pointsToNextItems() && this.hasMore || this.cursorValue !== void 0 && this.cursorValue.pointsToPreviousItems();
5604
5682
  }
@@ -5655,6 +5733,9 @@ var LengthAwarePaginator = class extends AbstractPaginator {
5655
5733
  total() {
5656
5734
  return this.totalValue;
5657
5735
  }
5736
+ through(callback) {
5737
+ return super.through(callback);
5738
+ }
5658
5739
  lastPage() {
5659
5740
  return this.lastPageValue;
5660
5741
  }
@@ -5743,6 +5824,9 @@ var Paginator = class extends AbstractPaginator {
5743
5824
  hasMorePages() {
5744
5825
  return this.hasMore;
5745
5826
  }
5827
+ through(callback) {
5828
+ return super.through(callback);
5829
+ }
5746
5830
  hasPages() {
5747
5831
  return this.currentPageValue !== 1 || this.hasMore;
5748
5832
  }
@@ -8202,12 +8286,19 @@ var Builder2 = class extends Builder {
8202
8286
  const key = column.includes(".") ? column.slice(column.lastIndexOf(".") + 1) : column;
8203
8287
  return new Collection(rows.map((row) => this.model.newInstance().setRawAttributes({ [key]: row[key] }).getAttribute(key)));
8204
8288
  }
8289
+ /** Whether `id` can't be one of the model's unique ids (not a UUID for HasUuids): it matches no row. */
8290
+ isMalformedKey(id) {
8291
+ return this.model.uniqueIds().includes(this.model.getKeyName()) && !this.model.isValidUniqueId(id);
8292
+ }
8293
+ /** A malformed unique id (HasUuids, HasUlids) finds nothing without a query, as route model binding does. */
8205
8294
  async find(id) {
8295
+ if (this.isMalformedKey(id)) return void 0;
8206
8296
  return this.clone().whereKey(id).first();
8207
8297
  }
8208
8298
  async findMany(ids) {
8209
- if (!ids.length) return new Collection();
8210
- return this.clone().whereKey([...ids]).get();
8299
+ const keys = ids.filter((id) => !this.isMalformedKey(id));
8300
+ if (!keys.length) return new Collection();
8301
+ return this.clone().whereKey(keys).get();
8211
8302
  }
8212
8303
  async findOrFail(id) {
8213
8304
  if (Array.isArray(id)) {
@@ -8844,6 +8935,8 @@ var HasOneOrMany = class extends Relation {
8844
8935
  foreignKey;
8845
8936
  localKey;
8846
8937
  morph;
8938
+ /** The relation on each related model that chaperone() points back at the parent. */
8939
+ inverseRelationship;
8847
8940
  /** 'post_id' */
8848
8941
  getForeignKeyName() {
8849
8942
  return this.foreignKey.slice(this.foreignKey.lastIndexOf(".") + 1);
@@ -8865,6 +8958,61 @@ var HasOneOrMany = class extends Relation {
8865
8958
  getMorphClass() {
8866
8959
  return this.morph?.value;
8867
8960
  }
8961
+ /**
8962
+ * Laravel's chaperone(): every related model this relation loads, eagerly or lazily, or makes, gets the parent as
8963
+ * its `relation` (guessed when not given: post_id → post, the parent's class, owner), so reading it needs no query:
8964
+ * `comments() { return this.hasMany(Comment).chaperone() }`.
8965
+ */
8966
+ chaperone(relation) {
8967
+ const name = relation ?? this.guessInverseRelation();
8968
+ if (name === void 0 || !this.model.isRelation(name)) throw new RelationNotFoundError(this.model.constructor.name, name ?? "null");
8969
+ this.inverseRelationship = name;
8970
+ return this;
8971
+ }
8972
+ /** Laravel's inverse(): chaperone() under its other name. */
8973
+ inverse(relation) {
8974
+ return this.chaperone(relation);
8975
+ }
8976
+ withoutChaperone() {
8977
+ this.inverseRelationship = void 0;
8978
+ return this;
8979
+ }
8980
+ withoutInverse() {
8981
+ return this.withoutChaperone();
8982
+ }
8983
+ getInverseRelationship() {
8984
+ return this.inverseRelationship;
8985
+ }
8986
+ /** The first of the possible names that is a relation of the related model. */
8987
+ guessInverseRelation() {
8988
+ return this.getPossibleInverseRelations().find((name) => this.model.isRelation(name));
8989
+ }
8990
+ /** Laravel's candidates: the morph name, the foreign key without the parent's key, the parent's class, owner, parent. */
8991
+ getPossibleInverseRelations() {
8992
+ const key = this.parent.getKeyName();
8993
+ const names = [
8994
+ this.morph ? Str.beforeLast(this.getMorphType(), "_type") : "",
8995
+ Str.camel(Str.beforeLast(this.getForeignKeyName(), key)),
8996
+ Str.camel(Str.beforeLast(this.parent.getForeignKey(), key)),
8997
+ Str.camel(this.parent.constructor.name),
8998
+ "owner",
8999
+ this.parent.constructor === this.model.constructor ? "parent" : ""
9000
+ ];
9001
+ return [...new Set(names.filter((name) => name !== ""))];
9002
+ }
9003
+ /** Sets the parent on a related model, when chaperone() named the relation. */
9004
+ applyInverseRelationToModel(model, parent = this.parent) {
9005
+ if (model && this.inverseRelationship !== void 0) model.setRelation(this.inverseRelationship, parent);
9006
+ return model;
9007
+ }
9008
+ applyInverseRelationToMany(models, parent = this.parent) {
9009
+ for (const model of models) this.applyInverseRelationToModel(model, parent);
9010
+ return models;
9011
+ }
9012
+ /** The relation query's rows (get(), first(), paginate(), …), pointing back at the parent with chaperone(). */
9013
+ async runSelect() {
9014
+ return this.applyInverseRelationToMany(await super.runSelect());
9015
+ }
8868
9016
  addConstraints() {
8869
9017
  this.where(this.foreignKey, "=", this.getParentKey());
8870
9018
  this.whereNotNull(this.foreignKey);
@@ -8898,7 +9046,7 @@ var HasOneOrMany = class extends Relation {
8898
9046
  setForeignAttributes(model) {
8899
9047
  model.setAttribute(this.getForeignKeyName(), this.getParentKey());
8900
9048
  if (this.morph) model.setAttribute(this.getMorphType(), this.morph.value);
8901
- return model;
9049
+ return this.applyInverseRelationToModel(model);
8902
9050
  }
8903
9051
  async saveMany(models) {
8904
9052
  const saved = [];
@@ -9217,7 +9365,8 @@ var HasMany = class extends HasOneOrMany {
9217
9365
  match(models, results, relation) {
9218
9366
  const dictionary = this.buildDictionary(results);
9219
9367
  for (const model of models) {
9220
- model.setRelation(relation, new Collection(dictionary.get(String(model.getAttribute(this.localKey))) ?? []));
9368
+ const children = this.applyInverseRelationToMany(dictionary.get(String(model.getAttribute(this.localKey))) ?? [], model);
9369
+ model.setRelation(relation, new Collection(children));
9221
9370
  }
9222
9371
  return models;
9223
9372
  }
@@ -9326,7 +9475,7 @@ var HasOne = class extends HasOneOrMany {
9326
9475
  match(models, results, relation) {
9327
9476
  const dictionary = this.buildDictionary(results);
9328
9477
  for (const model of models) {
9329
- model.setRelation(relation, dictionary.get(String(model.getAttribute(this.localKey)))?.[0] ?? null);
9478
+ model.setRelation(relation, this.applyInverseRelationToModel(dictionary.get(String(model.getAttribute(this.localKey)))?.[0] ?? null, model));
9330
9479
  }
9331
9480
  return models;
9332
9481
  }
@@ -9386,6 +9535,7 @@ function methodOf(model, name) {
9386
9535
  const value2 = Reflect.get(Object.getPrototypeOf(model), name);
9387
9536
  return typeof value2 === "function" ? value2 : void 0;
9388
9537
  }
9538
+ var serializing = /* @__PURE__ */ new WeakSet();
9389
9539
  function serializeDate(date) {
9390
9540
  return date.toISOString().replace(/\.(\d{3})Z$/, ".$1000Z");
9391
9541
  }
@@ -9841,7 +9991,7 @@ var Model = class _Model {
9841
9991
  /** Every cast, including the implied ones: the incrementing key (int) and the timestamp columns (datetime). */
9842
9992
  getCasts() {
9843
9993
  const implied = {};
9844
- if (this.incrementing) implied[this.getKeyName()] = this.keyType === "int" ? "int" : "string";
9994
+ if (this.getIncrementing()) implied[this.getKeyName()] = this.getKeyType() === "int" ? "int" : "string";
9845
9995
  if (this.usesTimestamps()) {
9846
9996
  const created = this.getCreatedAtColumn();
9847
9997
  const updated = this.getUpdatedAtColumn();
@@ -9964,6 +10114,29 @@ var Model = class _Model {
9964
10114
  getIncrementing() {
9965
10115
  return this.incrementing;
9966
10116
  }
10117
+ /** Whether new rows get unique string ids before `creating` (HasUuids, HasUlids). */
10118
+ usesUniqueIds() {
10119
+ return false;
10120
+ }
10121
+ /** The columns that get a unique id (HasUuids and HasUlids: the primary key). */
10122
+ uniqueIds() {
10123
+ return [];
10124
+ }
10125
+ /** A new unique id (HasUuids: a version 7 UUID; HasUlids: a lower-case ULID). */
10126
+ newUniqueId() {
10127
+ return void 0;
10128
+ }
10129
+ /** Whether a value has the shape of this model's unique ids (HasUuids: a UUID). Any value passes by default. */
10130
+ isValidUniqueId(_value) {
10131
+ return true;
10132
+ }
10133
+ /** Fills each empty unique id column with newUniqueId() (Laravel's setUniqueIds()). */
10134
+ setUniqueIds() {
10135
+ for (const column of this.uniqueIds()) {
10136
+ const value2 = this.getAttribute(column);
10137
+ if (value2 === void 0 || value2 === null || value2 === "") this.setAttribute(column, this.newUniqueId());
10138
+ }
10139
+ }
9967
10140
  getKey() {
9968
10141
  return this.getAttribute(this.getKeyName());
9969
10142
  }
@@ -9995,11 +10168,13 @@ var Model = class _Model {
9995
10168
  }
9996
10169
  /**
9997
10170
  * The model a route parameter value points at: where <field or route key> = value (global scopes apply, so a
9998
- * soft-deleted row isn't found). A non-numeric value for an integer primary key finds nothing without a query.
10171
+ * soft-deleted row isn't found). A non-numeric value for an integer primary key, or a value that isn't a UUID
10172
+ * (ULID) for a unique id column, finds nothing without a query.
9999
10173
  */
10000
10174
  async resolveRouteBinding(value2, field) {
10001
10175
  const column = field ?? this.getRouteKeyName();
10002
- if (column === this.getKeyName() && this.keyType === "int" && !/^-?\d+$/.test(value2)) return void 0;
10176
+ if (column === this.getKeyName() && this.getKeyType() === "int" && !/^-?\d+$/.test(value2)) return void 0;
10177
+ if (this.uniqueIds().includes(column) && !this.isValidUniqueId(value2)) return void 0;
10003
10178
  return this.resolveRouteBindingQuery(this.newQuery(), value2, column).first();
10004
10179
  }
10005
10180
  /** Override to customize the binding query (Laravel's resolveRouteBindingQuery()). */
@@ -10217,6 +10392,7 @@ var Model = class _Model {
10217
10392
  if (!this.exists && created && !this.isDirty(created)) this.setAttribute(created, time);
10218
10393
  }
10219
10394
  async performInsert(query) {
10395
+ if (this.usesUniqueIds()) this.setUniqueIds();
10220
10396
  if (await this.fireModelEvent("creating") === false) return false;
10221
10397
  if (this.usesTimestamps()) this.updateTimestamps();
10222
10398
  const attributes = this.getAttributes();
@@ -10400,6 +10576,10 @@ var Model = class _Model {
10400
10576
  if (!instance.getConnectionName()) instance.setConnection(this.getConnectionName());
10401
10577
  return instance;
10402
10578
  }
10579
+ /** Whether the model has a relation method called `name` (Laravel's isRelation()). */
10580
+ isRelation(name) {
10581
+ return !(name in _Model.prototype) && methodOf(this, name) !== void 0;
10582
+ }
10403
10583
  /** Calls the relation method `name` (post.comments()) and checks that it returns a relation. */
10404
10584
  getRelationInstance(name) {
10405
10585
  if (name in _Model.prototype) throw new RelationNotFoundError(this.constructor.name, name);
@@ -10494,9 +10674,18 @@ var Model = class _Model {
10494
10674
  this.appends = [.../* @__PURE__ */ new Set([...this.appends, ...attributes.flat()])];
10495
10675
  return this;
10496
10676
  }
10497
- /** Attributes (through accessors and casts, dates as ISO strings), appends and loaded relations, minus hidden ones. */
10677
+ /**
10678
+ * Attributes (through accessors and casts, dates as ISO strings), appends and loaded relations, minus hidden ones.
10679
+ * A model met again while its own toArray() runs (a child chaperone() pointed back at it) gives its attributes only.
10680
+ */
10498
10681
  toArray() {
10499
- return { ...this.attributesToArray(), ...this.relationsToArray() };
10682
+ if (serializing.has(this)) return this.attributesToArray();
10683
+ serializing.add(this);
10684
+ try {
10685
+ return { ...this.attributesToArray(), ...this.relationsToArray() };
10686
+ } finally {
10687
+ serializing.delete(this);
10688
+ }
10500
10689
  }
10501
10690
  toJSON() {
10502
10691
  return this.toArray();
@@ -10567,7 +10756,35 @@ var Pivot = class extends Model {
10567
10756
  incrementing = false;
10568
10757
  timestamps = false;
10569
10758
  };
10570
- var DatabaseNotification = class extends Model {
10759
+ function uniqueStringIds(Base, generate, valid) {
10760
+ return class WithUniqueStringIds extends Base {
10761
+ usesUniqueIds() {
10762
+ return true;
10763
+ }
10764
+ uniqueIds() {
10765
+ return [this.getKeyName()];
10766
+ }
10767
+ newUniqueId() {
10768
+ return generate();
10769
+ }
10770
+ isValidUniqueId(value2) {
10771
+ return valid(value2);
10772
+ }
10773
+ getKeyType() {
10774
+ return this.uniqueIds().includes(this.getKeyName()) ? "string" : super.getKeyType();
10775
+ }
10776
+ getIncrementing() {
10777
+ return this.uniqueIds().includes(this.getKeyName()) ? false : super.getIncrementing();
10778
+ }
10779
+ };
10780
+ }
10781
+ function HasUuids(Base) {
10782
+ return uniqueStringIds(Base, () => Str.uuid7(), Str.isUuid);
10783
+ }
10784
+ function HasUlids(Base) {
10785
+ return uniqueStringIds(Base, () => Str.ulid().toLowerCase(), Str.isUlid);
10786
+ }
10787
+ var DatabaseNotification = class extends HasUuids(Model) {
10571
10788
  table = "notifications";
10572
10789
  keyType = "string";
10573
10790
  incrementing = false;
@@ -11100,6 +11317,11 @@ async function generateModelTypes(context) {
11100
11317
  await new TypeGenerator({ outputFile, importSource: context.typesImportSource }).write((await context.models()).all());
11101
11318
  return relative2(context.app.basePath(), outputFile);
11102
11319
  }
11320
+ async function writeModelTypes(context, command) {
11321
+ if (context.app.isProduction() || !existsSync4(context.app.basePath("types"))) return;
11322
+ const written = await generateModelTypes(context);
11323
+ if (written) command.line(`Model types written to ${written}.`);
11324
+ }
11103
11325
  async function seedIfRequested(context, command) {
11104
11326
  const seeder = text(command.option("seeder"));
11105
11327
  if (command.option("seed") !== true && seeder === void 0) return;
@@ -11111,10 +11333,7 @@ function registerMigrationCommands(kernel, context) {
11111
11333
  if (!confirmToProceed(context, command)) return 1;
11112
11334
  await migrator(context, command).run(migrationPaths(context, command), { step: command.option("step") === true });
11113
11335
  await seedIfRequested(context, command);
11114
- if (!context.app.isProduction() && existsSync4(context.app.basePath("types"))) {
11115
- const written = await generateModelTypes(context);
11116
- if (written) command.line(`Model types written to ${written}.`);
11117
- }
11336
+ await writeModelTypes(context, command);
11118
11337
  return 0;
11119
11338
  });
11120
11339
  define(kernel, "migrate:install {--database=}", "Create the migration repository", async (command) => {
@@ -11145,12 +11364,14 @@ function registerMigrationCommands(kernel, context) {
11145
11364
  else await instance.reset(paths);
11146
11365
  await instance.run(paths);
11147
11366
  await seedIfRequested(context, command);
11367
+ await writeModelTypes(context, command);
11148
11368
  return 0;
11149
11369
  });
11150
11370
  define(kernel, "migrate:fresh {--database=} {--force} {--path=*} {--seed} {--seeder=}", "Drop all tables and re-run all migrations", async (command) => {
11151
11371
  if (!confirmToProceed(context, command)) return 1;
11152
11372
  await migrator(context, command).fresh(migrationPaths(context, command));
11153
11373
  await seedIfRequested(context, command);
11374
+ await writeModelTypes(context, command);
11154
11375
  return 0;
11155
11376
  });
11156
11377
  define(kernel, "migrate:status {--database=} {--path=*}", "Show the status of each migration", async (command) => {
@@ -12119,6 +12340,10 @@ var Dispatcher2 = class {
12119
12340
  forget(event2) {
12120
12341
  this.listeners.delete(event2);
12121
12342
  }
12343
+ /** Laravel's getRawListeners(): each event's listeners as registered (closures, classes, [class, method]). A copy. */
12344
+ getRawListeners() {
12345
+ return new Map([...this.listeners].map(([event2, list4]) => [event2, [...list4]]));
12346
+ }
12122
12347
  /** Introspection for the event:list command. Not part of the EventDispatcher contract. */
12123
12348
  registeredEvents() {
12124
12349
  const rows = [];
@@ -14678,6 +14903,42 @@ var DailyRotatingStream = class extends Writable {
14678
14903
  stream.end();
14679
14904
  }
14680
14905
  };
14906
+ function destinationCloser(destination) {
14907
+ const opened = new Promise((resolve3) => {
14908
+ const onReady = () => {
14909
+ destination.off("error", onError);
14910
+ resolve3(true);
14911
+ };
14912
+ const onError = () => {
14913
+ destination.off("ready", onReady);
14914
+ destination.flushSync = () => {
14915
+ };
14916
+ resolve3(false);
14917
+ };
14918
+ destination.once("ready", onReady);
14919
+ destination.once("error", onError);
14920
+ });
14921
+ let closing;
14922
+ const close = async () => {
14923
+ if (!await opened) return;
14924
+ await new Promise((resolve3) => {
14925
+ destination.once("close", () => resolve3());
14926
+ destination.once("error", () => resolve3());
14927
+ destination.end();
14928
+ });
14929
+ };
14930
+ return () => closing ??= close();
14931
+ }
14932
+ function writableCloser(stream) {
14933
+ let closing;
14934
+ const close = () => new Promise((resolve3) => {
14935
+ if (stream.writableFinished || stream.destroyed) return resolve3();
14936
+ stream.once("finish", () => resolve3());
14937
+ stream.once("error", () => resolve3());
14938
+ stream.end();
14939
+ });
14940
+ return () => closing ??= close();
14941
+ }
14681
14942
  var PINO_METHOD = {
14682
14943
  emergency: "fatal",
14683
14944
  alert: "fatal",
@@ -14689,12 +14950,25 @@ var PINO_METHOD = {
14689
14950
  debug: "debug"
14690
14951
  };
14691
14952
  var PinoLogger = class extends BaseLogger {
14692
- constructor(pinoInstance, minimumLevel = "debug") {
14953
+ constructor(pinoInstance, minimumLevel = "debug", closeDestination = async () => {
14954
+ }) {
14693
14955
  super(minimumLevel);
14694
14956
  this.pinoInstance = pinoInstance;
14957
+ this.closeDestination = closeDestination;
14695
14958
  }
14696
14959
  pinoInstance;
14960
+ closeDestination;
14961
+ flushing;
14962
+ /**
14963
+ * Writes out every entry still buffered and closes the destination. pino writes asynchronously, so a process
14964
+ * awaits this before it exits (runConsoleAndExit does). The channel takes no entries afterwards: they're dropped.
14965
+ * Not in Laravel, whose handlers write synchronously.
14966
+ */
14967
+ flush() {
14968
+ return this.flushing ??= this.closeDestination();
14969
+ }
14697
14970
  write(level2, message, context) {
14971
+ if (this.flushing) return;
14698
14972
  this.pinoInstance[PINO_METHOD[level2]]({ ...context, rfc5424Level: level2 }, message);
14699
14973
  }
14700
14974
  };
@@ -14703,19 +14977,19 @@ function createSingleChannel(path, minimumLevel = "debug") {
14703
14977
  assertLogLevel(minimumLevel);
14704
14978
  const destination = pino2.destination({ dest: path, mkdir: true, sync: false });
14705
14979
  destination.on("error", (error) => reportLogWriteFailure(path, error));
14706
- return new PinoLogger(pino2(PINO_OPTIONS, destination), minimumLevel);
14980
+ return new PinoLogger(pino2(PINO_OPTIONS, destination), minimumLevel, destinationCloser(destination));
14707
14981
  }
14708
14982
  function createDailyChannel(path, days = 14, minimumLevel = "debug", now5) {
14709
14983
  assertLogLevel(minimumLevel);
14710
14984
  const stream = new DailyRotatingStream(path, days, now5);
14711
- return new PinoLogger(pino2(PINO_OPTIONS, stream), minimumLevel);
14985
+ return new PinoLogger(pino2(PINO_OPTIONS, stream), minimumLevel, writableCloser(stream));
14712
14986
  }
14713
14987
  function createStderrChannel(minimumLevel = "debug") {
14714
14988
  assertLogLevel(minimumLevel);
14715
14989
  const destination = pino2.destination({ dest: 2, sync: false });
14716
14990
  destination.on("error", () => {
14717
14991
  });
14718
- return new PinoLogger(pino2(PINO_OPTIONS, destination), minimumLevel);
14992
+ return new PinoLogger(pino2(PINO_OPTIONS, destination), minimumLevel, destinationCloser(destination));
14719
14993
  }
14720
14994
  var Log = facade(LogManager);
14721
14995
  function logger(message, context) {
@@ -14765,6 +15039,7 @@ var StackLogger = class {
14765
15039
  return this;
14766
15040
  }
14767
15041
  };
15042
+ var isFlushable = (channel) => typeof channel.flush === "function";
14768
15043
  var level = (config2) => config2.level ?? "debug";
14769
15044
  var LogManager2 = class {
14770
15045
  constructor(container) {
@@ -14806,6 +15081,16 @@ var LogManager2 = class {
14806
15081
  stack(channels) {
14807
15082
  return new StackLogger(channels.map((name) => this.channel(name)));
14808
15083
  }
15084
+ /**
15085
+ * Writes out and closes every channel resolved so far, then forgets them, so the next entry opens its file again.
15086
+ * pino writes asynchronously, so a console process awaits this before it exits (runConsoleAndExit does). Not in
15087
+ * Laravel, whose handlers write synchronously.
15088
+ */
15089
+ async flush() {
15090
+ const channels = [...this.channels.values()];
15091
+ this.channels.clear();
15092
+ await Promise.all(channels.filter(isFlushable).map((channel) => channel.flush()));
15093
+ }
14809
15094
  log(logLevel, message, context) {
14810
15095
  this.channel().log(logLevel, message, context);
14811
15096
  }
@@ -15182,6 +15467,9 @@ var PendingMail = class {
15182
15467
  async send(mailable) {
15183
15468
  return this.mailer.send(mailable, this.overrides());
15184
15469
  }
15470
+ async sendNow(mailable) {
15471
+ return this.mailer.sendNow(mailable, this.overrides());
15472
+ }
15185
15473
  async queue(mailable) {
15186
15474
  await this.mailer.queue(mailable, this.overrides());
15187
15475
  }
@@ -15305,9 +15593,9 @@ var MailManager2 = class {
15305
15593
  async queue(mailable) {
15306
15594
  await this.mailer().queue(mailable);
15307
15595
  }
15308
- /** QueuedMailSender: always delivers immediately (see QueuedMailSender.ts for why this must bypass send()). */
15309
- async sendNow(mailerName, mailable, overrides) {
15310
- return this.mailer(mailerName).sendNow(mailable, overrides);
15596
+ /** Sends through the default mailer at once, even when the mailable is queueable (Laravel's Mail::sendNow()). */
15597
+ async sendNow(mailable) {
15598
+ return this.mailer().sendNow(mailable);
15311
15599
  }
15312
15600
  extend(transport, factory) {
15313
15601
  this.drivers.set(transport, factory);
@@ -15330,7 +15618,10 @@ var MailServiceProvider = class extends ServiceProvider {
15330
15618
  "mail"
15331
15619
  );
15332
15620
  this.app.singleton(MailManager, (container) => new MailManager2(container));
15333
- this.app.singleton(QueuedMailSender, (container) => container.make(MailManager));
15621
+ this.app.singleton(QueuedMailSender, (container) => {
15622
+ const manager2 = container.make(MailManager);
15623
+ return { sendNow: (mailerName, mailable, overrides) => manager2.mailer(mailerName).sendNow(mailable, overrides) };
15624
+ });
15334
15625
  this.app.make(ClassRegistry).register(SendQueuedMailable, "Mail.SendQueuedMailable");
15335
15626
  }
15336
15627
  };
@@ -15537,7 +15828,7 @@ var MailNotificationChannel = class {
15537
15828
  const mailable = isMailable(result) ? result : mailableFromMessage(toMailMessage(result, notification.constructor.name), to);
15538
15829
  const container = currentContainer();
15539
15830
  if (!container?.bound(MailManager)) throw new MailUnavailableError();
15540
- await container.make(MailManager).send(mailable);
15831
+ await container.make(MailManager).sendNow(mailable);
15541
15832
  }
15542
15833
  };
15543
15834
  var NOTIFICATIONS_TABLE = "notifications";
@@ -15892,6 +16183,8 @@ import { randomUUID as randomUUID24 } from "crypto";
15892
16183
  import { Redis as Redis2 } from "ioredis";
15893
16184
  import { randomUUID as randomUUID32 } from "crypto";
15894
16185
  import { setTimeout as sleep4 } from "timers/promises";
16186
+ import { spawn as spawnProcess } from "child_process";
16187
+ import { setTimeout as sleep22 } from "timers/promises";
15895
16188
  import { existsSync as existsSync8, readdirSync as readdirSync6, statSync as statSync3 } from "fs";
15896
16189
  import { join as join12 } from "path";
15897
16190
  import { pathToFileURL as pathToFileURL6 } from "url";
@@ -15980,6 +16273,40 @@ function isModel(value2) {
15980
16273
  const candidate = value2;
15981
16274
  return typeof candidate.getKey === "function" && typeof candidate.getKeyName === "function" && typeof candidate.getTable === "function" && typeof candidate.exists === "boolean";
15982
16275
  }
16276
+ var isModelList = (value2) => typeof value2 === "object" && value2 !== null && !isModel(value2) && typeof value2[Symbol.iterator] === "function";
16277
+ function queueableRelations(model, visiting = /* @__PURE__ */ new Set()) {
16278
+ if (visiting.has(model)) return [];
16279
+ visiting.add(model);
16280
+ try {
16281
+ const relations = [];
16282
+ for (const [name, value2] of Object.entries(model.getRelations?.() ?? {})) {
16283
+ if (typeof model[name] !== "function") continue;
16284
+ relations.push(name);
16285
+ for (const nested of nestedRelations(value2, visiting)) relations.push(`${name}.${nested}`);
16286
+ }
16287
+ return [...new Set(relations)];
16288
+ } finally {
16289
+ visiting.delete(model);
16290
+ }
16291
+ }
16292
+ function nestedRelations(value2, visiting) {
16293
+ if (isModel(value2)) return queueableRelations(value2, visiting);
16294
+ if (!isModelList(value2)) return [];
16295
+ const members = [...value2].filter(isModel).map((member) => queueableRelations(member, visiting));
16296
+ if (members.length === 0) return [];
16297
+ return members.reduce((shared2, next) => shared2.filter((path) => next.includes(path)));
16298
+ }
16299
+ async function findForRestoration(model, key, connection) {
16300
+ if (connection !== void 0) {
16301
+ const instance = new model();
16302
+ if (typeof instance.setConnection === "function" && typeof instance.newQueryWithoutScopes === "function") {
16303
+ instance.setConnection(connection);
16304
+ return instance.newQueryWithoutScopes().find(key);
16305
+ }
16306
+ }
16307
+ const query = typeof model.withoutGlobalScopes === "function" ? model.withoutGlobalScopes() : model;
16308
+ return query.find(key);
16309
+ }
15983
16310
  function classNameOf(registry2, cls) {
15984
16311
  const existing = registry2.nameOf(cls);
15985
16312
  if (existing !== void 0) return existing;
@@ -16049,7 +16376,12 @@ function serializeValue(value2, registry2, path = "data", seen = /* @__PURE__ */
16049
16376
  if (!object.exists) {
16050
16377
  throw new JobSerializationError(`Cannot serialize [${path}]: the [${name2}] model hasn't been saved, so it has no key to store.`);
16051
16378
  }
16052
- return { [TAG]: "model", class: name2, key: serializeValue(object.getKey(), registry2, `${path}.key`, seen) ?? null };
16379
+ const encoded = { [TAG]: "model", class: name2, key: serializeValue(object.getKey(), registry2, `${path}.key`, seen) ?? null };
16380
+ const connection = object.getConnectionName?.();
16381
+ if (connection !== void 0) encoded.connection = connection;
16382
+ const relations = queueableRelations(object);
16383
+ if (relations.length > 0) encoded.relations = relations;
16384
+ return encoded;
16053
16385
  }
16054
16386
  if (isPlainObject2(object)) {
16055
16387
  const fields = serializeFields(object, registry2, path, seen);
@@ -16105,9 +16437,10 @@ async function restoreValue(value2, registry2) {
16105
16437
  const name = value2.class;
16106
16438
  const model = resolveClass(registry2, name);
16107
16439
  const key = await restoreValue(value2.key, registry2);
16108
- const query = typeof model.withoutGlobalScopes === "function" ? model.withoutGlobalScopes() : model;
16109
- const found = await query.find(key);
16440
+ const found = await findForRestoration(model, key, typeof value2.connection === "string" ? value2.connection : void 0);
16110
16441
  if (found === void 0 || found === null) throw new MissingModelError(name, [key]);
16442
+ const relations = Array.isArray(value2.relations) ? value2.relations.map(String) : [];
16443
+ if (relations.length > 0) await (found.loadMissing ?? found.load)?.call(found, ...relations);
16111
16444
  return found;
16112
16445
  }
16113
16446
  case "instance": {
@@ -17224,7 +17557,7 @@ var DatabaseBatchRepository = class {
17224
17557
  var defaultWorkerOptions = () => ({
17225
17558
  name: "default",
17226
17559
  backoff: 0,
17227
- memory: 128,
17560
+ memory: 512,
17228
17561
  timeout: 60,
17229
17562
  sleep: 3,
17230
17563
  maxTries: 1,
@@ -17482,6 +17815,142 @@ var Worker = class _Worker {
17482
17815
  }
17483
17816
  }
17484
17817
  };
17818
+ var defaultListenerOptions = () => ({
17819
+ name: "default",
17820
+ backoff: 0,
17821
+ memory: 512,
17822
+ timeout: 60,
17823
+ sleep: 3,
17824
+ maxTries: 1,
17825
+ rest: 0,
17826
+ stopWhenEmpty: false
17827
+ });
17828
+ function lineSplitter(emit) {
17829
+ let rest = "";
17830
+ return {
17831
+ push(chunk) {
17832
+ const lines = (rest + chunk).split("\n");
17833
+ rest = lines.pop() ?? "";
17834
+ for (const line of lines) emit(line.endsWith("\r") ? line.slice(0, -1) : line);
17835
+ },
17836
+ end() {
17837
+ if (rest !== "") emit(rest);
17838
+ rest = "";
17839
+ }
17840
+ };
17841
+ }
17842
+ function currentRuntime() {
17843
+ const script = process.argv[1];
17844
+ if (script === void 0) throw new QueueError("queue:listen can't tell which script started this process, so it can't start workers.");
17845
+ return { command: process.execPath, args: [...process.execArgv, script] };
17846
+ }
17847
+ var Listener = class _Listener {
17848
+ constructor(manager2, config2) {
17849
+ this.manager = manager2;
17850
+ this.config = config2;
17851
+ }
17852
+ manager;
17853
+ config;
17854
+ static EXIT_SUCCESS = 0;
17855
+ shouldQuit = false;
17856
+ child;
17857
+ resting;
17858
+ /** Runs until stopped, the queue is empty (with stopWhenEmpty) or the memory limit is reached. Returns the exit code. */
17859
+ async listen(connection, queue, options) {
17860
+ const stopListening = this.listenForSignals();
17861
+ try {
17862
+ const command = this.makeCommand(connection, queue, options);
17863
+ while (!this.shouldQuit) {
17864
+ if (options.stopWhenEmpty && await this.isEmpty(connection, queue)) break;
17865
+ if (this.shouldQuit) break;
17866
+ await this.runProcess(command, options.timeout);
17867
+ if (this.shouldQuit || this.memoryExceeded(options.memory)) break;
17868
+ if (options.rest > 0) await this.rest(options.rest);
17869
+ }
17870
+ return _Listener.EXIT_SUCCESS;
17871
+ } finally {
17872
+ stopListening();
17873
+ }
17874
+ }
17875
+ /** Laravel's createCommand(): the runtime, the CLI script and `queue:work <connection> --once …`, plus `--timeout`. */
17876
+ makeCommand(connection, queue, options) {
17877
+ const { command, args } = this.config.runtime ?? currentRuntime();
17878
+ const backoff = Array.isArray(options.backoff) ? options.backoff.join(",") : String(options.backoff);
17879
+ return [
17880
+ command,
17881
+ ...args,
17882
+ "queue:work",
17883
+ connection,
17884
+ "--once",
17885
+ `--name=${options.name}`,
17886
+ `--queue=${queue}`,
17887
+ `--backoff=${backoff}`,
17888
+ `--memory=${options.memory}`,
17889
+ `--sleep=${options.sleep}`,
17890
+ `--tries=${options.maxTries}`,
17891
+ `--timeout=${options.timeout}`
17892
+ ];
17893
+ }
17894
+ /** Stops after the running process, which gets the same signal (SIGTERM/SIGINT/SIGQUIT call this). */
17895
+ stop(signal = "SIGTERM") {
17896
+ this.shouldQuit = true;
17897
+ this.child?.kill(signal);
17898
+ this.resting?.abort();
17899
+ }
17900
+ memoryExceeded(limitMb) {
17901
+ return limitMb > 0 && process.memoryUsage().heapUsed / 1024 / 1024 >= limitMb;
17902
+ }
17903
+ listenForSignals() {
17904
+ const signals = ["SIGTERM", "SIGINT", "SIGQUIT"];
17905
+ const handlers = signals.map((signal) => [signal, () => this.stop(signal)]);
17906
+ for (const [signal, handler3] of handlers) process.on(signal, handler3);
17907
+ return () => {
17908
+ for (const [signal, handler3] of handlers) process.off(signal, handler3);
17909
+ };
17910
+ }
17911
+ async isEmpty(connection, queue) {
17912
+ const queues = this.manager.connection(connection);
17913
+ for (const name of queue.split(",")) if (await queues.size(name.trim()) > 0) return false;
17914
+ return true;
17915
+ }
17916
+ runProcess([command, ...args], timeout) {
17917
+ const output = this.config.output ?? (() => {
17918
+ });
17919
+ return new Promise((resolve3, reject) => {
17920
+ const child = (this.config.spawn ?? spawnProcess)(command, args, { cwd: this.config.commandPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
17921
+ this.child = child;
17922
+ const stdout = lineSplitter((line) => output("out", line));
17923
+ const stderr = lineSplitter((line) => output("err", line));
17924
+ child.stdout?.setEncoding("utf8").on("data", (chunk) => stdout.push(chunk));
17925
+ child.stderr?.setEncoding("utf8").on("data", (chunk) => stderr.push(chunk));
17926
+ const timer = timeout > 0 ? setTimeout(() => {
17927
+ output("err", `The process "${[command, ...args].join(" ")}" exceeded the timeout of ${timeout} seconds.`);
17928
+ child.kill("SIGKILL");
17929
+ }, timeout * 1e3) : void 0;
17930
+ let settled = false;
17931
+ const settle = () => {
17932
+ if (settled) return false;
17933
+ settled = true;
17934
+ clearTimeout(timer);
17935
+ this.child = void 0;
17936
+ stdout.end();
17937
+ stderr.end();
17938
+ return true;
17939
+ };
17940
+ child.once("error", (error) => settle() && reject(error));
17941
+ child.once("close", () => settle() && resolve3());
17942
+ });
17943
+ }
17944
+ async rest(seconds) {
17945
+ this.resting = new AbortController();
17946
+ try {
17947
+ await sleep22(seconds * 1e3, void 0, { signal: this.resting.signal });
17948
+ } catch {
17949
+ } finally {
17950
+ this.resting = void 0;
17951
+ }
17952
+ }
17953
+ };
17485
17954
  var EXTENSIONS2 = [".ts", ".js", ".mts", ".mjs"];
17486
17955
  function files2(directory) {
17487
17956
  const found = [];
@@ -17520,6 +17989,22 @@ function localTimestamp(date = /* @__PURE__ */ new Date()) {
17520
17989
  const pad2 = (n) => String(n).padStart(2, "0");
17521
17990
  return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`;
17522
17991
  }
17992
+ function processingMessage(queue) {
17993
+ return `Processing jobs from the [${queue}] ${Str.plural("queue", queue.split(",").length)}.`;
17994
+ }
17995
+ function listenerOptionsFrom(option) {
17996
+ const defaults = defaultListenerOptions();
17997
+ return {
17998
+ name: text2(option("name")) ?? defaults.name,
17999
+ backoff: text2(option("backoff")) ?? defaults.backoff,
18000
+ memory: number2(option("memory"), defaults.memory),
18001
+ timeout: number2(option("timeout"), defaults.timeout),
18002
+ sleep: number2(option("sleep"), defaults.sleep),
18003
+ maxTries: number2(option("tries"), defaults.maxTries),
18004
+ rest: number2(option("rest"), defaults.rest),
18005
+ stopWhenEmpty: option("stop-when-empty") === true
18006
+ };
18007
+ }
17523
18008
  function workerOptionsFrom(option) {
17524
18009
  const defaults = defaultWorkerOptions();
17525
18010
  return {
@@ -17550,7 +18035,7 @@ function registerQueueCommands(kernel, context) {
17550
18035
  return false;
17551
18036
  };
17552
18037
  define2(
17553
- "queue:work {connection? : The name of the queue connection to work} {--name=default : The name of the worker} {--queue= : The names of the queues to work} {--once : Only process the next job on the queue} {--stop-when-empty : Stop when the queue is empty} {--backoff=0 : Seconds to wait before retrying a job that failed} {--max-jobs=0 : The number of jobs to process before stopping} {--max-time=0 : The maximum number of seconds the worker should run} {--memory=128 : The memory limit in megabytes} {--sleep=3 : Seconds to sleep when no job is available} {--rest=0 : Seconds to rest between jobs} {--timeout=60 : The number of seconds a child process can run} {--tries=1 : Number of times to attempt a job before logging it failed}",
18038
+ "queue:work {connection? : The name of the queue connection to work} {--name=default : The name of the worker} {--queue= : The names of the queues to work} {--once : Only process the next job on the queue} {--stop-when-empty : Stop when the queue is empty} {--backoff=0 : Seconds to wait before retrying a job that failed} {--max-jobs=0 : The number of jobs to process before stopping} {--max-time=0 : The maximum number of seconds the worker should run} {--memory=512 : The memory limit in megabytes, measured as V8 heap in use (a Node app on tsx uses about 115 MB at idle)} {--sleep=3 : Seconds to sleep when no job is available} {--rest=0 : Seconds to rest between jobs} {--timeout=60 : The number of seconds a child process can run} {--tries=1 : Number of times to attempt a job before logging it failed}",
17554
18039
  "Start processing jobs on the queue as a daemon",
17555
18040
  async (out) => {
17556
18041
  const connection = text2(out.argument("connection")) ?? defaultConnection();
@@ -17567,13 +18052,34 @@ function registerQueueCommands(kernel, context) {
17567
18052
  exception: (error, job) => out.error(`${job ? `${name(job)}: ` : ""}${errorClass(error)}`)
17568
18053
  });
17569
18054
  const options = workerOptionsFrom((option) => out.option(option));
17570
- out.info(`Processing jobs from the [${queue}] ${Str.plural("queue", queue.split(",").length)}.`);
18055
+ out.info(processingMessage(queue));
17571
18056
  if (out.option("once") === true) {
17572
18057
  return worker.runNextJob(connection, queue, options);
17573
18058
  }
17574
18059
  return worker.daemon(connection, queue, options);
17575
18060
  }
17576
18061
  );
18062
+ define2(
18063
+ "queue:listen {connection? : The name of connection} {--name=default : The name of the worker} {--backoff=0 : The number of seconds to wait before retrying a job that encountered an uncaught exception} {--memory=512 : The memory limit in megabytes, measured as V8 heap in use (a Node app on tsx uses about 115 MB at idle)} {--queue= : The queue to listen on} {--sleep=3 : Number of seconds to sleep when no job is available} {--rest=0 : Number of seconds to rest between jobs} {--timeout=60 : The number of seconds a child process can run} {--tries=1 : Number of times to attempt a job before logging it failed} {--stop-when-empty : Stop when the queue is empty}",
18064
+ "Listen to a given queue",
18065
+ async (out) => {
18066
+ const connection = text2(out.argument("connection")) ?? defaultConnection();
18067
+ const queue = text2(out.option("queue")) ?? defaultQueue(connection);
18068
+ const banner = processingMessage(queue);
18069
+ out.info(banner);
18070
+ const listener = new Listener(container.make(QueueManager), {
18071
+ commandPath: context.app.basePath(),
18072
+ spawn: context.spawn,
18073
+ // Each `queue:work` starts with the same line as this command; it's shown once.
18074
+ output: (type, line) => {
18075
+ if (line === banner) return;
18076
+ if (type === "out") out.line(line);
18077
+ else out.error(line);
18078
+ }
18079
+ });
18080
+ return listener.listen(connection, queue, listenerOptionsFrom((option) => out.option(option)));
18081
+ }
18082
+ );
17577
18083
  define2("queue:restart", "Restart queue worker daemons after their current job", async (out) => {
17578
18084
  if (!container.bound(CacheManager)) {
17579
18085
  out.error("queue:restart needs a cache store. Register the CacheServiceProvider.");
@@ -18040,6 +18546,32 @@ var RouteDefinition = class {
18040
18546
  this.#compiled = void 0;
18041
18547
  return this;
18042
18548
  }
18549
+ /** Laravel's whereAlpha(): ASCII letters only. One parameter name or several. */
18550
+ whereAlpha(parameters) {
18551
+ return this.#assign(parameters, "[a-zA-Z]+");
18552
+ }
18553
+ /** Laravel's whereAlphaNumeric(): ASCII letters and digits. */
18554
+ whereAlphaNumeric(parameters) {
18555
+ return this.#assign(parameters, "[a-zA-Z0-9]+");
18556
+ }
18557
+ /** Laravel's whereNumber(): digits only. */
18558
+ whereNumber(parameters) {
18559
+ return this.#assign(parameters, "[0-9]+");
18560
+ }
18561
+ /** Laravel's whereUlid(): 26 Crockford base-32 characters, the first at most 7. */
18562
+ whereUlid(parameters) {
18563
+ return this.#assign(parameters, "[0-7][0-9a-hjkmnp-tv-zA-HJKMNP-TV-Z]{25}");
18564
+ }
18565
+ /** Laravel's whereUuid(): five groups of hex digits (8-4-4-4-12), any version. */
18566
+ whereUuid(parameters) {
18567
+ return this.#assign(parameters, "[\\da-fA-F]{8}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{12}");
18568
+ }
18569
+ /** Laravel's whereIn(): one of the values. Unlike Laravel, each value is matched literally (`c++` is not a pattern). */
18570
+ whereIn(parameters, values) {
18571
+ const first = typeof parameters === "string" ? parameters : parameters[0];
18572
+ if (values.length === 0 && first !== void 0) throw new Error(`Routing requirement for "${first}" cannot be empty.`);
18573
+ return this.#assign(parameters, values.map((value2) => escapeRegex(String(value2))).join("|"));
18574
+ }
18043
18575
  gatherMiddleware() {
18044
18576
  return [...this.#middleware];
18045
18577
  }
@@ -18070,6 +18602,10 @@ var RouteDefinition = class {
18070
18602
  if (this.uri === "/") return [];
18071
18603
  return this.uri.split("/").map((segment) => WHOLE_OPTIONAL.test(segment) ? 2 : segment.includes("{") ? 1 : 0);
18072
18604
  }
18605
+ #assign(parameters, pattern) {
18606
+ const names = typeof parameters === "string" ? [parameters] : parameters;
18607
+ return this.where(Object.fromEntries(names.map((name) => [name, pattern])));
18608
+ }
18073
18609
  #regex(patterns, version) {
18074
18610
  if (this.#compiled !== void 0 && this.#compiled.version === version) return this.#compiled.regex;
18075
18611
  const wheres = { ...patterns, ...this.#wheres };
@@ -20887,6 +21423,8 @@ export {
20887
21423
  HasManyThrough,
20888
21424
  HasOne,
20889
21425
  HasOneOrMany,
21426
+ HasUlids,
21427
+ HasUuids,
20890
21428
  Hash,
20891
21429
  HashAlgorithmError,
20892
21430
  HashManager,
@@ -21051,6 +21589,7 @@ export {
21051
21589
  Queue2 as Queue,
21052
21590
  QueueBase,
21053
21591
  QueueError,
21592
+ Listener as QueueListener,
21054
21593
  QueueManager2 as QueueManager,
21055
21594
  QueueServiceProvider,
21056
21595
  QueueUnavailableError,
@@ -21085,7 +21624,8 @@ export {
21085
21624
  S3Filesystem,
21086
21625
  SSR_RENDER_FAILED,
21087
21626
  STORAGE_LINK_SIGNATURE,
21088
- Schedule4 as Schedule,
21627
+ Schedule5 as Schedule,
21628
+ ScheduleClearCacheCommand,
21089
21629
  ScheduleImpl,
21090
21630
  ScheduleListCommand,
21091
21631
  ScheduleNameRequiredError,
@@ -21117,6 +21657,7 @@ export {
21117
21657
  StartSession,
21118
21658
  Storage2 as Storage,
21119
21659
  Str,
21660
+ Stringable,
21120
21661
  SyncJob,
21121
21662
  SyncQueue,
21122
21663
  TagsNotSupportedError,
@@ -21225,6 +21766,7 @@ export {
21225
21766
  decodePayload,
21226
21767
  decrypt,
21227
21768
  defaultAttributeName,
21769
+ defaultListenerOptions,
21228
21770
  defaultLockFactory,
21229
21771
  defaultProviders,
21230
21772
  defaultWorkerOptions,
@@ -21290,6 +21832,7 @@ export {
21290
21832
  isUploadedFileLike,
21291
21833
  joinDiskPath,
21292
21834
  keyGenerateHandler,
21835
+ listenerOptionsFrom,
21293
21836
  live,
21294
21837
  loadRelations,
21295
21838
  loadRoutesFrom,