effect-mq 0.3.1 → 0.3.2

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/README.md CHANGED
@@ -16,7 +16,7 @@ One package, tree-shakeable modules:
16
16
  | `effect-mq` | `Job`, `JobStore`, `MemoryJobStore`, `Worker` | — |
17
17
  | `effect-mq/drizzle-postgres` | drizzle-postgres schema factories + the Postgres `JobStore` | `drizzle-orm` (v1), `@effect/sql-pg` |
18
18
  | `effect-mq/redis` | the Redis `JobStore` (Lua-script atomicity) | a `Redis` service (`@effect/platform-node`/`-bun`) |
19
- | `effect-mq/testing` | the `JobStore` conformance suite for driver authors | `@effect/vitest` |
19
+ | `effect-mq/testing` | `TestJobStore` (assert enqueues in unit tests) + the driver conformance suite | `@effect/vitest` (conformance only) |
20
20
 
21
21
  ## Five-minute tour
22
22
 
@@ -510,6 +510,34 @@ handler).
510
510
  and `historySweepInterval`; drizzle-postgres additionally takes the table
511
511
  instances (+ `validate`), Redis a key `prefix`.
512
512
 
513
+ ## Testing your app
514
+
515
+ Unit-test that services enqueue correctly — no worker, no boilerplate, and
516
+ payloads come back **decoded through the job's schema** (so `Redacted`,
517
+ `DateTime`, and branded values are real instances, not stored JSON):
518
+
519
+ ```ts
520
+ import { TestJobStore } from "effect-mq/testing"
521
+
522
+ it.effect("signup enqueues a welcome email", () =>
523
+ Effect.gen(function*() {
524
+ yield* SignupService.register({ email: "ada@example.com" })
525
+
526
+ const emails = yield* TestJobStore.enqueuedOf(SendEmail)
527
+ expect(emails).toHaveLength(1)
528
+ expect(emails[0]?.payload.to).toBe("ada@example.com")
529
+ expect(emails[0]?.state).toBe("waiting")
530
+ }).pipe(Effect.provide(TestJobStore.layer)))
531
+ ```
532
+
533
+ `TestJobStore.layer` provides a fresh in-memory store as both the default
534
+ `JobStore` (for the code under test) and the inspection service; jobs just
535
+ accumulate in `waiting`/`delayed` since nothing claims them. Named stores
536
+ use `TestJobStore.layerFor(Durable)`. Records surface scheduling detail
537
+ (`state`, `priority`, `runAt`, `metadata`, `dedupeKey`), and the raw store
538
+ is exposed for simulating claims/acks. No `@effect/vitest` required — it is
539
+ plain Effect, so it works with any test runner.
540
+
513
541
  ## Writing a storage driver
514
542
 
