@effect-app/infra 4.0.0-beta.20 → 4.0.0-beta.200
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 +1378 -0
- package/_check.sh +1 -1
- package/dist/CUPS.d.ts +15 -7
- package/dist/CUPS.d.ts.map +1 -1
- package/dist/CUPS.js +10 -12
- package/dist/Emailer/Sendgrid.d.ts +14 -14
- package/dist/Emailer/Sendgrid.d.ts.map +1 -1
- package/dist/Emailer/Sendgrid.js +16 -15
- package/dist/Emailer/fake.d.ts +1 -1
- package/dist/Emailer/service.d.ts +10 -4
- package/dist/Emailer/service.d.ts.map +1 -1
- package/dist/Emailer/service.js +3 -3
- package/dist/Emailer.d.ts +1 -1
- package/dist/MainFiberSet.d.ts +9 -9
- package/dist/MainFiberSet.d.ts.map +1 -1
- package/dist/MainFiberSet.js +3 -3
- package/dist/Model/Repository/Registry.d.ts +20 -0
- package/dist/Model/Repository/Registry.d.ts.map +1 -0
- package/dist/Model/Repository/Registry.js +17 -0
- package/dist/Model/Repository/ext.d.ts +33 -15
- package/dist/Model/Repository/ext.d.ts.map +1 -1
- package/dist/Model/Repository/ext.js +54 -2
- package/dist/Model/Repository/internal/internal.d.ts +6 -6
- package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
- package/dist/Model/Repository/internal/internal.js +43 -32
- package/dist/Model/Repository/legacy.d.ts +1 -1
- package/dist/Model/Repository/makeRepo.d.ts +7 -6
- package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
- package/dist/Model/Repository/makeRepo.js +5 -1
- package/dist/Model/Repository/service.d.ts +28 -23
- package/dist/Model/Repository/service.d.ts.map +1 -1
- package/dist/Model/Repository/validation.d.ts +142 -17
- package/dist/Model/Repository/validation.d.ts.map +1 -1
- package/dist/Model/Repository/validation.js +5 -5
- package/dist/Model/Repository.d.ts +2 -1
- package/dist/Model/Repository.d.ts.map +1 -1
- package/dist/Model/Repository.js +2 -1
- package/dist/Model/dsl.d.ts +4 -4
- package/dist/Model/dsl.d.ts.map +1 -1
- package/dist/Model/filter/filterApi.d.ts +5 -5
- package/dist/Model/filter/filterApi.d.ts.map +1 -1
- package/dist/Model/filter/types/errors.d.ts +1 -1
- package/dist/Model/filter/types/fields.d.ts +1 -1
- package/dist/Model/filter/types/path/common.d.ts +1 -1
- package/dist/Model/filter/types/path/eager.d.ts +1 -1
- package/dist/Model/filter/types/path/eager.d.ts.map +1 -1
- package/dist/Model/filter/types/path/eager.js +1 -1
- package/dist/Model/filter/types/path/index.d.ts +1 -1
- package/dist/Model/filter/types/utils.d.ts +1 -1
- package/dist/Model/filter/types/validator.d.ts +1 -1
- package/dist/Model/filter/types.d.ts +1 -1
- package/dist/Model/query/dsl.d.ts +16 -16
- package/dist/Model/query/dsl.d.ts.map +1 -1
- package/dist/Model/query/new-kid-interpreter.d.ts +6 -6
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
- package/dist/Model/query/new-kid-interpreter.js +3 -3
- package/dist/Model/query.d.ts +1 -1
- package/dist/Model.d.ts +2 -1
- package/dist/Model.d.ts.map +1 -1
- package/dist/Model.js +2 -1
- package/dist/QueueMaker/SQLQueue.d.ts +5 -7
- package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
- package/dist/QueueMaker/SQLQueue.js +105 -114
- package/dist/QueueMaker/errors.d.ts +2 -2
- package/dist/QueueMaker/errors.d.ts.map +1 -1
- package/dist/QueueMaker/memQueue.d.ts +7 -4
- package/dist/QueueMaker/memQueue.d.ts.map +1 -1
- package/dist/QueueMaker/memQueue.js +51 -62
- package/dist/QueueMaker/sbqueue.d.ts +6 -3
- package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
- package/dist/QueueMaker/sbqueue.js +37 -53
- package/dist/QueueMaker/service.d.ts +1 -1
- package/dist/RequestContext.d.ts +112 -26
- package/dist/RequestContext.d.ts.map +1 -1
- package/dist/RequestContext.js +7 -8
- package/dist/RequestFiberSet.d.ts +7 -7
- package/dist/RequestFiberSet.d.ts.map +1 -1
- package/dist/RequestFiberSet.js +5 -5
- package/dist/Store/ContextMapContainer.d.ts +19 -3
- package/dist/Store/ContextMapContainer.d.ts.map +1 -1
- package/dist/Store/ContextMapContainer.js +13 -3
- package/dist/Store/Cosmos/query.d.ts +1 -1
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +10 -12
- package/dist/Store/Cosmos.d.ts +1 -1
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +318 -240
- package/dist/Store/Disk.d.ts +2 -2
- package/dist/Store/Disk.d.ts.map +1 -1
- package/dist/Store/Disk.js +25 -22
- package/dist/Store/Memory.d.ts +4 -4
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +27 -22
- package/dist/Store/SQL/Pg.d.ts +4 -0
- package/dist/Store/SQL/Pg.d.ts.map +1 -0
- package/dist/Store/SQL/Pg.js +189 -0
- package/dist/Store/SQL/query.d.ts +38 -0
- package/dist/Store/SQL/query.d.ts.map +1 -0
- package/dist/Store/SQL/query.js +367 -0
- package/dist/Store/SQL.d.ts +20 -0
- package/dist/Store/SQL.d.ts.map +1 -0
- package/dist/Store/SQL.js +381 -0
- package/dist/Store/codeFilter.d.ts +1 -1
- package/dist/Store/codeFilter.d.ts.map +1 -1
- package/dist/Store/codeFilter.js +2 -1
- package/dist/Store/index.d.ts +5 -2
- package/dist/Store/index.d.ts.map +1 -1
- package/dist/Store/index.js +15 -3
- package/dist/Store/service.d.ts +18 -7
- package/dist/Store/service.d.ts.map +1 -1
- package/dist/Store/service.js +24 -6
- package/dist/Store/utils.d.ts +1 -1
- package/dist/Store/utils.d.ts.map +1 -1
- package/dist/Store/utils.js +3 -4
- package/dist/Store.d.ts +1 -1
- package/dist/adapters/SQL/Model.d.ts +28 -42
- package/dist/adapters/SQL/Model.d.ts.map +1 -1
- package/dist/adapters/SQL/Model.js +2 -2
- package/dist/adapters/SQL.d.ts +1 -1
- package/dist/adapters/ServiceBus.d.ts +11 -11
- package/dist/adapters/ServiceBus.d.ts.map +1 -1
- package/dist/adapters/ServiceBus.js +13 -15
- package/dist/adapters/cosmos-client.d.ts +3 -3
- package/dist/adapters/cosmos-client.d.ts.map +1 -1
- package/dist/adapters/cosmos-client.js +3 -3
- package/dist/adapters/index.d.ts +8 -2
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +8 -2
- package/dist/adapters/logger.d.ts +2 -2
- package/dist/adapters/logger.d.ts.map +1 -1
- package/dist/adapters/memQueue.d.ts +3 -3
- package/dist/adapters/memQueue.d.ts.map +1 -1
- package/dist/adapters/memQueue.js +3 -3
- package/dist/adapters/mongo-client.d.ts +3 -3
- package/dist/adapters/mongo-client.d.ts.map +1 -1
- package/dist/adapters/mongo-client.js +3 -3
- package/dist/adapters/redis-client.d.ts +3 -3
- package/dist/adapters/redis-client.d.ts.map +1 -1
- package/dist/adapters/redis-client.js +3 -3
- package/dist/api/ContextProvider.d.ts +8 -8
- package/dist/api/ContextProvider.d.ts.map +1 -1
- package/dist/api/ContextProvider.js +6 -6
- package/dist/api/codec.d.ts +1 -1
- package/dist/api/internal/RequestContextMiddleware.d.ts +2 -2
- package/dist/api/internal/RequestContextMiddleware.d.ts.map +1 -1
- package/dist/api/internal/RequestContextMiddleware.js +2 -2
- package/dist/api/internal/auth.d.ts +44 -6
- package/dist/api/internal/auth.d.ts.map +1 -1
- package/dist/api/internal/auth.js +160 -29
- package/dist/api/internal/events.d.ts +3 -3
- package/dist/api/internal/events.d.ts.map +1 -1
- package/dist/api/internal/events.js +9 -7
- package/dist/api/internal/health.d.ts +1 -1
- package/dist/api/layerUtils.d.ts +6 -6
- package/dist/api/layerUtils.d.ts.map +1 -1
- package/dist/api/layerUtils.js +5 -5
- package/dist/api/middlewares.d.ts +1 -1
- package/dist/api/reportError.d.ts +1 -1
- package/dist/api/routing/middleware/RouterMiddleware.d.ts +4 -4
- package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
- package/dist/api/routing/middleware/middleware.d.ts +50 -4
- package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
- package/dist/api/routing/middleware/middleware.js +79 -17
- package/dist/api/routing/middleware.d.ts +1 -2
- package/dist/api/routing/middleware.d.ts.map +1 -1
- package/dist/api/routing/middleware.js +1 -2
- package/dist/api/routing/schema/jwt.d.ts +1 -1
- package/dist/api/routing/schema/jwt.d.ts.map +1 -1
- package/dist/api/routing/tsort.d.ts +1 -1
- package/dist/api/routing/tsort.d.ts.map +1 -1
- package/dist/api/routing/utils.d.ts +3 -3
- package/dist/api/routing/utils.d.ts.map +1 -1
- package/dist/api/routing.d.ts +25 -26
- package/dist/api/routing.d.ts.map +1 -1
- package/dist/api/routing.js +83 -35
- package/dist/api/setupRequest.d.ts +8 -5
- package/dist/api/setupRequest.d.ts.map +1 -1
- package/dist/api/setupRequest.js +12 -7
- package/dist/api/util.d.ts +1 -1
- package/dist/arbs.d.ts +1 -1
- package/dist/arbs.d.ts.map +1 -1
- package/dist/arbs.js +5 -3
- package/dist/errorReporter.d.ts +4 -4
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +20 -25
- package/dist/errors.d.ts +1 -1
- package/dist/fileUtil.d.ts +1 -1
- package/dist/fileUtil.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/logger/jsonLogger.d.ts +1 -1
- package/dist/logger/logFmtLogger.d.ts +1 -1
- package/dist/logger/shared.d.ts +1 -1
- package/dist/logger/shared.js +2 -2
- package/dist/logger.d.ts +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/rateLimit.d.ts +9 -3
- package/dist/rateLimit.d.ts.map +1 -1
- package/dist/rateLimit.js +5 -11
- package/dist/test.d.ts +2 -2
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +1 -1
- package/dist/vitest.d.ts +1 -1
- package/examples/query.ts +39 -35
- package/package.json +41 -37
- package/src/CUPS.ts +9 -11
- package/src/Emailer/Sendgrid.ts +17 -14
- package/src/Emailer/service.ts +9 -3
- package/src/MainFiberSet.ts +5 -6
- package/src/Model/Repository/Registry.ts +33 -0
- package/src/Model/Repository/ext.ts +96 -10
- package/src/Model/Repository/internal/internal.ts +97 -88
- package/src/Model/Repository/makeRepo.ts +12 -10
- package/src/Model/Repository/service.ts +31 -22
- package/src/Model/Repository/validation.ts +4 -4
- package/src/Model/Repository.ts +1 -0
- package/src/Model/dsl.ts +3 -3
- package/src/Model/filter/types/path/eager.ts +1 -2
- package/src/Model/query/dsl.ts +18 -18
- package/src/Model/query/new-kid-interpreter.ts +2 -2
- package/src/Model.ts +1 -0
- package/src/QueueMaker/SQLQueue.ts +121 -151
- package/src/QueueMaker/memQueue.ts +82 -103
- package/src/QueueMaker/sbqueue.ts +56 -86
- package/src/RequestContext.ts +8 -10
- package/src/RequestFiberSet.ts +4 -4
- package/src/Store/ContextMapContainer.ts +41 -2
- package/src/Store/Cosmos/query.ts +16 -20
- package/src/Store/Cosmos.ts +452 -342
- package/src/Store/Disk.ts +52 -49
- package/src/Store/Memory.ts +54 -48
- package/src/Store/SQL/Pg.ts +318 -0
- package/src/Store/SQL/query.ts +409 -0
- package/src/Store/SQL.ts +668 -0
- package/src/Store/codeFilter.ts +1 -0
- package/src/Store/index.ts +17 -2
- package/src/Store/service.ts +32 -8
- package/src/Store/utils.ts +23 -22
- package/src/adapters/SQL/Model.ts +10 -4
- package/src/adapters/ServiceBus.ts +112 -116
- package/src/adapters/cosmos-client.ts +2 -2
- package/src/adapters/index.ts +7 -0
- package/src/adapters/memQueue.ts +2 -2
- package/src/adapters/mongo-client.ts +2 -2
- package/src/adapters/redis-client.ts +2 -2
- package/src/api/ContextProvider.ts +12 -13
- package/src/api/internal/RequestContextMiddleware.ts +1 -1
- package/src/api/internal/auth.ts +246 -44
- package/src/api/internal/events.ts +12 -8
- package/src/api/layerUtils.ts +8 -8
- package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
- package/src/api/routing/middleware/middleware.ts +112 -15
- package/src/api/routing/middleware.ts +0 -2
- package/src/api/routing.ts +142 -63
- package/src/api/setupRequest.ts +28 -8
- package/src/arbs.ts +4 -2
- package/src/errorReporter.ts +62 -74
- package/src/logger/shared.ts +1 -1
- package/src/rateLimit.ts +30 -22
- package/src/test.ts +1 -1
- package/test/auth.test.ts +101 -0
- package/test/contextProvider.test.ts +11 -11
- package/test/controller.test.ts +18 -16
- package/test/dist/auth.test.d.ts.map +1 -0
- package/test/dist/contextProvider.test.d.ts.map +1 -1
- package/test/dist/controller.test.d.ts.map +1 -1
- package/test/dist/fixtures.d.ts +26 -12
- package/test/dist/fixtures.d.ts.map +1 -1
- package/test/dist/fixtures.js +12 -10
- package/test/dist/query.test.d.ts.map +1 -1
- package/test/dist/rawQuery.test.d.ts.map +1 -1
- package/test/dist/repository-ext.test.d.ts.map +1 -0
- package/test/dist/requires.test.d.ts.map +1 -1
- package/test/dist/router-generator.test.d.ts.map +1 -0
- package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
- package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
- package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
- package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
- package/test/dist/sql-store.test.d.ts.map +1 -0
- package/test/fixtures.ts +11 -9
- package/test/query.test.ts +216 -34
- package/test/rawQuery.test.ts +23 -19
- package/test/repository-ext.test.ts +60 -0
- package/test/requires.test.ts +6 -6
- package/test/router-generator.test.ts +180 -0
- package/test/routing-interruptibility.test.ts +63 -0
- package/test/rpc-e2e-invalidation.test.ts +507 -0
- package/test/rpc-multi-middleware.test.ts +78 -9
- package/test/rpc-stream-fullstack.test.ts +325 -0
- package/test/sql-store.test.ts +1064 -0
- package/test/validateSample.test.ts +15 -12
- package/tsconfig.examples.json +1 -1
- package/tsconfig.json +0 -1
- package/tsconfig.json.bak +2 -2
- package/tsconfig.src.json +35 -35
- package/tsconfig.test.json +2 -2
- package/dist/Operations.d.ts +0 -55
- package/dist/Operations.d.ts.map +0 -1
- package/dist/Operations.js +0 -102
- package/dist/OperationsRepo.d.ts +0 -41
- package/dist/OperationsRepo.d.ts.map +0 -1
- package/dist/OperationsRepo.js +0 -14
- package/eslint.config.mjs +0 -24
- package/src/Operations.ts +0 -235
- package/src/OperationsRepo.ts +0 -16
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E tests for commands and queries using in-memory RPC transport.
|
|
3
|
+
*
|
|
4
|
+
* These tests exercise the full server-side pipeline:
|
|
5
|
+
* - `InvalidationMiddlewareLive` — reads the `Invalidates` annotation, pre-populates the
|
|
6
|
+
* `InvalidationSet`, and wraps command results in `{ payload, metadata }`.
|
|
7
|
+
* - `RequestType` annotation — decides whether to wrap (command) or not (query).
|
|
8
|
+
* - `InvalidationSet.use()` — dynamic key accumulation inside a handler.
|
|
9
|
+
*
|
|
10
|
+
* Transport is in-memory via `RpcTest.makeClient`, so no HTTP server is needed.
|
|
11
|
+
*/
|
|
12
|
+
import { expect, it } from "@effect/vitest"
|
|
13
|
+
import { Effect, Layer, Ref, Stream } from "effect"
|
|
14
|
+
import { S } from "effect-app"
|
|
15
|
+
import { InvalidationKeysFromServer, makeInvalidationKeysService } from "effect-app/client"
|
|
16
|
+
import { InvalidationMiddleware } from "effect-app/middleware"
|
|
17
|
+
import { Invalidation } from "effect-app/rpc"
|
|
18
|
+
import { Rpc, RpcGroup, RpcTest } from "effect/unstable/rpc"
|
|
19
|
+
import { InvalidationMiddlewareLive, RequestType } from "../src/api/routing/middleware.js"
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Shared test keys
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const StaticKey: Invalidation.InvalidationKey = ["static", "key"]
|
|
26
|
+
const DynamicKey: Invalidation.InvalidationKey = ["dynamic", "key"]
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// RPC group definition
|
|
30
|
+
//
|
|
31
|
+
// The success schema for commands is defined as the PLAIN type. At runtime,
|
|
32
|
+
// `InvalidationMiddlewareLive` wraps command results into
|
|
33
|
+
// `{ payload: <plain>, metadata: { invalidateQueries: [...] } }`.
|
|
34
|
+
// `RpcTest.makeClient` uses no-serialization transport, so the wrapped runtime
|
|
35
|
+
// value is what the test receives — no codec is applied to coerce it back.
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
const E2eRpcs = RpcGroup.make(
|
|
39
|
+
// Plain query — result is not wrapped
|
|
40
|
+
Rpc
|
|
41
|
+
.make("getGreeting", {
|
|
42
|
+
payload: S.Struct({ name: S.String }),
|
|
43
|
+
success: S.String
|
|
44
|
+
})
|
|
45
|
+
.annotate(RequestType, "query")
|
|
46
|
+
.middleware(InvalidationMiddleware),
|
|
47
|
+
// Command — no invalidation keys
|
|
48
|
+
Rpc
|
|
49
|
+
.make("doNothing", { success: S.Void })
|
|
50
|
+
.annotate(RequestType, "command")
|
|
51
|
+
.middleware(InvalidationMiddleware),
|
|
52
|
+
// Command — static `Invalidates` annotation
|
|
53
|
+
Rpc
|
|
54
|
+
.make("doWithStaticKey", {
|
|
55
|
+
success: S.Struct({ count: S.Number })
|
|
56
|
+
})
|
|
57
|
+
.annotate(RequestType, "command")
|
|
58
|
+
.annotate(Invalidation.Invalidates, [StaticKey])
|
|
59
|
+
.middleware(InvalidationMiddleware),
|
|
60
|
+
// Command — dynamic key added via `InvalidationSet.use`
|
|
61
|
+
Rpc
|
|
62
|
+
.make("doWithDynamicKey", { success: S.String })
|
|
63
|
+
.annotate(RequestType, "command")
|
|
64
|
+
.middleware(InvalidationMiddleware),
|
|
65
|
+
// Command — static annotation + dynamic key combined
|
|
66
|
+
Rpc
|
|
67
|
+
.make("doWithBothKeys", { success: S.Number })
|
|
68
|
+
.annotate(RequestType, "command")
|
|
69
|
+
.annotate(Invalidation.Invalidates, [StaticKey])
|
|
70
|
+
.middleware(InvalidationMiddleware),
|
|
71
|
+
// Command — fails, V2: failure should include accumulated keys
|
|
72
|
+
Rpc
|
|
73
|
+
.make("doAndFail", {
|
|
74
|
+
success: S.Void,
|
|
75
|
+
error: S.Struct({ message: S.String })
|
|
76
|
+
})
|
|
77
|
+
.annotate(RequestType, "command")
|
|
78
|
+
.annotate(Invalidation.Invalidates, [StaticKey])
|
|
79
|
+
.middleware(InvalidationMiddleware),
|
|
80
|
+
// Stream — no input
|
|
81
|
+
Rpc
|
|
82
|
+
.make("streamTicks", {
|
|
83
|
+
success: S.Number,
|
|
84
|
+
stream: true
|
|
85
|
+
})
|
|
86
|
+
.annotate(RequestType, "query")
|
|
87
|
+
.middleware(InvalidationMiddleware),
|
|
88
|
+
// Stream — with input payload
|
|
89
|
+
Rpc
|
|
90
|
+
.make("streamCountTo", {
|
|
91
|
+
payload: S.Struct({ to: S.Number }),
|
|
92
|
+
success: S.Number,
|
|
93
|
+
stream: true
|
|
94
|
+
})
|
|
95
|
+
.annotate(RequestType, "query")
|
|
96
|
+
.middleware(InvalidationMiddleware)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Server implementation layer
|
|
101
|
+
//
|
|
102
|
+
// Handlers return the PLAIN success value. `InvalidationMiddlewareLive`
|
|
103
|
+
// intercepts every command call and wraps the result into
|
|
104
|
+
// `{ payload, metadata: { invalidateQueries } }` before it reaches the client.
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
const E2eImplLayer = E2eRpcs.toLayer({
|
|
108
|
+
getGreeting: ({ name }) => Effect.succeed(`Hello, ${name}!`),
|
|
109
|
+
doNothing: () => Effect.void,
|
|
110
|
+
doWithStaticKey: () => Effect.succeed({ count: 42 }),
|
|
111
|
+
doWithDynamicKey: Effect.fnUntraced(function*() {
|
|
112
|
+
yield* Invalidation.InvalidationSet.use((_) => _.add(DynamicKey))
|
|
113
|
+
return "done"
|
|
114
|
+
}),
|
|
115
|
+
doWithBothKeys: Effect.fnUntraced(function*() {
|
|
116
|
+
yield* Invalidation.InvalidationSet.use((_) => _.add(DynamicKey))
|
|
117
|
+
return 99
|
|
118
|
+
}),
|
|
119
|
+
// V2: command that fails — middleware wraps the failure with accumulated keys
|
|
120
|
+
doAndFail: Effect.fnUntraced(function*() {
|
|
121
|
+
yield* Invalidation.InvalidationSet.use((_) => _.add(DynamicKey))
|
|
122
|
+
return yield* Effect.fail({ message: "intentional failure" })
|
|
123
|
+
}),
|
|
124
|
+
streamTicks: () => Stream.fromIterable([1, 2, 3]),
|
|
125
|
+
streamCountTo: ({ to }) => Stream.range(1, to)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const E2eTestLayer = Layer.merge(E2eImplLayer, InvalidationMiddlewareLive)
|
|
129
|
+
|
|
130
|
+
// Helper: validates that the runtime-wrapped command result has the expected shape.
|
|
131
|
+
// `RpcTest` skips codec encoding/decoding, so the value in the client IS the
|
|
132
|
+
// wrapped object produced by `InvalidationMiddlewareLive`, even when the declared
|
|
133
|
+
// schema is the plain type.
|
|
134
|
+
type CommandResult = { payload: unknown; metadata: { invalidateQueries: ReadonlyArray<Invalidation.InvalidationKey> } }
|
|
135
|
+
|
|
136
|
+
const isCommandResult = (value: unknown): value is CommandResult =>
|
|
137
|
+
typeof value === "object"
|
|
138
|
+
&& value !== null
|
|
139
|
+
&& "payload" in value
|
|
140
|
+
&& "metadata" in value
|
|
141
|
+
&& typeof (value as Record<string, unknown>)["metadata"] === "object"
|
|
142
|
+
&& (value as Record<string, unknown>)["metadata"] !== null
|
|
143
|
+
&& "invalidateQueries" in ((value as Record<string, unknown>)["metadata"] as object)
|
|
144
|
+
|
|
145
|
+
const asCommand = (value: unknown): CommandResult => {
|
|
146
|
+
if (!isCommandResult(value)) throw new Error(`Expected a wrapped command result, got: ${JSON.stringify(value)}`)
|
|
147
|
+
return value
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Tests
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
it.live(
|
|
155
|
+
"query returns the correct value",
|
|
156
|
+
Effect.fnUntraced(function*() {
|
|
157
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
158
|
+
const result = yield* client.getGreeting({ name: "World" })
|
|
159
|
+
expect(result).toBe("Hello, World!")
|
|
160
|
+
}, Effect.provide(E2eTestLayer))
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
it.live(
|
|
164
|
+
"query result is NOT wrapped in CommandResponseWithMetaData",
|
|
165
|
+
Effect.fnUntraced(function*() {
|
|
166
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
167
|
+
const result = yield* client.getGreeting({ name: "Check" })
|
|
168
|
+
expect(typeof result).toBe("string")
|
|
169
|
+
expect(isCommandResult(result)).toBe(false)
|
|
170
|
+
}, Effect.provide(E2eTestLayer))
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
it.live(
|
|
174
|
+
"command with no invalidation keys has empty invalidateQueries in metadata",
|
|
175
|
+
Effect.fnUntraced(function*() {
|
|
176
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
177
|
+
const result = asCommand(yield* client.doNothing())
|
|
178
|
+
expect(result.metadata.invalidateQueries).toStrictEqual([])
|
|
179
|
+
}, Effect.provide(E2eTestLayer))
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
it.live(
|
|
183
|
+
"command with static Invalidates annotation propagates key to metadata",
|
|
184
|
+
Effect.fnUntraced(function*() {
|
|
185
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
186
|
+
const result = asCommand(yield* client.doWithStaticKey())
|
|
187
|
+
expect(result.payload).toStrictEqual({ count: 42 })
|
|
188
|
+
expect(result.metadata.invalidateQueries).toStrictEqual([StaticKey])
|
|
189
|
+
}, Effect.provide(E2eTestLayer))
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
it.live(
|
|
193
|
+
"command with dynamic InvalidationSet.use propagates key to metadata",
|
|
194
|
+
Effect.fnUntraced(function*() {
|
|
195
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
196
|
+
const result = asCommand(yield* client.doWithDynamicKey())
|
|
197
|
+
expect(result.payload).toBe("done")
|
|
198
|
+
expect(result.metadata.invalidateQueries).toStrictEqual([DynamicKey])
|
|
199
|
+
}, Effect.provide(E2eTestLayer))
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
it.live(
|
|
203
|
+
"command combining static annotation + dynamic key merges all into metadata",
|
|
204
|
+
Effect.fnUntraced(function*() {
|
|
205
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
206
|
+
const result = asCommand(yield* client.doWithBothKeys())
|
|
207
|
+
expect(result.payload).toBe(99)
|
|
208
|
+
expect(result.metadata.invalidateQueries).toStrictEqual([StaticKey, DynamicKey])
|
|
209
|
+
}, Effect.provide(E2eTestLayer))
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
it.live(
|
|
213
|
+
"stream RPC without input emits all values",
|
|
214
|
+
Effect.fnUntraced(function*() {
|
|
215
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
216
|
+
const values = yield* Stream.runCollect(client.streamTicks())
|
|
217
|
+
expect(values).toStrictEqual([1, 2, 3])
|
|
218
|
+
}, Effect.provide(E2eTestLayer))
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
it.live(
|
|
222
|
+
"stream RPC with input emits values driven by payload",
|
|
223
|
+
Effect.fnUntraced(function*() {
|
|
224
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
225
|
+
const values = yield* Stream.runCollect(client.streamCountTo({ to: 4 }))
|
|
226
|
+
expect(values).toStrictEqual([1, 2, 3, 4])
|
|
227
|
+
}, Effect.provide(E2eTestLayer))
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
it.live(
|
|
231
|
+
"per-request isolation: each command call has a fresh InvalidationSet",
|
|
232
|
+
Effect.fnUntraced(function*() {
|
|
233
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
234
|
+
const r1 = asCommand(yield* client.doWithDynamicKey())
|
|
235
|
+
const r2 = asCommand(yield* client.doWithDynamicKey())
|
|
236
|
+
// Each call must have exactly one key — no accumulation from prior calls
|
|
237
|
+
expect(r1.metadata.invalidateQueries).toStrictEqual([DynamicKey])
|
|
238
|
+
expect(r2.metadata.invalidateQueries).toStrictEqual([DynamicKey])
|
|
239
|
+
}, Effect.provide(E2eTestLayer))
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Client-side consumption tests
|
|
244
|
+
//
|
|
245
|
+
// These tests verify the full roundtrip for cache invalidation:
|
|
246
|
+
// 1. The server wraps command results in `{ payload, metadata: { invalidateQueries } }`.
|
|
247
|
+
// 2. The client-side `unwrapCommand` logic (as implemented in `apiClientFactory`) extracts
|
|
248
|
+
// the payload and forwards each key to `InvalidationKeysFromServer.add()`.
|
|
249
|
+
//
|
|
250
|
+
// `runAndCapture` replicates that logic using a fresh `Ref`-backed service so the test
|
|
251
|
+
// can inspect which keys the client received after the command completes.
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Replicates `apiClientFactory`'s `unwrapCommand` + `InvalidationKeysFromServer` pattern:
|
|
256
|
+
* - Unwraps the `CommandResponseWithMetaData` result
|
|
257
|
+
* - Calls `svc.add(key)` for every key in `metadata.invalidateQueries`
|
|
258
|
+
* - Returns `{ payload, keys }` for assertion
|
|
259
|
+
*/
|
|
260
|
+
const runAndCapture = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
|
|
261
|
+
Effect.gen(function*() {
|
|
262
|
+
const keysRef = yield* Ref.make<ReadonlyArray<Invalidation.InvalidationKey>>([])
|
|
263
|
+
const svc = makeInvalidationKeysService(keysRef)
|
|
264
|
+
const cmd = asCommand(yield* eff)
|
|
265
|
+
yield* Effect.forEach(cmd.metadata.invalidateQueries, svc.add, { discard: true })
|
|
266
|
+
return { payload: cmd.payload, keys: yield* Ref.get(keysRef) }
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it.live(
|
|
270
|
+
"client consumes static key: payload unwrapped and key forwarded to InvalidationKeysFromServer",
|
|
271
|
+
Effect.fnUntraced(function*() {
|
|
272
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
273
|
+
const { payload, keys } = yield* runAndCapture(client.doWithStaticKey())
|
|
274
|
+
expect(payload).toStrictEqual({ count: 42 })
|
|
275
|
+
expect(keys).toStrictEqual([StaticKey])
|
|
276
|
+
}, Effect.provide(E2eTestLayer))
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
it.live(
|
|
280
|
+
"client consumes dynamic key: payload unwrapped and key forwarded to InvalidationKeysFromServer",
|
|
281
|
+
Effect.fnUntraced(function*() {
|
|
282
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
283
|
+
const { payload, keys } = yield* runAndCapture(client.doWithDynamicKey())
|
|
284
|
+
expect(payload).toBe("done")
|
|
285
|
+
expect(keys).toStrictEqual([DynamicKey])
|
|
286
|
+
}, Effect.provide(E2eTestLayer))
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
it.live(
|
|
290
|
+
"client consumes combined static+dynamic keys: all keys forwarded to InvalidationKeysFromServer",
|
|
291
|
+
Effect.fnUntraced(function*() {
|
|
292
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
293
|
+
const { payload, keys } = yield* runAndCapture(client.doWithBothKeys())
|
|
294
|
+
expect(payload).toBe(99)
|
|
295
|
+
expect(keys).toStrictEqual([StaticKey, DynamicKey])
|
|
296
|
+
}, Effect.provide(E2eTestLayer))
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Stream metadata tests (V1)
|
|
301
|
+
//
|
|
302
|
+
// These tests verify the stream chunk wrapping: the routing layer wraps each
|
|
303
|
+
// emitted value as `{ _tag: "value", value }` and appends a final
|
|
304
|
+
// `{ _tag: "done", metadata: { invalidateQueries } }` chunk. The client
|
|
305
|
+
// side filters out "done" chunks (accumulating keys) and maps "value" chunks
|
|
306
|
+
// to extract the payload. The tests below exercise both layers independently
|
|
307
|
+
// using an RPC group whose success schema is `StreamResponseChunk(S.Number)`.
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
const StreamMetaRpcs = RpcGroup.make(
|
|
311
|
+
// Stream that emits plain numbers — the server wraps them as StreamResponseChunk items
|
|
312
|
+
Rpc
|
|
313
|
+
.make("streamWithMeta", {
|
|
314
|
+
success: Invalidation.StreamResponseChunk(S.Number),
|
|
315
|
+
stream: true
|
|
316
|
+
})
|
|
317
|
+
.annotate(RequestType, "query")
|
|
318
|
+
.middleware(InvalidationMiddleware)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
const StreamKey: Invalidation.InvalidationKey = ["stream", "key"]
|
|
322
|
+
|
|
323
|
+
const StreamMetaImplLayer = StreamMetaRpcs.toLayer({
|
|
324
|
+
// Handler returns pre-wrapped chunks: simulates what routing.ts produces
|
|
325
|
+
streamWithMeta: () =>
|
|
326
|
+
Stream.fromIterable([
|
|
327
|
+
{ _tag: "value" as const, value: 1 },
|
|
328
|
+
{ _tag: "value" as const, value: 2 },
|
|
329
|
+
{ _tag: "value" as const, value: 3 },
|
|
330
|
+
{ _tag: "done" as const, metadata: { invalidateQueries: [StreamKey] } }
|
|
331
|
+
])
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
const StreamMetaTestLayer = Layer.merge(StreamMetaImplLayer, InvalidationMiddlewareLive)
|
|
335
|
+
|
|
336
|
+
it.live(
|
|
337
|
+
"stream: client-side unwrapping delivers plain values and discards 'done' chunk",
|
|
338
|
+
Effect.fnUntraced(function*() {
|
|
339
|
+
const client = yield* RpcTest.makeClient(StreamMetaRpcs)
|
|
340
|
+
const raw = yield* Stream.runCollect(client.streamWithMeta())
|
|
341
|
+
// Client must filter out the "done" chunk and extract only values
|
|
342
|
+
const values = raw
|
|
343
|
+
.filter((item: any) => item._tag === "value")
|
|
344
|
+
.map((item: any) => item.value)
|
|
345
|
+
expect(values).toStrictEqual([1, 2, 3])
|
|
346
|
+
}, Effect.provide(StreamMetaTestLayer))
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
it.live(
|
|
350
|
+
"stream: client-side invalidation keys are collected from the 'done' chunk",
|
|
351
|
+
Effect.fnUntraced(function*() {
|
|
352
|
+
const keysRef = yield* Ref.make<ReadonlyArray<Invalidation.InvalidationKey>>([])
|
|
353
|
+
const svc = makeInvalidationKeysService(keysRef)
|
|
354
|
+
const client = yield* RpcTest.makeClient(StreamMetaRpcs)
|
|
355
|
+
const raw = yield* Stream.runCollect(client.streamWithMeta())
|
|
356
|
+
// Simulate what buildStream does: tap "done" items to accumulate keys
|
|
357
|
+
for (const item of raw) {
|
|
358
|
+
if ((item as any)._tag === "done") {
|
|
359
|
+
const meta = (item as any).metadata as Invalidation.CommandMetaData
|
|
360
|
+
yield* Effect.forEach(meta.invalidateQueries, svc.add, { discard: true })
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const keys = yield* Ref.get(keysRef)
|
|
364
|
+
expect(keys).toStrictEqual([StreamKey])
|
|
365
|
+
}, Effect.provide(StreamMetaTestLayer))
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
it.live(
|
|
369
|
+
"stream: InvalidationKeysFromServer receives keys from 'done' chunk via buildStream-style tap",
|
|
370
|
+
Effect.fnUntraced(function*() {
|
|
371
|
+
const keysRef = yield* Ref.make<ReadonlyArray<Invalidation.InvalidationKey>>([])
|
|
372
|
+
const invKeys = makeInvalidationKeysService(keysRef)
|
|
373
|
+
const client = yield* RpcTest.makeClient(StreamMetaRpcs)
|
|
374
|
+
// Replicate the buildStream processing pipeline: tap must run in the same fiber
|
|
375
|
+
// context as the InvalidationKeysFromServer provider, so we use Effect.provideService
|
|
376
|
+
// (fiber-level) rather than Stream.provideService (element-level) to ensure the
|
|
377
|
+
// tap's Effect.use call resolves invKeys.
|
|
378
|
+
const values = yield* client.streamWithMeta().pipe(
|
|
379
|
+
Stream.tap((item: any) =>
|
|
380
|
+
item._tag === "done"
|
|
381
|
+
? InvalidationKeysFromServer.use((s) =>
|
|
382
|
+
Effect.forEach(
|
|
383
|
+
(item.metadata as Invalidation.CommandMetaData).invalidateQueries,
|
|
384
|
+
s.add,
|
|
385
|
+
{ discard: true }
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
: Effect.void
|
|
389
|
+
),
|
|
390
|
+
Stream.filter((item: any) => item._tag === "value"),
|
|
391
|
+
Stream.map((item: any) => item.value),
|
|
392
|
+
Stream.runCollect,
|
|
393
|
+
Effect.provideService(InvalidationKeysFromServer, invKeys)
|
|
394
|
+
)
|
|
395
|
+
const keys = yield* Ref.get(keysRef)
|
|
396
|
+
expect(values).toStrictEqual([1, 2, 3])
|
|
397
|
+
expect(keys).toStrictEqual([StreamKey])
|
|
398
|
+
}, Effect.provide(StreamMetaTestLayer))
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// V2 tests — invalidation keys included in failures
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
it.live(
|
|
406
|
+
"V2: command failure includes accumulated keys in CommandFailureWithMetaData",
|
|
407
|
+
Effect.fnUntraced(function*() {
|
|
408
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
409
|
+
const exit = yield* Effect.exit(client.doAndFail())
|
|
410
|
+
// Should fail with CommandFailureWithMetaData wrapping the original error
|
|
411
|
+
if (exit._tag === "Success") throw new Error("Expected failure")
|
|
412
|
+
const err = (exit.cause as any).reasons?.[0]?.error
|
|
413
|
+
expect(err?._tag).toBe("CommandFailureWithMetaData")
|
|
414
|
+
expect(err?.error).toStrictEqual({ message: "intentional failure" })
|
|
415
|
+
expect(err?.metadata?.invalidateQueries).toStrictEqual([StaticKey, DynamicKey])
|
|
416
|
+
}, Effect.provide(E2eTestLayer))
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
it.live(
|
|
420
|
+
"V2: client unwraps CommandFailureWithMetaData — re-fails with original error and forwards keys",
|
|
421
|
+
Effect.fnUntraced(function*() {
|
|
422
|
+
const keysRef = yield* Ref.make<ReadonlyArray<Invalidation.InvalidationKey>>([])
|
|
423
|
+
const svc = makeInvalidationKeysService(keysRef)
|
|
424
|
+
const client = yield* RpcTest.makeClient(E2eRpcs)
|
|
425
|
+
|
|
426
|
+
// Simulate apiClientFactory unwrapCommand: catch CommandFailureWithMetaData,
|
|
427
|
+
// forward keys, re-fail with the original error.
|
|
428
|
+
const exit = yield* Effect.exit(
|
|
429
|
+
client.doAndFail().pipe(
|
|
430
|
+
Effect.catch((err: any) =>
|
|
431
|
+
err?._tag === "CommandFailureWithMetaData"
|
|
432
|
+
? Effect
|
|
433
|
+
.forEach(
|
|
434
|
+
(err.metadata?.invalidateQueries ?? []) as ReadonlyArray<Invalidation.InvalidationKey>,
|
|
435
|
+
svc.add,
|
|
436
|
+
{ discard: true }
|
|
437
|
+
)
|
|
438
|
+
.pipe(Effect.flatMap(() => Effect.fail(err.error)))
|
|
439
|
+
: Effect.fail(err)
|
|
440
|
+
),
|
|
441
|
+
Effect.provideService(InvalidationKeysFromServer, svc)
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
const keys = yield* Ref.get(keysRef)
|
|
446
|
+
if (exit._tag === "Success") throw new Error("Expected failure")
|
|
447
|
+
const originalErr = (exit.cause as any).reasons?.[0]?.error
|
|
448
|
+
expect(originalErr).toStrictEqual({ message: "intentional failure" })
|
|
449
|
+
expect(keys).toStrictEqual([StaticKey, DynamicKey])
|
|
450
|
+
}, Effect.provide(E2eTestLayer))
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
const StreamMetaV2Rpcs = RpcGroup.make(
|
|
454
|
+
// Stream with pre-wrapped failure chunk — simulates routing.ts V2 failure output
|
|
455
|
+
Rpc
|
|
456
|
+
.make("streamWithFailure", {
|
|
457
|
+
success: Invalidation.StreamResponseChunk(S.Number),
|
|
458
|
+
error: Invalidation.StreamFailureChunk(S.Struct({ msg: S.String })),
|
|
459
|
+
stream: true
|
|
460
|
+
})
|
|
461
|
+
.annotate(RequestType, "query")
|
|
462
|
+
.middleware(InvalidationMiddleware)
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
const StreamV2Key: Invalidation.InvalidationKey = ["stream-v2", "key"]
|
|
466
|
+
|
|
467
|
+
const StreamMetaV2ImplLayer = StreamMetaV2Rpcs.toLayer({
|
|
468
|
+
// Emits two values then fails with a StreamFailureChunk — simulates routing.ts wrapping
|
|
469
|
+
streamWithFailure: () =>
|
|
470
|
+
Stream.concat(
|
|
471
|
+
Stream.fromIterable([
|
|
472
|
+
{ _tag: "value" as const, value: 1 },
|
|
473
|
+
{ _tag: "value" as const, value: 2 }
|
|
474
|
+
]),
|
|
475
|
+
Stream.fromEffect(
|
|
476
|
+
Effect.fail({
|
|
477
|
+
_tag: "error" as const,
|
|
478
|
+
error: { msg: "stream error" },
|
|
479
|
+
metadata: { invalidateQueries: [StreamV2Key] }
|
|
480
|
+
})
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
const StreamMetaV2TestLayer = Layer.merge(StreamMetaV2ImplLayer, InvalidationMiddlewareLive)
|
|
486
|
+
|
|
487
|
+
it.live(
|
|
488
|
+
"V2: stream failure chunk carries accumulated keys and original error",
|
|
489
|
+
Effect.fnUntraced(function*() {
|
|
490
|
+
const client = yield* RpcTest.makeClient(StreamMetaV2Rpcs)
|
|
491
|
+
const chunks: Array<any> = []
|
|
492
|
+
const exit = yield* Effect.exit(
|
|
493
|
+
Stream.runForEach(client.streamWithFailure(), (item) =>
|
|
494
|
+
Effect.sync(() => {
|
|
495
|
+
chunks.push(item)
|
|
496
|
+
}))
|
|
497
|
+
)
|
|
498
|
+
// Two value chunks should have been seen before the failure
|
|
499
|
+
expect(chunks.map((c: any) => c.value)).toStrictEqual([1, 2])
|
|
500
|
+
// The stream should fail with the StreamFailureChunk
|
|
501
|
+
if (exit._tag === "Success") throw new Error("Expected failure")
|
|
502
|
+
const err = (exit.cause as any).reasons?.[0]?.error
|
|
503
|
+
expect(err?._tag).toBe("error")
|
|
504
|
+
expect(err?.error).toStrictEqual({ msg: "stream error" })
|
|
505
|
+
expect(err?.metadata?.invalidateQueries).toStrictEqual([StreamV2Key])
|
|
506
|
+
}, Effect.provide(StreamMetaV2TestLayer))
|
|
507
|
+
)
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { NodeHttpServer } from "@effect/platform-node"
|
|
2
2
|
import { expect, expectTypeOf, it } from "@effect/vitest"
|
|
3
|
-
import { Console, Effect, Layer, Result } from "effect"
|
|
4
|
-
import { S } from "effect-app"
|
|
3
|
+
import { Console, Effect, Layer, Ref, Result } from "effect"
|
|
4
|
+
import { Context, RpcX, S } from "effect-app"
|
|
5
5
|
import { NotLoggedInError } from "effect-app/client"
|
|
6
6
|
import { HttpRouter } from "effect-app/http"
|
|
7
7
|
import { DefaultGenericMiddlewares } from "effect-app/middleware"
|
|
8
|
-
import { MiddlewareMaker } from "effect-app/rpc"
|
|
9
|
-
import { middlewareGroup } from "effect-app/rpc/MiddlewareMaker"
|
|
10
8
|
import { FetchHttpClient } from "effect/unstable/http"
|
|
11
|
-
import { RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc"
|
|
9
|
+
import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc"
|
|
12
10
|
import { createServer } from "http"
|
|
13
11
|
import { DefaultGenericMiddlewaresLive } from "../src/api/routing.js"
|
|
14
12
|
import { AllowAnonymous, AllowAnonymousLive, RequestContextMap, RequireRoles, RequireRolesLive, Some, SomeElseMiddleware, SomeElseMiddlewareLive, SomeMiddleware, SomeMiddlewareLive, SomeService, Test, TestLive, UserProfile } from "./fixtures.js"
|
|
15
13
|
|
|
16
|
-
const incomplete =
|
|
14
|
+
const incomplete = RpcX
|
|
15
|
+
.MiddlewareMaker
|
|
17
16
|
.Tag<middleware>()("MiddlewareMaker", RequestContextMap)
|
|
18
17
|
.middleware(RequireRoles)
|
|
19
18
|
.middleware(AllowAnonymous, Test)
|
|
@@ -21,7 +20,8 @@ const incomplete = MiddlewareMaker
|
|
|
21
20
|
// this extension is allowed otherwise the error is quite obscure
|
|
22
21
|
export class incompleteMiddleware extends incomplete {}
|
|
23
22
|
|
|
24
|
-
class middleware extends
|
|
23
|
+
class middleware extends RpcX
|
|
24
|
+
.MiddlewareMaker
|
|
25
25
|
.Tag<middleware>()("MiddlewareMaker", RequestContextMap)
|
|
26
26
|
.middleware(RequireRoles)
|
|
27
27
|
.middleware(AllowAnonymous, Test)
|
|
@@ -29,7 +29,7 @@ class middleware extends MiddlewareMaker
|
|
|
29
29
|
.middleware(...DefaultGenericMiddlewares)
|
|
30
30
|
{}
|
|
31
31
|
|
|
32
|
-
const UserRpcs = middlewareGroup(middleware)(
|
|
32
|
+
const UserRpcs = RpcX.MiddlewareMaker.middlewareGroup(middleware)(
|
|
33
33
|
RpcGroup.make(
|
|
34
34
|
middleware.rpc("getUser", {
|
|
35
35
|
success: S.Literal("awesome")
|
|
@@ -56,7 +56,7 @@ const impl = UserRpcs
|
|
|
56
56
|
|
|
57
57
|
expectTypeOf<Layer.Services<typeof impl>>().toEqualTypeOf<never>()
|
|
58
58
|
|
|
59
|
-
const UserRpcsBad = middlewareGroup(middleware)(
|
|
59
|
+
const UserRpcsBad = RpcX.MiddlewareMaker.middlewareGroup(middleware)(
|
|
60
60
|
RpcGroup.make(
|
|
61
61
|
middleware.rpc("doSomethingElse", {
|
|
62
62
|
success: S.Literal("also-awesome2"),
|
|
@@ -136,3 +136,72 @@ it.live(
|
|
|
136
136
|
Effect.provide(RpcTestLayer)
|
|
137
137
|
)
|
|
138
138
|
)
|
|
139
|
+
|
|
140
|
+
// Per-request service isolation test
|
|
141
|
+
|
|
142
|
+
class PerRequestCounter extends Context.Service<PerRequestCounter>()(
|
|
143
|
+
"PerRequestCounter",
|
|
144
|
+
{ make: Effect.sync(() => ({ a: 0 })) }
|
|
145
|
+
) {
|
|
146
|
+
static Default = Layer.effect(this, this.make)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
class GlobalCounter extends Context.Service<GlobalCounter, {
|
|
150
|
+
readonly ref: Ref.Ref<number>
|
|
151
|
+
}>()("GlobalCounter") {}
|
|
152
|
+
|
|
153
|
+
const CounterRpcs = RpcGroup.make(
|
|
154
|
+
Rpc.make("incrementA", {
|
|
155
|
+
success: S.Number
|
|
156
|
+
}),
|
|
157
|
+
Rpc.make("incrementB", {
|
|
158
|
+
success: S.Number
|
|
159
|
+
})
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
const counterImpl = CounterRpcs
|
|
163
|
+
.toLayer({
|
|
164
|
+
incrementA: Effect.fn(function*() {
|
|
165
|
+
const counter = yield* PerRequestCounter
|
|
166
|
+
counter.a++
|
|
167
|
+
const global = yield* GlobalCounter
|
|
168
|
+
yield* Ref.update(global.ref, (n) => n + 1)
|
|
169
|
+
return counter.a
|
|
170
|
+
}, Effect.provide(PerRequestCounter.Default)),
|
|
171
|
+
incrementB: Effect.fn(function*() {
|
|
172
|
+
const counter = yield* PerRequestCounter
|
|
173
|
+
counter.a++
|
|
174
|
+
const global = yield* GlobalCounter
|
|
175
|
+
yield* Ref.update(global.ref, (n) => n + 1)
|
|
176
|
+
return counter.a
|
|
177
|
+
}, Effect.provide(PerRequestCounter.Default))
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const GlobalCounterLive = Layer.effect(
|
|
181
|
+
GlobalCounter,
|
|
182
|
+
Ref.make(0).pipe(Effect.map((ref) => ({ ref })))
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
const CounterTestLayer = counterImpl.pipe(Layer.provideMerge(GlobalCounterLive))
|
|
186
|
+
|
|
187
|
+
it.live(
|
|
188
|
+
"per-request service isolation with shared global counter",
|
|
189
|
+
Effect.fnUntraced(
|
|
190
|
+
function*() {
|
|
191
|
+
const client = yield* RpcTest.makeClient(CounterRpcs)
|
|
192
|
+
const global = yield* GlobalCounter
|
|
193
|
+
|
|
194
|
+
const r1 = yield* client.incrementA()
|
|
195
|
+
const r2 = yield* client.incrementB()
|
|
196
|
+
|
|
197
|
+
// per-request counter is fresh each time → both return 1
|
|
198
|
+
expect(r1).toBe(1)
|
|
199
|
+
expect(r2).toBe(1)
|
|
200
|
+
|
|
201
|
+
// global counter is shared across requests → accumulates to 2
|
|
202
|
+
const globalCount = yield* Ref.get(global.ref)
|
|
203
|
+
expect(globalCount).toBe(2)
|
|
204
|
+
},
|
|
205
|
+
Effect.provide(CounterTestLayer)
|
|
206
|
+
)
|
|
207
|
+
)
|