@c9up/bay 0.1.12 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +172 -1
  2. package/dist/BayProvider.d.ts +55 -33
  3. package/dist/BayProvider.d.ts.map +1 -1
  4. package/dist/BayProvider.js +81 -10
  5. package/dist/BayProvider.js.map +1 -1
  6. package/dist/Job.d.ts +99 -0
  7. package/dist/Job.d.ts.map +1 -0
  8. package/dist/Job.js +78 -0
  9. package/dist/Job.js.map +1 -0
  10. package/dist/QueueManager.d.ts +147 -22
  11. package/dist/QueueManager.d.ts.map +1 -1
  12. package/dist/QueueManager.js +290 -52
  13. package/dist/QueueManager.js.map +1 -1
  14. package/dist/adapters.d.ts +68 -0
  15. package/dist/adapters.d.ts.map +1 -0
  16. package/dist/adapters.js +56 -0
  17. package/dist/adapters.js.map +1 -0
  18. package/dist/augmentations.d.ts +28 -0
  19. package/dist/augmentations.d.ts.map +1 -0
  20. package/dist/augmentations.js +17 -0
  21. package/dist/augmentations.js.map +1 -0
  22. package/dist/configure.d.ts +19 -0
  23. package/dist/configure.d.ts.map +1 -0
  24. package/dist/configure.js +48 -0
  25. package/dist/configure.js.map +1 -0
  26. package/dist/console/contract.d.ts +60 -0
  27. package/dist/console/contract.d.ts.map +1 -0
  28. package/dist/console/contract.js +36 -0
  29. package/dist/console/contract.js.map +1 -0
  30. package/dist/console/index.d.ts +29 -0
  31. package/dist/console/index.d.ts.map +1 -0
  32. package/dist/console/index.js +45 -0
  33. package/dist/console/index.js.map +1 -0
  34. package/dist/console/makeJob.d.ts +32 -0
  35. package/dist/console/makeJob.d.ts.map +1 -0
  36. package/dist/console/makeJob.js +118 -0
  37. package/dist/console/makeJob.js.map +1 -0
  38. package/dist/console/queueWork.d.ts +18 -0
  39. package/dist/console/queueWork.d.ts.map +1 -0
  40. package/dist/console/queueWork.js +58 -0
  41. package/dist/console/queueWork.js.map +1 -0
  42. package/dist/drivers/MemoryDriver.d.ts +14 -8
  43. package/dist/drivers/MemoryDriver.d.ts.map +1 -1
  44. package/dist/drivers/MemoryDriver.js +61 -7
  45. package/dist/drivers/MemoryDriver.js.map +1 -1
  46. package/dist/drivers/RedisDriver.d.ts +66 -8
  47. package/dist/drivers/RedisDriver.d.ts.map +1 -1
  48. package/dist/drivers/RedisDriver.js +257 -45
  49. package/dist/drivers/RedisDriver.js.map +1 -1
  50. package/dist/index.d.ts +10 -5
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +8 -3
  53. package/dist/index.js.map +1 -1
  54. package/dist/jobs.d.ts +43 -0
  55. package/dist/jobs.d.ts.map +1 -0
  56. package/dist/jobs.js +105 -0
  57. package/dist/jobs.js.map +1 -0
  58. package/dist/nodeEnv.d.ts +16 -0
  59. package/dist/nodeEnv.d.ts.map +1 -0
  60. package/dist/nodeEnv.js +32 -0
  61. package/dist/nodeEnv.js.map +1 -0
  62. package/dist/quasar.d.ts +1 -1
  63. package/dist/quasar.js +1 -1
  64. package/dist/services/main.d.ts +5 -0
  65. package/dist/services/main.d.ts.map +1 -1
  66. package/dist/services/main.js +7 -0
  67. package/dist/services/main.js.map +1 -1
  68. package/dist/testing/FakeQueue.d.ts +15 -9
  69. package/dist/testing/FakeQueue.d.ts.map +1 -1
  70. package/dist/testing/FakeQueue.js +13 -3
  71. package/dist/testing/FakeQueue.js.map +1 -1
  72. package/package.json +9 -3
  73. package/src/BayProvider.ts +143 -25
  74. package/src/Job.ts +137 -0
  75. package/src/QueueManager.ts +454 -56
  76. package/src/adapters.ts +75 -0
  77. package/src/augmentations.ts +31 -0
  78. package/src/configure.ts +63 -0
  79. package/src/console/contract.ts +94 -0
  80. package/src/console/index.ts +68 -0
  81. package/src/console/makeJob.ts +139 -0
  82. package/src/console/queueWork.ts +70 -0
  83. package/src/drivers/MemoryDriver.ts +66 -14
  84. package/src/drivers/RedisDriver.ts +366 -63
  85. package/src/index.ts +35 -5
  86. package/src/jobs.ts +111 -0
  87. package/src/nodeEnv.ts +30 -0
  88. package/src/quasar.ts +1 -1
  89. package/src/services/main.ts +8 -0
  90. package/src/testing/FakeQueue.ts +25 -15