515
543
  Implement the `JobStore` service (one atomic seam: `enqueue`, `claim`, `ack`,
@@ -0,0 +1,95 @@
1
+ /**
2
+ * A test harness for asserting what your services enqueue, without running
3
+ * a worker.
4
+ *
5
+ * Provide `TestJobStore.layer` in a unit test and jobs enqueued by the code
6
+ * under test simply accumulate in `waiting`/`delayed` (nothing claims them).
7
+ * `enqueuedOf` returns them with payloads **decoded through the job's own
8
+ * schema** — you assert against the typed values your service produced, not
9
+ * the encoded JSON the store persists:
10
+ *
11
+ * ```ts
12
+ * import { TestJobStore } from "effect-mq/testing"
13
+ *
14
+ * it.effect("signup enqueues a welcome email", () =>
15
+ * Effect.gen(function*() {
16
+ * yield* SignupService.register({ email: "ada@example.com" })
17
+ *
18
+ * const emails = yield* TestJobStore.enqueuedOf(SendEmail)
19
+ * expect(emails).toHaveLength(1)
20
+ * expect(emails[0]?.payload.to).toBe("ada@example.com")
21
+ * expect(emails[0]?.state).toBe("waiting")
22
+ * }).pipe(Effect.provide(TestJobStore.layer)))
23
+ * ```
24
+ *
25
+ * Jobs bound to named stores use `TestJobStore.layerFor(Durable)` instead.
26
+ * The raw `JobStore` service is also exposed (as `.store`) for advanced
27
+ * scenarios — simulating claims/acks, reading `counts()`, and so on.
28
+ *
29
+ * @since 0.3.2
30
+ */
31
+ import * as JobStore from "../JobStore.ts";
32
+ import { Context, Effect, Layer, Schema } from "effect";
33
+ /**
34
+ * The minimal structural view of a `Job.make` class that `enqueuedOf`
35
+ * needs: its tag and its JSON payload codec.
36
+ *
37
+ * @since 0.3.2
38
+ */
39
+ export interface AnyJobDefinition {
40
+ readonly _tag: string;
41
+ readonly payloadJsonSchema: Schema.Top & {
42
+ readonly DecodingServices: never;
43
+ };
44
+ }
45
+ /**
46
+ * A stored job with its payload decoded back to the definition's payload
47
+ * type.
48
+ *
49
+ * @since 0.3.2
50
+ */
51
+ export interface EnqueuedJob<Payload> extends Omit<JobStore.JobRecord, "payload"> {
52
+ readonly payload: Payload;
53
+ }
54
+ declare const TestJobStore_base: Context.ServiceClass<TestJobStore, "effect-mq/testing/TestJobStore", {
55
+ store: JobStore.Service;
56
+ enqueued: (name?: string) => Effect.Effect<JobStore.JobRecord[], never, never>;
57
+ enqueuedOf: <J extends AnyJobDefinition>(job: J) => Effect.Effect<EnqueuedJob<J["payloadJsonSchema"]["Type"]>[], never, never>;
58
+ }>;
59
+ /**
60
+ * Inspection API over the test store. `enqueued(name?)` returns raw records
61
+ * oldest-first; `enqueuedOf(JobClass)` additionally decodes payloads through
62
+ * the definition's schema.
63
+ *
64
+ * @since 0.3.2
65
+ */
66
+ export declare class TestJobStore extends TestJobStore_base {
67
+ }
68
+ /**
69
+ * A fresh in-memory store provided as BOTH the default `JobStore` (for the
70
+ * code under test) and the `TestJobStore` inspection service (for the
71
+ * assertions).
72
+ *
73
+ * @since 0.3.2
74
+ */
75
+ export declare const layer: Layer.Layer<JobStore.JobStore | TestJobStore>;
76
+ /**
77
+ * Like `layer`, for jobs bound to a `JobStore.named(...)` key.
78
+ *
79
+ * @since 0.3.2
80
+ */
81
+ export declare const layerFor: <Id>(store: Context.Key<Id, JobStore.Service>) => Layer.Layer<Id | TestJobStore>;
82
+ /**
83
+ * Convenience accessors so tests don't have to `yield* TestJobStore` first.
84
+ *
85
+ * @since 0.3.2
86
+ */
87
+ export declare const enqueuedOf: <J extends AnyJobDefinition>(job: J) => Effect.Effect<Array<EnqueuedJob<J["payloadJsonSchema"]["Type"]>>, never, TestJobStore>;
88
+ /**
89
+ * Raw records (optionally filtered by job name), oldest-first.
90
+ *
91
+ * @since 0.3.2
92
+ */
93
+ export declare const enqueued: (name?: string) => Effect.Effect<Array<JobStore.JobRecord>, never, TestJobStore>;
94
+ export {};
95
+ //# sourceMappingURL=TestJobStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestJobStore.d.ts","sourceRoot":"","sources":["../../src/testing/TestJobStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAE1C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEvD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC,GAAG,GAAG;QAAE,QAAQ,CAAC,gBAAgB,EAAE,KAAK,CAAA;KAAE,CAAA;CAC9E;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW,CAAC,OAAO,CAAE,SAAQ,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC/E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;;;sBAmBmB,MAAM;iBACX,CAAC,SAAS,gBAAgB,OAAO,CAAC;;AAWjD;;;;;;GAMG;AACH,qBAAa,YAAa,SAAQ,iBAEjC;CAAG;AAEJ;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,GAAG,YAAY,CAK/D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAI,EAAE,SAClB,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,OAAO,CAAC,KACvC,KAAK,CAAC,KAAK,CAAC,EAAE,GAAG,YAAY,CAM7B,CAAA;AAEH;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,gBAAgB,OAC9C,CAAC,KACL,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAC5B,CAAA;AAE5D;;;;GAIG;AACH,eAAO,MAAM,QAAQ,UACZ,MAAM,KACZ,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,YAAY,CACJ,CAAA"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * A test harness for asserting what your services enqueue, without running
3
+ * a worker.
4
+ *
5
+ * Provide `TestJobStore.layer` in a unit test and jobs enqueued by the code
6
+ * under test simply accumulate in `waiting`/`delayed` (nothing claims them).
7
+ * `enqueuedOf` returns them with payloads **decoded through the job's own
8
+ * schema** — you assert against the typed values your service produced, not
9
+ * the encoded JSON the store persists:
10
+ *
11
+ * ```ts
12
+ * import { TestJobStore } from "effect-mq/testing"
13
+ *
14
+ * it.effect("signup enqueues a welcome email", () =>
15
+ * Effect.gen(function*() {
16
+ * yield* SignupService.register({ email: "ada@example.com" })
17
+ *
18
+ * const emails = yield* TestJobStore.enqueuedOf(SendEmail)
19
+ * expect(emails).toHaveLength(1)
20
+ * expect(emails[0]?.payload.to).toBe("ada@example.com")
21
+ * expect(emails[0]?.state).toBe("waiting")
22
+ * }).pipe(Effect.provide(TestJobStore.layer)))
23
+ * ```
24
+ *
25
+ * Jobs bound to named stores use `TestJobStore.layerFor(Durable)` instead.
26
+ * The raw `JobStore` service is also exposed (as `.store`) for advanced
27
+ * scenarios — simulating claims/acks, reading `counts()`, and so on.
28
+ *
29
+ * @since 0.3.2
30
+ */
31
+ import * as JobStore from "../JobStore.js";
32
+ import * as MemoryJobStore from "../MemoryJobStore.js";
33
+ import { Context, Effect, Layer, Schema } from "effect";
34
+ const drainList = (store, name) => Effect.gen(function* () {
35
+ const all = [];
36
+ let cursor = undefined;
37
+ while (true) {
38
+ const page = yield* store.list({ name, cursor, limit: 200 }).pipe(Effect.orDie);
39
+ all.push(...page.items);
40
+ if (page.cursor === undefined)
41
+ break;
42
+ cursor = page.cursor;
43
+ }
44
+ // list() is newest-first; flip to oldest-first for natural reading.
45
+ // Ties (same-instant enqueues) order by id, not submission order.
46
+ return all.toReversed();
47
+ });
48
+ const makeApi = (store) => ({
49
+ store,
50
+ enqueued: (name) => drainList(store, name),
51
+ enqueuedOf: (job) => drainList(store, job._tag).pipe(Effect.flatMap(Effect.forEach((record) => Schema.decodeUnknownEffect(job.payloadJsonSchema)(record.payload).pipe(Effect.orDie, Effect.map((payload) => ({ ...record, payload }))))))
52
+ });
53
+ /**
54
+ * Inspection API over the test store. `enqueued(name?)` returns raw records
55
+ * oldest-first; `enqueuedOf(JobClass)` additionally decodes payloads through
56
+ * the definition's schema.
57
+ *
58
+ * @since 0.3.2
59
+ */
60
+ export class TestJobStore extends Context.Service()("effect-mq/testing/TestJobStore") {
61
+ }
62
+ /**
63
+ * A fresh in-memory store provided as BOTH the default `JobStore` (for the
64
+ * code under test) and the `TestJobStore` inspection service (for the
65
+ * assertions).
66
+ *
67
+ * @since 0.3.2
68
+ */
69
+ export const layer = Layer.effectContext(Effect.map(MemoryJobStore.makeWith(), (store) => Context.make(JobStore.JobStore, store).pipe(Context.add(TestJobStore, TestJobStore.of(makeApi(store))))));
70
+ /**
71
+ * Like `layer`, for jobs bound to a `JobStore.named(...)` key.
72
+ *
73
+ * @since 0.3.2
74
+ */
75
+ export const layerFor = (store) => Layer.effectContext(Effect.map(MemoryJobStore.makeWith(), (memory) => Context.make(store, memory).pipe(Context.add(TestJobStore, TestJobStore.of(makeApi(memory))))));
76
+ /**
77
+ * Convenience accessors so tests don't have to `yield* TestJobStore` first.
78
+ *
79
+ * @since 0.3.2
80
+ */
81
+ export const enqueuedOf = (job) => Effect.flatMap(TestJobStore, (api) => api.enqueuedOf(job));
82
+ /**
83
+ * Raw records (optionally filtered by job name), oldest-first.
84
+ *
85
+ * @since 0.3.2
86
+ */
87
+ export const enqueued = (name) => Effect.flatMap(TestJobStore, (api) => api.enqueued(name));
88
+ //# sourceMappingURL=TestJobStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestJobStore.js","sourceRoot":"","sources":["../../src/testing/TestJobStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAC1C,OAAO,KAAK,cAAc,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAuBvD,MAAM,SAAS,GAAG,CAAC,KAAuB,EAAE,IAAa,EAAE,EAAE,CAC3D,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,GAAG,GAA8B,EAAE,CAAA;IACzC,IAAI,MAAM,GAAuB,SAAS,CAAA;IAC1C,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,IAAI,GAAwB,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACpG,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;QACvB,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;YAAE,MAAK;QACpC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;IACtB,CAAC;IACD,oEAAoE;IACpE,kEAAkE;IAClE,OAAO,GAAG,CAAC,UAAU,EAAE,CAAA;AACzB,CAAC,CAAC,CAAA;AAEJ,MAAM,OAAO,GAAG,CAAC,KAAuB,EAAE,EAAE,CAAC,CAAC;IAC5C,KAAK;IACL,QAAQ,EAAE,CAAC,IAAa,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC;IACnD,UAAU,EAAE,CAA6B,GAAM,EAAE,EAAE,CACjD,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAC7B,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CACvC,MAAM,CAAC,mBAAmB,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CACpE,MAAM,CAAC,KAAK,EACZ,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAA+C,EAAE,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAC/F,CACF,CAAC,CACH;CACJ,CAAC,CAAA;AAEF;;;;;;GAMG;AACH,MAAM,OAAO,YAAa,SAAQ,OAAO,CAAC,OAAO,EAA4C,CAC3F,gCAAgC,CACjC;CAAG;AAEJ;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAkD,KAAK,CAAC,aAAa,CACrF,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,QAAQ,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAC9C,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,IAAI,CACzC,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,YAAY,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAC3D,CAAC,CACL,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CACtB,KAAwC,EACR,EAAE,CAClC,KAAK,CAAC,aAAa,CACjB,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAC/C,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,IAAI,CAC9B,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,YAAY,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAC5D,CAAC,CACL,CAAA;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CACxB,GAAM,EACkF,EAAE,CAC1F,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;AAE5D;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CACtB,IAAa,EACkD,EAAE,CACjE,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA"}
@@ -5,4 +5,10 @@
5
5
  * @since 0.1.0
6
6
  */
7
7
  export * from "./conformance.ts";
8
+ /**
9
+ * Assert what services enqueue in unit tests, with typed payloads.
10
+ *
11
+ * @since 0.3.2
12
+ */
13
+ export * as TestJobStore from "./TestJobStore.ts";
8
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,kBAAkB,CAAA;AAEhC;;;;GAIG;AACH,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAA"}
@@ -5,4 +5,10 @@
5
5
  * @since 0.1.0
6
6
  */
7
7
  export * from "./conformance.js";
8
+ /**
9
+ * Assert what services enqueue in unit tests, with typed payloads.
10
+ *
11
+ * @since 0.3.2
12
+ */
13
+ export * as TestJobStore from "./TestJobStore.js";
8
14
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,kBAAkB,CAAA;AAEhC;;;;GAIG;AACH,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-mq",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Effect-native background jobs: schema-first definitions, storage-agnostic queue core, worker runtime, and a Postgres store through drizzle.",
5
5
  "license": "MIT",
6
6
  "author": "Adam Rankin",
@@ -0,0 +1,143 @@
1
+ /**
2
+ * A test harness for asserting what your services enqueue, without running
3
+ * a worker.
4
+ *
5
+ * Provide `TestJobStore.layer` in a unit test and jobs enqueued by the code
6
+ * under test simply accumulate in `waiting`/`delayed` (nothing claims them).
7
+ * `enqueuedOf` returns them with payloads **decoded through the job's own
8
+ * schema** — you assert against the typed values your service produced, not
9
+ * the encoded JSON the store persists:
10
+ *
11
+ * ```ts
12
+ * import { TestJobStore } from "effect-mq/testing"
13
+ *
14
+ * it.effect("signup enqueues a welcome email", () =>
15
+ * Effect.gen(function*() {
16
+ * yield* SignupService.register({ email: "ada@example.com" })
17
+ *
18
+ * const emails = yield* TestJobStore.enqueuedOf(SendEmail)
19
+ * expect(emails).toHaveLength(1)
20
+ * expect(emails[0]?.payload.to).toBe("ada@example.com")
21
+ * expect(emails[0]?.state).toBe("waiting")
22
+ * }).pipe(Effect.provide(TestJobStore.layer)))
23
+ * ```
24
+ *
25
+ * Jobs bound to named stores use `TestJobStore.layerFor(Durable)` instead.
26
+ * The raw `JobStore` service is also exposed (as `.store`) for advanced
27
+ * scenarios — simulating claims/acks, reading `counts()`, and so on.
28
+ *
29
+ * @since 0.3.2
30
+ */
31
+ import * as JobStore from "../JobStore.ts"
32
+ import * as MemoryJobStore from "../MemoryJobStore.ts"
33
+ import { Context, Effect, Layer, Schema } from "effect"
34
+
35
+ /**
36
+ * The minimal structural view of a `Job.make` class that `enqueuedOf`
37
+ * needs: its tag and its JSON payload codec.
38
+ *
39
+ * @since 0.3.2
40
+ */
41
+ export interface AnyJobDefinition {
42
+ readonly _tag: string
43
+ readonly payloadJsonSchema: Schema.Top & { readonly DecodingServices: never }
44
+ }
45
+
46
+ /**
47
+ * A stored job with its payload decoded back to the definition's payload
48
+ * type.
49
+ *
50
+ * @since 0.3.2
51
+ */
52
+ export interface EnqueuedJob<Payload> extends Omit<JobStore.JobRecord, "payload"> {
53
+ readonly payload: Payload
54
+ }
55
+
56
+ const drainList = (store: JobStore.Service, name?: string) =>
57
+ Effect.gen(function*() {
58
+ const all: Array<JobStore.JobRecord> = []
59
+ let cursor: string | undefined = undefined
60
+ while (true) {
61
+ const page: JobStore.ListResult = yield* store.list({ name, cursor, limit: 200 }).pipe(Effect.orDie)
62
+ all.push(...page.items)
63
+ if (page.cursor === undefined) break
64
+ cursor = page.cursor
65
+ }
66
+ // list() is newest-first; flip to oldest-first for natural reading.
67
+ // Ties (same-instant enqueues) order by id, not submission order.
68
+ return all.toReversed()
69
+ })
70
+
71
+ const makeApi = (store: JobStore.Service) => ({
72
+ store,
73
+ enqueued: (name?: string) => drainList(store, name),
74
+ enqueuedOf: <J extends AnyJobDefinition>(job: J) =>
75
+ drainList(store, job._tag).pipe(
76
+ Effect.flatMap(Effect.forEach((record) =>
77
+ Schema.decodeUnknownEffect(job.payloadJsonSchema)(record.payload).pipe(
78
+ Effect.orDie,
79
+ Effect.map((payload): EnqueuedJob<J["payloadJsonSchema"]["Type"]> => ({ ...record, payload }))
80
+ )
81
+ ))
82
+ )
83
+ })
84
+
85
+ /**
86
+ * Inspection API over the test store. `enqueued(name?)` returns raw records
87
+ * oldest-first; `enqueuedOf(JobClass)` additionally decodes payloads through
88
+ * the definition's schema.
89
+ *
90
+ * @since 0.3.2
91
+ */
92
+ export class TestJobStore extends Context.Service<TestJobStore, ReturnType<typeof makeApi>>()(
93
+ "effect-mq/testing/TestJobStore"
94
+ ) {}
95
+
96
+ /**
97
+ * A fresh in-memory store provided as BOTH the default `JobStore` (for the
98
+ * code under test) and the `TestJobStore` inspection service (for the
99
+ * assertions).
100
+ *
101
+ * @since 0.3.2
102
+ */
103
+ export const layer: Layer.Layer<JobStore.JobStore | TestJobStore> = Layer.effectContext(
104
+ Effect.map(MemoryJobStore.makeWith(), (store) =>
105
+ Context.make(JobStore.JobStore, store).pipe(
106
+ Context.add(TestJobStore, TestJobStore.of(makeApi(store)))
107
+ ))
108
+ )
109
+
110
+ /**
111
+ * Like `layer`, for jobs bound to a `JobStore.named(...)` key.
112
+ *
113
+ * @since 0.3.2
114
+ */
115
+ export const layerFor = <Id>(
116
+ store: Context.Key<Id, JobStore.Service>
117
+ ): Layer.Layer<Id | TestJobStore> =>
118
+ Layer.effectContext(
119
+ Effect.map(MemoryJobStore.makeWith(), (memory) =>
120
+ Context.make(store, memory).pipe(
121
+ Context.add(TestJobStore, TestJobStore.of(makeApi(memory)))
122
+ ))
123
+ )
124
+
125
+ /**
126
+ * Convenience accessors so tests don't have to `yield* TestJobStore` first.
127
+ *
128
+ * @since 0.3.2
129
+ */
130
+ export const enqueuedOf = <J extends AnyJobDefinition>(
131
+ job: J
132
+ ): Effect.Effect<Array<EnqueuedJob<J["payloadJsonSchema"]["Type"]>>, never, TestJobStore> =>
133
+ Effect.flatMap(TestJobStore, (api) => api.enqueuedOf(job))
134
+
135
+ /**
136
+ * Raw records (optionally filtered by job name), oldest-first.
137
+ *
138
+ * @since 0.3.2
139
+ */
140
+ export const enqueued = (
141
+ name?: string
142
+ ): Effect.Effect<Array<JobStore.JobRecord>, never, TestJobStore> =>
143
+ Effect.flatMap(TestJobStore, (api) => api.enqueued(name))
@@ -5,3 +5,10 @@
5
5
  * @since 0.1.0
6
6
  */
7
7
  export * from "./conformance.ts"
8
+
9
+ /**
10
+ * Assert what services enqueue in unit tests, with typed payloads.
11
+ *
12
+ * @since 0.3.2
13
+ */
14
+ export * as TestJobStore from "./TestJobStore.ts"