@zerotal/queue 1.12.0 → 1.13.1

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/CHANGELOG.md CHANGED
@@ -8,6 +8,26 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.13.1] — 2026-08-31
12
+
13
+ ### Added
14
+
15
+ - **`Job.jobName`** — a declared name for the queue payload, so renaming a job class is free.
16
+
17
+ A queued job is a persisted reference to a class: the payload carries a string and the
18
+ worker resolves the class by it, so a job enqueued yesterday is looked up by today's
19
+ process. The class name was therefore a compatibility surface that nothing said was one —
20
+ renaming a job class silently invalidated every job already enqueued under the old name,
21
+ and the failure landed on the deploy rather than on the change. A test never sees it,
22
+ because a test enqueues and runs in the same process.
23
+
24
+ `static jobName` decouples the stored identity from the class, exactly as `Migration.id`
25
+ does for a migration's filename, and for the same reason. It defaults to the class name, so
26
+ a job that declares nothing behaves as before.
27
+
28
+ Found while sizing a suite-wide rename of `@internal` exports: three `Job` classes were on
29
+ that list, and renaming them would have been a data migration wearing a refactor's clothes.
30
+
11
31
  ## [1.9.0] — 2026-08-29
12
32
 
13
33
  ### Documented
package/api-surface.md CHANGED
@@ -31,6 +31,7 @@ class Bus = {
31
31
 
32
32
  class Job = {
33
33
  new (): Job
34
+ static jobName?: string
34
35
  _chain: SerializedJob[] | undefined
35
36
  batchId: string | undefined
36
37
  className: string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/queue",
3
- "version": "1.12.0",
3
+ "version": "1.13.1",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -31,8 +31,8 @@
31
31
  "typecheck": "tsc --noEmit"
32
32
  },
33
33
  "dependencies": {
34
- "@zerotal/core": "1.12.0",
35
- "@zerotal/orm": "1.12.0"
34
+ "@zerotal/core": "1.13.1",
35
+ "@zerotal/orm": "1.13.1"
36
36
  },
37
37
  "devDependencies": {
38
38
  "typescript": "^5.8.0"
package/src/Job.ts CHANGED
@@ -81,10 +81,39 @@ export abstract class Job {
81
81
  }
82
82
 
83
83
  /**
84
- * The class name used as the serialization key.
85
- * Must match what JobRegistry.register() stores.
84
+ * The name this job is stored and resolved under, when it should not be the
85
+ * class name.
86
+ *
87
+ * A queued job is a *persisted* reference to a class: the payload in the
88
+ * database or in Redis carries a string, and the worker looks the class up by
89
+ * it. So the class name is a wire identifier, not just a source symbol — and
90
+ * renaming the class silently invalidates every job already enqueued under the
91
+ * old name. The failure lands on the deploy rather than on the change, and a
92
+ * test never sees it, because a test enqueues and runs in the same process.
93
+ *
94
+ * Declaring the name decouples the two, exactly as
95
+ * {@link @zerotal/orm!Migration.id} does for a migration's filename:
96
+ *
97
+ * ```ts
98
+ * export class SendWelcomeEmail extends Job {
99
+ * static override jobName = "SendWelcomeEmail"; // survives a class rename
100
+ * }
101
+ * ```
102
+ *
103
+ * It also survives a build that mangles names. `zt compile` does not minify
104
+ * today, so this is not a live hazard — but nothing about the registry said it
105
+ * depended on that, which is the kind of assumption worth writing down before
106
+ * it is discovered.
107
+ *
108
+ * Defaults to the class name, so nothing changes for a job that does not set it.
109
+ */
110
+ static jobName?: string;
111
+
112
+ /**
113
+ * The name used as the serialization key. Matches what `JobRegistry.register()`
114
+ * stores: {@link Job.jobName} when declared, the class name otherwise.
86
115
  */
87
116
  get className(): string {
88
- return this.constructor.name;
117
+ return (this.constructor as typeof Job).jobName ?? this.constructor.name;
89
118
  }
90
119
  }
@@ -1,17 +1,22 @@
1
1
  import type { Job } from "./Job.ts";
2
2
 
3
- type JobClass = new (...args: never[]) => Job;
3
+ type JobClass = (new (...args: never[]) => Job) & { jobName?: string };
4
4
 
5
5
  /**
6
- * Global registry mapping job class names to their constructors.
6
+ * Global registry mapping a job's stored name to its constructor.
7
7
  * Required for deserialization — when a job is popped from the queue,
8
8
  * the worker looks up the class here to instantiate it.
9
+ *
10
+ * The key is {@link Job.jobName} when a job declares one, and the class name
11
+ * otherwise. That distinction exists because the key is written into a persisted
12
+ * payload: a job enqueued yesterday is resolved by today's process, so the name
13
+ * is a compatibility surface and renaming the class is a migration.
9
14
  */
10
15
  export const JobRegistry = {
11
16
  _map: new Map<string, JobClass>(),
12
17
 
13
18
  register(JobClass: JobClass): void {
14
- JobRegistry._map.set(JobClass.name, JobClass);
19
+ JobRegistry._map.set(JobClass.jobName ?? JobClass.name, JobClass);
15
20
  },
16
21
 
17
22
  resolve(className: string): JobClass | undefined {