package/src/jobs.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Finding the job classes an application wrote.
3
+ *
4
+ * A job class carries its own name, and a worker resolves a queued record by
5
+ * that name — so the worker process has to have imported the class. Written by
6
+ * hand that is a registration list to keep in step with a directory:
7
+ *
8
+ * queue.registerJob(SendEmail)
9
+ * queue.registerJob(SendInvoice) // …and the one nobody added
10
+ *
11
+ * `locations` in `config/queue.ts` is the directory instead. Every module under
12
+ * it is imported once at boot, and a default export that is a job class is
13
+ * registered under its own name.
14
+ *
15
+ * Directories, not globs. `'./app/jobs/**\/*.{ts,js}'` — the spelling upstream's
16
+ * config uses — is accepted and read as the directory it starts with, so a
17
+ * config copied from there works; bay ships no glob engine and adding a
18
+ * dependency for one path shape is not worth it.
19
+ */
20
+
21
+ import * as fsp from "node:fs/promises";
22
+ import * as path from "node:path";
23
+ import { pathToFileURL } from "node:url";
24
+ import { isJobClass, type JobClass } from "./Job.js";
25
+
26
+ /** Where `make:job` writes, and where discovery looks when nothing is declared. */
27
+ export const DEFAULT_JOBS_DIR = "app/jobs";
28
+
29
+ let jobsDir = DEFAULT_JOBS_DIR;
30
+
31
+ /** @internal Told by the provider what the config declared. */
32
+ export function setJobsDir(dir: string): void {
33
+ jobsDir = dir;
34
+ }
35
+
36
+ /** Where job files live — the first `locations` entry, or the default. */
37
+ export function getJobsDir(): string {
38
+ return jobsDir;
39
+ }
40
+
41
+ /**
42
+ * The directory a `locations` entry names.
43
+ *
44
+ * Everything from the first glob character on is dropped: `app/jobs/** /*.ts`
45
+ * and `app/jobs` name the same directory, and the walk below is recursive
46
+ * either way.
47
+ */
48
+ export function directoryOf(location: string): string {
49
+ const withoutGlob = location.split(/[*?[{]/)[0] ?? location;
50
+ const trimmed = withoutGlob.replace(/\/+$/, "");
51
+ return trimmed.replace(/^\.\//, "") || ".";
52
+ }
53
+
54
+ /** Every module file under `dir`, recursively. */
55
+ async function walk(dir: string, depth = 0): Promise<string[]> {
56
+ // A jobs directory is a flat convention with the occasional subdirectory;
57
+ // an unbounded walk would follow whatever happens to live under it.
58
+ if (depth > 8) return [];
59
+ let entries: import("node:fs").Dirent[];
60
+ try {
61
+ entries = await fsp.readdir(dir, { withFileTypes: true });
62
+ } catch {
63
+ // A declared directory that does not exist yet is not an error: a project
64
+ // can name where its jobs will go before writing the first one.
65
+ return [];
66
+ }
67
+ const found: string[] = [];
68
+ for (const entry of entries) {
69
+ const full = path.join(dir, entry.name);
70
+ if (entry.isDirectory()) {
71
+ found.push(...(await walk(full, depth + 1)));
72
+ continue;
73
+ }
74
+ // `.d.ts` is a declaration, not a module with a job in it.
75
+ if (/\.d\.[cm]?ts$/.test(entry.name)) continue;
76
+ if (/\.[cm]?[jt]s$/.test(entry.name)) found.push(full);
77
+ }
78
+ return found.sort();
79
+ }
80
+
81
+ /**
82
+ * Import every module under `locations` and return the job classes they
83
+ * default-export.
84
+ *
85
+ * A module that throws on import is reported and skipped: one unfinished job
86
+ * file must not stop the worker from running every other job.
87
+ */
88
+ export async function discoverJobs(
89
+ locations: readonly string[],
90
+ ): Promise<JobClass[]> {
91
+ const found: JobClass[] = [];
92
+ for (const location of locations) {
93
+ for (const file of await walk(directoryOf(location))) {
94
+ let module: unknown;
95
+ try {
96
+ module = await import(pathToFileURL(path.resolve(file)).href);
97
+ } catch (err) {
98
+ process.stderr.write(
99
+ `[bay] could not load '${file}': ${
100
+ err instanceof Error ? err.message : String(err)
101
+ }\n`,
102
+ );
103
+ continue;
104
+ }
105
+ if (typeof module !== "object" || module === null) continue;
106
+ const exported = Reflect.get(module, "default");
107
+ if (isJobClass(exported)) found.push(exported);
108
+ }
109
+ }
110
+ return found;
111
+ }
package/src/nodeEnv.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Reading `NODE_ENV`, with the aliases people actually set.
3
+ *
4
+ * `NODE_ENV=prod` is ordinary in a Dockerfile or a platform dashboard. Read
5
+ * verbatim it answers "not production" — and here that decides whether a queue
6
+ * silently accepts a delivery guarantee weaker than the one it advertises.
7
+ *
8
+ * Duplicated rather than imported: bay depends on no other package in this
9
+ * workspace, and a safety decision that only holds when an optional peer is
10
+ * installed is not a decision.
11
+ */
12
+
13
+ const DEV_ENVS = ["dev", "develop", "development"];
14
+ const PROD_ENVS = ["prod", "production"];
15
+ const TEST_ENVS = ["test", "testing"];
16
+
17
+ /** The canonical name for whatever `NODE_ENV` holds. */
18
+ export function normalizeNodeEnv(value: string | undefined): string {
19
+ if (!value || typeof value !== "string") return "unknown";
20
+ const env = value.toLowerCase();
21
+ if (DEV_ENVS.includes(env)) return "development";
22
+ if (PROD_ENVS.includes(env)) return "production";
23
+ if (TEST_ENVS.includes(env)) return "test";
24
+ return env;
25
+ }
26
+
27
+ /** Whether this process is running in production, under any spelling. */
28
+ export function inProduction(): boolean {
29
+ return normalizeNodeEnv(process.env.NODE_ENV) === "production";
30
+ }
package/src/quasar.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Bay does not depend on quasar: it is an optional peer, and this module
5
5
  * never imports it statically. The specifier is built at runtime so the
6
- * TypeScript build stays free of it too — a hard type import would make echo
6
+ * TypeScript build stays free of it too — a hard type import would make bay
7
7
  * unbuildable for anyone who queues in memory.
8
8
  *
9
9
  * The shape is checked before use rather than asserted, the same way bay
@@ -27,6 +27,14 @@ export function getQueue(): QueueManager | undefined {
27
27
  return instance;
28
28
  }
29
29
 
30
+ /**
31
+ * @internal Release the singleton, so a shut-down application does not leave a
32
+ * dead queue reachable through `services/main`.
33
+ */
34
+ export function clearQueue(): void {
35
+ instance = undefined;
36
+ }
37
+
30
38
  const queue: QueueManager = new Proxy({} as QueueManager, {
31
39
  get(_target, prop) {
32
40
  // A module loader inspects what it imports before anyone uses it: it reads
@@ -9,7 +9,7 @@
9
9
  * the fake into a runtime build.
10
10
  */
11
11
 
12
- import type { Job, QueueDriver } from "../QueueManager.js";
12
+ import type { JobRecord, QueueDriver } from "../QueueManager.js";
13
13
 
14
14
  export interface FakeQueuePredicate {
15
15
  /** Custom payload predicate — receives the job's `payload` and
@@ -23,12 +23,12 @@ export interface FakeQueuePredicate {
23
23
 
24
24
  export type FakeQueuePredicateArg =
25
25
  | FakeQueuePredicate
26
- | ((job: Job) => boolean);
26
+ | ((job: JobRecord) => boolean);
27
27
 
28
28
  export class FakeQueue implements QueueDriver {
29
- #pushed: Job[] = [];
29
+ #pushed: JobRecord[] = [];
30
30
 
31
- async push(job: Job): Promise<void> {
31
+ async push(job: JobRecord): Promise<void> {
32
32
  // Reject duplicate ids to surface the most common test-fixture
33
33
  // mistake — two `makeJob({ id: 'x' })` reused across pushes
34
34
  // silently corrupts later `fail`/`complete`/`retry` lookups.
@@ -43,25 +43,35 @@ export class FakeQueue implements QueueDriver {
43
43
  /** Always returns `null` — fake queues never auto-dispatch.
44
44
  * Tests that need handler execution should use the memory
45
45
  * driver directly. */
46
- async pop(): Promise<Job | null> {
46
+ /**
47
+ * Always empty: this fake captures what was dispatched, it does not run it.
48
+ *
49
+ * The parameter is declared so the queue names a caller asks for are part of
50
+ * the signature a test reads, even though nothing here serves them.
51
+ */
52
+ async pop(_queues?: readonly string[]): Promise<JobRecord | null> {
47
53
  return null;
48
54
  }
49
55
 
50
- async fail(job: Job, error: string): Promise<void> {
56
+ async fail(job: JobRecord, error: string): Promise<void> {
51
57
  const found = this.#requireJob(job, "fail");
52
58
  found.status = "failed";
53
59
  found.error = error;
54
60
  }
55
61
 
56
- async complete(job: Job): Promise<void> {
62
+ async complete(job: JobRecord): Promise<void> {
57
63
  const found = this.#requireJob(job, "complete");
58
64
  found.status = "completed";
59
65
  found.processedAt = Date.now();
60
66
  }
61
67
 
62
- async retry(job: Job): Promise<void> {
68
+ async retry(job: JobRecord): Promise<void> {
63
69
  const found = this.#requireJob(job, "retry");
64
- found.attempts += 1;
70
+ // `attempts` is the WORKER's counter, not the driver's: `processOne`
71
+ // increments it before it calls this, and neither real driver touches it
72
+ // again. Incrementing here counted the same attempt twice, so a fake
73
+ // exhausted `maxAttempts` in half the tries the real queue takes — a
74
+ // test double that disagrees with what it stands in for.
65
75
  found.status = "pending";
66
76
  // Reset transient state from the prior failure so a retried
67
77
  // job's invariants match a fresh push (a real driver would
@@ -73,9 +83,9 @@ export class FakeQueue implements QueueDriver {
73
83
 
74
84
  /** Look up the captured copy of a job by id. Throws when the id
75
85
  * isn't present — silent no-op on a missing job is the most
76
- * insidious test bug (caller's local Job ref shows the new
86
+ * insidious test bug (caller's local job ref shows the new
77
87
  * status while the FakeQueue's internal capture is unchanged). */
78
- #requireJob(job: Job, verb: string): Job {
88
+ #requireJob(job: JobRecord, verb: string): JobRecord {
79
89
  const found = this.#pushed.find((j) => j.id === job.id);
80
90
  if (!found) {
81
91
  throw new Error(
@@ -85,7 +95,7 @@ export class FakeQueue implements QueueDriver {
85
95
  return found;
86
96
  }
87
97
 
88
- async failed(): Promise<Job[]> {
98
+ async failed(): Promise<JobRecord[]> {
89
99
  return this.#pushed
90
100
  .filter((j) => j.status === "failed")
91
101
  .map((j) => ({ ...j }));
@@ -100,7 +110,7 @@ export class FakeQueue implements QueueDriver {
100
110
  * shallow clone so test-side mutations can't bleed back into the
101
111
  * internal capture store — avoids cross-test contamination.
102
112
  */
103
- getPushed(): Job[] {
113
+ getPushed(): JobRecord[] {
104
114
  return this.#pushed.map((j) => ({ ...j }));
105
115
  }
106
116
 
@@ -129,7 +139,7 @@ export class FakeQueue implements QueueDriver {
129
139
  function makeMatcher(
130
140
  name: string,
131
141
  predicate: FakeQueuePredicateArg | undefined,
132
- ): (j: Job) => boolean {
142
+ ): (j: JobRecord) => boolean {
133
143
  // Function-form predicate — caller does ALL the matching, the
134
144
  // `name` arg is still a hard prerequisite.
135
145
  if (typeof predicate === "function") {
@@ -162,7 +172,7 @@ function describePredicate(
162
172
  return `, ${JSON.stringify(predicate)}`;
163
173
  }
164
174
 
165
- function describeCaptured(captured: Job[]): string {
175
+ function describeCaptured(captured: JobRecord[]): string {
166
176
  if (captured.length === 0) return "Captured: (none)";
167
177
  const lines = captured.map(
168
178
  (j, i) =>