@effect-app/infra 4.0.0-beta.22 → 4.0.0-beta.221
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 +1648 -0
- package/_check.sh +1 -1
- package/dist/CUPS.d.ts +12 -7
- package/dist/CUPS.d.ts.map +1 -1
- package/dist/CUPS.js +16 -12
- package/dist/Emailer/Sendgrid.d.ts +15 -15
- package/dist/Emailer/Sendgrid.d.ts.map +1 -1
- package/dist/Emailer/Sendgrid.js +20 -16
- package/dist/Emailer/fake.d.ts +1 -1
- package/dist/Emailer/fake.js +2 -2
- package/dist/Emailer/service.d.ts +13 -4
- package/dist/Emailer/service.d.ts.map +1 -1
- package/dist/Emailer/service.js +4 -3
- package/dist/Emailer.d.ts +1 -1
- package/dist/MainFiberSet.d.ts +12 -9
- package/dist/MainFiberSet.d.ts.map +1 -1
- package/dist/MainFiberSet.js +7 -3
- package/dist/Model/Repository/Registry.d.ts +21 -0
- package/dist/Model/Repository/Registry.d.ts.map +1 -0
- package/dist/Model/Repository/Registry.js +18 -0
- package/dist/Model/Repository/ext.d.ts +35 -16
- package/dist/Model/Repository/ext.d.ts.map +1 -1
- package/dist/Model/Repository/ext.js +60 -3
- package/dist/Model/Repository/internal/internal.d.ts +9 -6
- package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
- package/dist/Model/Repository/internal/internal.js +115 -51
- package/dist/Model/Repository/legacy.d.ts +4 -2
- package/dist/Model/Repository/legacy.d.ts.map +1 -1
- package/dist/Model/Repository/makeRepo.d.ts +10 -6
- package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
- package/dist/Model/Repository/makeRepo.js +5 -2
- package/dist/Model/Repository/service.d.ts +32 -24
- package/dist/Model/Repository/service.d.ts.map +1 -1
- package/dist/Model/Repository/validation.d.ts +47 -18
- package/dist/Model/Repository/validation.d.ts.map +1 -1
- package/dist/Model/Repository/validation.js +6 -6
- 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 +6 -5
- package/dist/Model/dsl.d.ts.map +1 -1
- package/dist/Model/dsl.js +2 -3
- 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 +142 -17
- package/dist/Model/query/dsl.d.ts.map +1 -1
- package/dist/Model/query/dsl.js +190 -5
- package/dist/Model/query/new-kid-interpreter.d.ts +77 -8
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
- package/dist/Model/query/new-kid-interpreter.js +127 -6
- 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 +7 -8
- package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
- package/dist/QueueMaker/SQLQueue.js +135 -117
- package/dist/QueueMaker/errors.d.ts +5 -3
- package/dist/QueueMaker/errors.d.ts.map +1 -1
- package/dist/QueueMaker/errors.js +4 -2
- package/dist/QueueMaker/memQueue.d.ts +9 -5
- package/dist/QueueMaker/memQueue.d.ts.map +1 -1
- package/dist/QueueMaker/memQueue.js +81 -65
- package/dist/QueueMaker/sbqueue.d.ts +8 -4
- package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
- package/dist/QueueMaker/sbqueue.js +57 -55
- package/dist/QueueMaker/service.d.ts +4 -2
- package/dist/QueueMaker/service.d.ts.map +1 -1
- package/dist/QueueMaker/service.js +1 -1
- package/dist/RequestContext.d.ts +75 -35
- package/dist/RequestContext.d.ts.map +1 -1
- package/dist/RequestContext.js +14 -14
- package/dist/RequestFiberSet.d.ts +10 -7
- package/dist/RequestFiberSet.d.ts.map +1 -1
- package/dist/RequestFiberSet.js +8 -3
- package/dist/Store/ContextMapContainer.d.ts +22 -3
- package/dist/Store/ContextMapContainer.d.ts.map +1 -1
- package/dist/Store/ContextMapContainer.js +17 -3
- package/dist/Store/Cosmos/query.d.ts +7 -2
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +115 -35
- package/dist/Store/Cosmos.d.ts +2 -2
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +343 -244
- package/dist/Store/Disk.d.ts +3 -3
- package/dist/Store/Disk.d.ts.map +1 -1
- package/dist/Store/Disk.js +76 -36
- package/dist/Store/Memory.d.ts +7 -4
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +251 -58
- 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 +233 -0
- package/dist/Store/SQL/query.d.ts +43 -0
- package/dist/Store/SQL/query.d.ts.map +1 -0
- package/dist/Store/SQL/query.js +478 -0
- package/dist/Store/SQL.d.ts +21 -0
- package/dist/Store/SQL.d.ts.map +1 -0
- package/dist/Store/SQL.js +450 -0
- package/dist/Store/codeFilter.d.ts +2 -2
- package/dist/Store/codeFilter.d.ts.map +1 -1
- package/dist/Store/codeFilter.js +6 -3
- package/dist/Store/index.d.ts +6 -3
- package/dist/Store/index.d.ts.map +1 -1
- package/dist/Store/index.js +18 -4
- package/dist/Store/service.d.ts +26 -8
- package/dist/Store/service.d.ts.map +1 -1
- package/dist/Store/service.js +25 -6
- package/dist/Store/utils.d.ts +3 -2
- package/dist/Store/utils.d.ts.map +1 -1
- package/dist/Store/utils.js +5 -5
- package/dist/Store.d.ts +1 -1
- package/dist/adapters/SQL/Model.d.ts +32 -43
- package/dist/adapters/SQL/Model.d.ts.map +1 -1
- package/dist/adapters/SQL/Model.js +30 -39
- package/dist/adapters/SQL.d.ts +1 -1
- package/dist/adapters/ServiceBus.d.ts +14 -11
- package/dist/adapters/ServiceBus.d.ts.map +1 -1
- package/dist/adapters/ServiceBus.js +30 -21
- package/dist/adapters/cosmos-client.d.ts +5 -3
- package/dist/adapters/cosmos-client.d.ts.map +1 -1
- package/dist/adapters/cosmos-client.js +5 -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 +5 -3
- package/dist/adapters/memQueue.d.ts.map +1 -1
- package/dist/adapters/memQueue.js +6 -5
- package/dist/adapters/mongo-client.d.ts +4 -3
- package/dist/adapters/mongo-client.d.ts.map +1 -1
- package/dist/adapters/mongo-client.js +5 -3
- package/dist/adapters/redis-client.d.ts +6 -3
- package/dist/adapters/redis-client.d.ts.map +1 -1
- package/dist/adapters/redis-client.js +7 -3
- package/dist/api/ContextProvider.d.ts +12 -8
- package/dist/api/ContextProvider.d.ts.map +1 -1
- package/dist/api/ContextProvider.js +9 -7
- package/dist/api/codec.d.ts +1 -1
- package/dist/api/internal/RequestContextMiddleware.d.ts +3 -3
- package/dist/api/internal/RequestContextMiddleware.d.ts.map +1 -1
- package/dist/api/internal/RequestContextMiddleware.js +10 -6
- package/dist/api/internal/auth.d.ts +45 -7
- package/dist/api/internal/auth.d.ts.map +1 -1
- package/dist/api/internal/auth.js +162 -29
- package/dist/api/internal/events.d.ts +6 -4
- package/dist/api/internal/events.d.ts.map +1 -1
- package/dist/api/internal/events.js +16 -9
- package/dist/api/internal/health.d.ts +1 -1
- package/dist/api/layerUtils.d.ts +10 -6
- package/dist/api/layerUtils.d.ts.map +1 -1
- package/dist/api/layerUtils.js +7 -6
- package/dist/api/middlewares.d.ts +1 -1
- package/dist/api/reportError.d.ts +2 -2
- package/dist/api/reportError.d.ts.map +1 -1
- package/dist/api/reportError.js +3 -2
- package/dist/api/routing/middleware/RouterMiddleware.d.ts +5 -4
- package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
- package/dist/api/routing/middleware/middleware.d.ts +42 -3
- package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
- package/dist/api/routing/middleware/middleware.js +53 -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/schema/jwt.js +3 -2
- 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 +4 -4
- package/dist/api/routing/utils.d.ts.map +1 -1
- package/dist/api/routing/utils.js +3 -2
- package/dist/api/routing.d.ts +84 -37
- package/dist/api/routing.d.ts.map +1 -1
- package/dist/api/routing.js +115 -45
- package/dist/api/setupRequest.d.ts +10 -6
- package/dist/api/setupRequest.d.ts.map +1 -1
- package/dist/api/setupRequest.js +15 -7
- package/dist/api/util.d.ts +1 -1
- package/dist/arbs.d.ts +2 -2
- package/dist/arbs.d.ts.map +1 -1
- package/dist/arbs.js +5 -3
- package/dist/errorReporter.d.ts +7 -5
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +22 -26
- package/dist/errors.d.ts +1 -1
- package/dist/fileUtil.d.ts +2 -2
- package/dist/fileUtil.d.ts.map +1 -1
- package/dist/fileUtil.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/logger/jsonLogger.d.ts +2 -2
- package/dist/logger/jsonLogger.d.ts.map +1 -1
- package/dist/logger/jsonLogger.js +4 -2
- package/dist/logger/logFmtLogger.d.ts +2 -2
- package/dist/logger/logFmtLogger.d.ts.map +1 -1
- package/dist/logger/logFmtLogger.js +2 -2
- package/dist/logger/shared.d.ts +2 -2
- package/dist/logger/shared.d.ts.map +1 -1
- package/dist/logger/shared.js +3 -3
- package/dist/logger.d.ts +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/otel.d.ts +75 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +65 -0
- package/dist/rateLimit.d.ts +12 -4
- package/dist/rateLimit.d.ts.map +1 -1
- package/dist/rateLimit.js +7 -12
- package/dist/test.d.ts +3 -3
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +2 -2
- package/dist/vitest.d.ts +1 -1
- package/examples/query.ts +46 -38
- package/package.json +46 -37
- package/src/CUPS.ts +15 -11
- package/src/Emailer/Sendgrid.ts +21 -15
- package/src/Emailer/fake.ts +1 -1
- package/src/Emailer/service.ts +13 -3
- package/src/MainFiberSet.ts +9 -6
- package/src/Model/Repository/Registry.ts +34 -0
- package/src/Model/Repository/ext.ts +103 -11
- package/src/Model/Repository/internal/internal.ts +231 -149
- package/src/Model/Repository/legacy.ts +3 -1
- package/src/Model/Repository/makeRepo.ts +15 -10
- package/src/Model/Repository/service.ts +35 -23
- package/src/Model/Repository/validation.ts +5 -5
- package/src/Model/Repository.ts +1 -0
- package/src/Model/dsl.ts +5 -4
- package/src/Model/filter/types/path/eager.ts +1 -2
- package/src/Model/query/dsl.ts +353 -19
- package/src/Model/query/new-kid-interpreter.ts +211 -6
- package/src/Model.ts +1 -0
- package/src/QueueMaker/SQLQueue.ts +150 -153
- package/src/QueueMaker/errors.ts +3 -1
- package/src/QueueMaker/memQueue.ts +111 -105
- package/src/QueueMaker/sbqueue.ts +76 -88
- package/src/QueueMaker/service.ts +3 -1
- package/src/RequestContext.ts +15 -16
- package/src/RequestFiberSet.ts +8 -2
- package/src/Store/ContextMapContainer.ts +45 -2
- package/src/Store/Cosmos/query.ts +143 -44
- package/src/Store/Cosmos.ts +491 -350
- package/src/Store/Disk.ts +106 -66
- package/src/Store/Memory.ts +285 -87
- package/src/Store/SQL/Pg.ts +364 -0
- package/src/Store/SQL/query.ts +540 -0
- package/src/Store/SQL.ts +736 -0
- package/src/Store/codeFilter.ts +5 -2
- package/src/Store/index.ts +20 -3
- package/src/Store/service.ts +45 -10
- package/src/Store/utils.ts +25 -23
- package/src/adapters/SQL/Model.ts +42 -41
- package/src/adapters/ServiceBus.ts +131 -121
- package/src/adapters/cosmos-client.ts +4 -2
- package/src/adapters/index.ts +7 -0
- package/src/adapters/memQueue.ts +5 -4
- package/src/adapters/mongo-client.ts +4 -2
- package/src/adapters/redis-client.ts +6 -2
- package/src/api/ContextProvider.ts +17 -13
- package/src/api/internal/RequestContextMiddleware.ts +16 -5
- package/src/api/internal/auth.ts +248 -44
- package/src/api/internal/events.ts +19 -10
- package/src/api/layerUtils.ts +12 -8
- package/src/api/reportError.ts +2 -1
- package/src/api/routing/middleware/RouterMiddleware.ts +5 -4
- package/src/api/routing/middleware/middleware.ts +60 -15
- package/src/api/routing/middleware.ts +0 -2
- package/src/api/routing/schema/jwt.ts +2 -1
- package/src/api/routing/utils.ts +2 -1
- package/src/api/routing.ts +304 -131
- package/src/api/setupRequest.ts +31 -8
- package/src/arbs.ts +5 -3
- package/src/errorReporter.ts +65 -75
- package/src/fileUtil.ts +1 -1
- package/src/logger/jsonLogger.ts +3 -1
- package/src/logger/logFmtLogger.ts +1 -1
- package/src/logger/shared.ts +3 -2
- package/src/otel.ts +152 -0
- package/src/rateLimit.ts +34 -23
- package/src/test.ts +2 -2
- package/test/auth.test.ts +101 -0
- package/test/contextProvider.test.ts +14 -11
- package/test/controller.test.ts +25 -29
- 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/date-query.test.d.ts.map +1 -0
- package/test/dist/fixtures.d.ts +30 -12
- package/test/dist/fixtures.d.ts.map +1 -1
- package/test/dist/fixtures.js +17 -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 +16 -9
- package/test/layerUtils.test.ts +1 -1
- package/test/query.test.ts +819 -38
- package/test/rawQuery.test.ts +312 -20
- package/test/repository-ext.test.ts +62 -0
- package/test/requires.test.ts +10 -5
- package/test/router-generator.test.ts +187 -0
- package/test/routing-interruptibility.test.ts +66 -0
- package/test/rpc-e2e-invalidation.test.ts +256 -0
- package/test/rpc-multi-middleware.test.ts +84 -9
- package/test/rpc-stream-fullstack.test.ts +304 -0
- package/test/sql-store.test.ts +1592 -0
- package/test/validateSample.test.ts +17 -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,187 @@
|
|
|
1
|
+
import { type MakeContext, type MakeErrors, makeRouter } from "@effect-app/infra/api/routing"
|
|
2
|
+
import { makeAllDSL, makeOneDSL } from "@effect-app/infra/Model"
|
|
3
|
+
import { expectTypeOf, it } from "@effect/vitest"
|
|
4
|
+
import { InvalidStateError, makeRpcClient, UnauthorizedError } from "effect-app/client"
|
|
5
|
+
import * as Context from "effect-app/Context"
|
|
6
|
+
import * as Effect from "effect-app/Effect"
|
|
7
|
+
import * as Layer from "effect-app/Layer"
|
|
8
|
+
import { DefaultGenericMiddlewares } from "effect-app/middleware"
|
|
9
|
+
import { type FixEnv } from "effect-app/Pure"
|
|
10
|
+
import * as RpcX from "effect-app/rpc"
|
|
11
|
+
import { MiddlewareMaker } from "effect-app/rpc"
|
|
12
|
+
import * as S from "effect-app/Schema"
|
|
13
|
+
import { type TypeTestId } from "effect-app/TypeTest"
|
|
14
|
+
import { type ConfigError } from "effect/Config"
|
|
15
|
+
import { type RpcSerialization } from "effect/unstable/rpc/RpcSerialization"
|
|
16
|
+
import { DefaultGenericMiddlewaresLive, DevModeMiddlewareLive } from "../src/api/routing/middleware.js"
|
|
17
|
+
import { AllowAnonymous, AllowAnonymousLive, RequestContextMap, RequireRoles, RequireRolesLive, Some, SomeElse, SomeService, Test, TestLive } from "./fixtures.js"
|
|
18
|
+
|
|
19
|
+
// Inline minimal context provider (provides `Some`)
|
|
20
|
+
class CtxProvider extends RpcX.RpcMiddleware.Tag<CtxProvider, { provides: Some }>()("CtxProvider") {
|
|
21
|
+
static Default = Layer.make(this, {
|
|
22
|
+
*make() {
|
|
23
|
+
return Effect.fnUntraced(function*(effect) {
|
|
24
|
+
return yield* Effect.provideService(effect, Some, Some.of({ a: 1 }))
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Provides `SomeElse` so AllowAnonymous's requirement is met.
|
|
31
|
+
class SomeElseProvider extends RpcX.RpcMiddleware.Tag<SomeElseProvider, { provides: SomeElse }>()("SomeElseProvider") {
|
|
32
|
+
static Default = Layer.make(this, {
|
|
33
|
+
*make() {
|
|
34
|
+
return Effect.fnUntraced(function*(effect) {
|
|
35
|
+
return yield* Effect.provideService(effect, SomeElse, SomeElse.of({ b: 2 }))
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class mw extends MiddlewareMaker
|
|
42
|
+
.Tag<mw>()("mw", RequestContextMap)
|
|
43
|
+
.middleware(RequireRoles, Test)
|
|
44
|
+
.middleware(AllowAnonymous)
|
|
45
|
+
.middleware(CtxProvider)
|
|
46
|
+
.middleware(...DefaultGenericMiddlewares, SomeElseProvider)
|
|
47
|
+
{
|
|
48
|
+
static Default = this.layer.pipe(
|
|
49
|
+
Layer.provide([
|
|
50
|
+
RequireRolesLive,
|
|
51
|
+
TestLive,
|
|
52
|
+
AllowAnonymousLive,
|
|
53
|
+
CtxProvider.Default,
|
|
54
|
+
SomeElseProvider.Default,
|
|
55
|
+
DefaultGenericMiddlewaresLive,
|
|
56
|
+
DevModeMiddlewareLive,
|
|
57
|
+
SomeService.Default
|
|
58
|
+
])
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { TaggedRequestFor } = makeRpcClient(mw)
|
|
63
|
+
const Req = TaggedRequestFor("GenRouter")
|
|
64
|
+
|
|
65
|
+
class GetThing extends Req.Query<GetThing>()("GetThing", { id: S.String }, {
|
|
66
|
+
success: S.String,
|
|
67
|
+
error: UnauthorizedError
|
|
68
|
+
}) {}
|
|
69
|
+
class DoThing extends Req.Command<DoThing>()("DoThing", { id: S.String }, { success: S.Void }) {}
|
|
70
|
+
|
|
71
|
+
const Resource = { GetThing, DoThing }
|
|
72
|
+
|
|
73
|
+
const { Router, matchAll } = makeRouter(mw.Default)
|
|
74
|
+
|
|
75
|
+
class ThingRepo extends Context.Service<ThingRepo>()("ThingRepo", {
|
|
76
|
+
make: Effect.succeed({ get: (id: string) => Effect.succeed(id + "!") })
|
|
77
|
+
}) {
|
|
78
|
+
static Default = Layer.effect(this, this.make)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Case under test:
|
|
82
|
+
// `match({})` is given handlers as **shorthand generator methods** (`*GetThing(req) { ... }`).
|
|
83
|
+
// tsgo (>= 7 dev) infers `TNext = unknown` for these shorthand generators while TS6 infers `never`.
|
|
84
|
+
// `HandlerWithInputGen` in routing.ts must accept both — see the structural fix.
|
|
85
|
+
const router = Router(Resource)({
|
|
86
|
+
dependencies: [ThingRepo.Default],
|
|
87
|
+
*effect(match) {
|
|
88
|
+
const repo = yield* ThingRepo
|
|
89
|
+
|
|
90
|
+
if (Math.random() > 0.5) return yield* new InvalidStateError("nope")
|
|
91
|
+
|
|
92
|
+
return match({
|
|
93
|
+
*GetThing(req) {
|
|
94
|
+
const some = yield* Some
|
|
95
|
+
if (req.id === "boom") {
|
|
96
|
+
return yield* Effect.fail(new UnauthorizedError())
|
|
97
|
+
}
|
|
98
|
+
return yield* repo.get(req.id + String(some.a))
|
|
99
|
+
},
|
|
100
|
+
*DoThing(_req) {
|
|
101
|
+
yield* Effect.succeed(1)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Same scenario but using the `raw:` variant — exercises the `raw` path of `HandlerWithInputGen`.
|
|
108
|
+
const routerRaw = Router({ GetThing })({
|
|
109
|
+
*effect(match) {
|
|
110
|
+
return match({
|
|
111
|
+
GetThing: {
|
|
112
|
+
*raw(req) {
|
|
113
|
+
const some = yield* Some
|
|
114
|
+
return yield* Effect.succeed(req.id + String(some.a))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("router with generator-method handlers compiles", () => {
|
|
122
|
+
expectTypeOf(router).toMatchTypeOf<
|
|
123
|
+
Layer.Layer<never, ConfigError | InvalidStateError, SomeService | RpcSerialization>
|
|
124
|
+
>()
|
|
125
|
+
expectTypeOf(routerRaw).toMatchTypeOf<Layer.Layer<never, ConfigError, SomeService | RpcSerialization>>()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Type-level assertions: verify generator yields propagate to MakeErrors / MakeContext
|
|
129
|
+
type Errors = MakeErrors<typeof router[TypeTestId]>
|
|
130
|
+
type Ctx = MakeContext<typeof router[TypeTestId]>
|
|
131
|
+
expectTypeOf<Errors>().toEqualTypeOf<InvalidStateError>()
|
|
132
|
+
expectTypeOf<Ctx>().toEqualTypeOf<ThingRepo>()
|
|
133
|
+
|
|
134
|
+
const matched = matchAll({ router })
|
|
135
|
+
expectTypeOf(matched).toMatchTypeOf<
|
|
136
|
+
Layer.Layer<never, ConfigError | InvalidStateError, SomeService | RpcSerialization>
|
|
137
|
+
>()
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// DSL R-inference regression
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// `OneDSL`/`OneDSLExt.update`/`.modify` previously annotated the callback's
|
|
143
|
+
// effect R as `FixEnv<R, Evt, S1, S2>`. That deadlocked inference of `R`
|
|
144
|
+
// (TS6 → `never`, tsgo → `unknown`), causing yielded effects to leak
|
|
145
|
+
// `unknown` in the R slot when consumed by generator handlers.
|
|
146
|
+
// The fix uses bare `R` in the callback and `FixEnv<R, …>` only on the return.
|
|
147
|
+
class Item extends S.Class<Item>("Item")({ id: S.String, label: S.String }) {}
|
|
148
|
+
class Dep extends Context.Service<Dep>()("Dep", { make: Effect.succeed({ tag: "dep" as const }) }) {}
|
|
149
|
+
|
|
150
|
+
type Evt = { _tag: "Updated"; id: string }
|
|
151
|
+
|
|
152
|
+
const Items$ = makeAllDSL<Item, Evt>()
|
|
153
|
+
const Item$ = makeOneDSL<Item, Evt>()
|
|
154
|
+
|
|
155
|
+
// Callback body uses generator syntax (TNext = unknown under tsgo) and yields
|
|
156
|
+
// a service-dependent effect — R must be inferred as `Dep` (plus the
|
|
157
|
+
// canonical PureEnvEnv contributed by FixEnv on the return).
|
|
158
|
+
const oneUpdate = Item$.update((item) =>
|
|
159
|
+
Effect.gen(function*() {
|
|
160
|
+
const dep = yield* Dep
|
|
161
|
+
return new Item({ id: item.id, label: item.label + dep.tag })
|
|
162
|
+
})
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const allUpdate = Items$.update((items) =>
|
|
166
|
+
Effect.gen(function*() {
|
|
167
|
+
const dep = yield* Dep
|
|
168
|
+
return items.map((_) => new Item({ id: _.id, label: _.label + dep.tag }))
|
|
169
|
+
})
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const oneModify = Item$.modify((item, _dsl) =>
|
|
173
|
+
Effect.gen(function*() {
|
|
174
|
+
const dep = yield* Dep
|
|
175
|
+
return { ...item, tag: dep.tag }
|
|
176
|
+
})
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
// `R` should be `FixEnv<Dep, Evt, …>` — never collapsed to `unknown`/`never`.
|
|
180
|
+
// The regression manifested as `unknown` here, breaking `Dep` assignability.
|
|
181
|
+
expectTypeOf(oneUpdate).toMatchTypeOf<Effect.Effect<Item, never, FixEnv<Dep, Evt, Item, Item>>>()
|
|
182
|
+
expectTypeOf(allUpdate).toMatchTypeOf<
|
|
183
|
+
Effect.Effect<readonly Item[], never, FixEnv<Dep, Evt, readonly Item[], readonly Item[]>>
|
|
184
|
+
>()
|
|
185
|
+
expectTypeOf(oneModify).toMatchTypeOf<
|
|
186
|
+
Effect.Effect<{ tag: "dep"; id: string; label: string }, never, FixEnv<Dep, Evt, Item, Item>>
|
|
187
|
+
>()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
2
|
+
import { ConfigureInterruptibilityMiddleware } from "effect-app/middleware"
|
|
3
|
+
import * as S from "effect-app/Schema"
|
|
4
|
+
import * as Effect from "effect/Effect"
|
|
5
|
+
import * as Fiber from "effect/Fiber"
|
|
6
|
+
import * as Layer from "effect/Layer"
|
|
7
|
+
import * as Ref from "effect/Ref"
|
|
8
|
+
import { Rpc, RpcGroup, RpcTest } from "effect/unstable/rpc"
|
|
9
|
+
import { applyRequestTypeInterruptibility } from "../src/api/routing.js"
|
|
10
|
+
import { ConfigureInterruptibilityMiddlewareLive, RequestType } from "../src/api/routing/middleware.js"
|
|
11
|
+
|
|
12
|
+
const InterruptibilityRpcs = RpcGroup.make(
|
|
13
|
+
Rpc
|
|
14
|
+
.make("doCommand", { success: S.Void })
|
|
15
|
+
.annotate(RequestType, "command")
|
|
16
|
+
.middleware(ConfigureInterruptibilityMiddleware),
|
|
17
|
+
Rpc
|
|
18
|
+
.make("doQuery", { success: S.Void })
|
|
19
|
+
.annotate(RequestType, "query")
|
|
20
|
+
.middleware(ConfigureInterruptibilityMiddleware)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const makeImplLayer = (commandDone: Ref.Ref<boolean>, queryDone: Ref.Ref<boolean>) =>
|
|
24
|
+
InterruptibilityRpcs.toLayer({
|
|
25
|
+
doCommand: () =>
|
|
26
|
+
applyRequestTypeInterruptibility(
|
|
27
|
+
"command",
|
|
28
|
+
Effect.sleep("120 millis").pipe(Effect.andThen(Ref.set(commandDone, true)))
|
|
29
|
+
),
|
|
30
|
+
doQuery: () =>
|
|
31
|
+
applyRequestTypeInterruptibility(
|
|
32
|
+
"query",
|
|
33
|
+
Effect.sleep("120 millis").pipe(Effect.andThen(Ref.set(queryDone, true)))
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe("routing interruptibility", () => {
|
|
38
|
+
it.live(
|
|
39
|
+
"e2e: command continues after client interrupt, query does not",
|
|
40
|
+
() =>
|
|
41
|
+
Effect.gen(function*() {
|
|
42
|
+
const commandDone = yield* Ref.make(false)
|
|
43
|
+
const queryDone = yield* Ref.make(false)
|
|
44
|
+
|
|
45
|
+
const client = yield* RpcTest
|
|
46
|
+
.makeClient(InterruptibilityRpcs)
|
|
47
|
+
.pipe(
|
|
48
|
+
Effect.provide(
|
|
49
|
+
Layer.mergeAll(makeImplLayer(commandDone, queryDone), ConfigureInterruptibilityMiddlewareLive)
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const commandFiber = yield* Effect.forkDetach(client.doCommand())
|
|
54
|
+
yield* Effect.sleep("20 millis")
|
|
55
|
+
yield* Fiber.interrupt(commandFiber)
|
|
56
|
+
yield* Effect.sleep("180 millis")
|
|
57
|
+
expect(yield* Ref.get(commandDone)).toBe(true)
|
|
58
|
+
|
|
59
|
+
const queryFiber = yield* Effect.forkDetach(client.doQuery())
|
|
60
|
+
yield* Effect.sleep("20 millis")
|
|
61
|
+
yield* Fiber.interrupt(queryFiber)
|
|
62
|
+
yield* Effect.sleep("180 millis")
|
|
63
|
+
expect(yield* Ref.get(queryDone)).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
})
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E tests for the invalidation key flow exercised end-to-end via the
|
|
3
|
+
* production wrap/unwrap path:
|
|
4
|
+
*
|
|
5
|
+
* server: routing.ts wraps command success with `CommandResponseWithMetaData`
|
|
6
|
+
* and handler-thrown failure with `CommandFailureWithMetaData`;
|
|
7
|
+
* routing wraps stream values into `{_tag:"value"|"metadata"|"done"}`
|
|
8
|
+
* chunks. `InvalidationSet.use(...)` inside a handler accumulates keys.
|
|
9
|
+
* client: apiClientFactory unwraps both envelopes and forwards keys to
|
|
10
|
+
* `InvalidationKeysFromServer`.
|
|
11
|
+
*
|
|
12
|
+
* Transport is real HTTP (NodeHttpServer on a loopback port) so the wire
|
|
13
|
+
* encoding is exercised too.
|
|
14
|
+
*/
|
|
15
|
+
import { NodeHttpServer } from "@effect/platform-node"
|
|
16
|
+
import { expect, it } from "@effect/vitest"
|
|
17
|
+
import { ApiClientFactory, InvalidationKeysFromServer, makeInvalidationKeysService, makeRpcClient } from "effect-app/client"
|
|
18
|
+
import { HttpRouter, HttpServer } from "effect-app/http"
|
|
19
|
+
import { DefaultGenericMiddlewares } from "effect-app/middleware"
|
|
20
|
+
import { Invalidation, MiddlewareMaker } from "effect-app/rpc"
|
|
21
|
+
import * as S from "effect-app/Schema"
|
|
22
|
+
import { TaggedErrorClass } from "effect-app/Schema"
|
|
23
|
+
import * as Effect from "effect/Effect"
|
|
24
|
+
import * as Exit from "effect/Exit"
|
|
25
|
+
import * as Layer from "effect/Layer"
|
|
26
|
+
import * as Option from "effect/Option"
|
|
27
|
+
import * as Ref from "effect/Ref"
|
|
28
|
+
import * as Stream from "effect/Stream"
|
|
29
|
+
import { FetchHttpClient } from "effect/unstable/http"
|
|
30
|
+
import { RpcSerialization } from "effect/unstable/rpc"
|
|
31
|
+
import { createServer } from "http"
|
|
32
|
+
import { makeRouter } from "../src/api/routing.js"
|
|
33
|
+
import { DefaultGenericMiddlewaresLive } from "../src/api/routing/middleware.js"
|
|
34
|
+
import { AllowAnonymous, AllowAnonymousLive, RequestContextMap, RequireRoles, RequireRolesLive, SomeElseMiddleware, SomeElseMiddlewareLive, SomeService, Test, TestLive } from "./fixtures.js"
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Middleware (mirrors AppMiddleware shape — same composite as other e2e tests).
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
class AppMiddleware extends MiddlewareMaker
|
|
41
|
+
.Tag<AppMiddleware>()("AppMiddleware", RequestContextMap)
|
|
42
|
+
.middleware(RequireRoles, Test)
|
|
43
|
+
.middleware(AllowAnonymous)
|
|
44
|
+
.middleware(SomeElseMiddleware)
|
|
45
|
+
.middleware(...DefaultGenericMiddlewares)
|
|
46
|
+
{
|
|
47
|
+
static Default = this.layer.pipe(
|
|
48
|
+
Layer.provide(
|
|
49
|
+
[
|
|
50
|
+
RequireRolesLive.pipe(Layer.provide(SomeService.Default)),
|
|
51
|
+
AllowAnonymousLive,
|
|
52
|
+
TestLive,
|
|
53
|
+
SomeElseMiddlewareLive,
|
|
54
|
+
DefaultGenericMiddlewaresLive
|
|
55
|
+
] as const
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { Router, matchAll } = makeRouter(AppMiddleware.Default)
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Resources
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
const DynamicKey: Invalidation.InvalidationKey = ["dynamic", "key"]
|
|
67
|
+
const ExtraKey: Invalidation.InvalidationKey = ["extra", "key"]
|
|
68
|
+
const StreamKey: Invalidation.InvalidationKey = ["stream", "key"]
|
|
69
|
+
|
|
70
|
+
const { TaggedRequestFor } = makeRpcClient(AppMiddleware)
|
|
71
|
+
const Req = TaggedRequestFor("Inv")
|
|
72
|
+
|
|
73
|
+
class CmdBoom extends TaggedErrorClass<CmdBoom>()("CmdBoom", { reason: S.String }) {}
|
|
74
|
+
|
|
75
|
+
class DoNothing extends Req.Command<DoNothing>()("DoNothing", {}, {
|
|
76
|
+
allowAnonymous: true,
|
|
77
|
+
success: S.Void
|
|
78
|
+
}) {}
|
|
79
|
+
|
|
80
|
+
class DoWithDynamicKey extends Req.Command<DoWithDynamicKey>()("DoWithDynamicKey", {}, {
|
|
81
|
+
allowAnonymous: true,
|
|
82
|
+
success: S.String
|
|
83
|
+
}) {}
|
|
84
|
+
|
|
85
|
+
class DoWithBothKeys extends Req.Command<DoWithBothKeys>()("DoWithBothKeys", {}, {
|
|
86
|
+
allowAnonymous: true,
|
|
87
|
+
success: S.Number
|
|
88
|
+
}) {}
|
|
89
|
+
|
|
90
|
+
class DoAndFail extends Req.Command<DoAndFail>()("DoAndFail", {}, {
|
|
91
|
+
allowAnonymous: true,
|
|
92
|
+
success: S.Void,
|
|
93
|
+
error: CmdBoom
|
|
94
|
+
}) {}
|
|
95
|
+
|
|
96
|
+
class StreamWithKey extends Req.Command<StreamWithKey>()("StreamWithKey", {}, {
|
|
97
|
+
stream: true,
|
|
98
|
+
allowAnonymous: true,
|
|
99
|
+
success: S.Number
|
|
100
|
+
}) {}
|
|
101
|
+
|
|
102
|
+
const InvRsc = { DoNothing, DoWithDynamicKey, DoWithBothKeys, DoAndFail, StreamWithKey }
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Controllers / router
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
const router = Router(InvRsc)({
|
|
109
|
+
*effect(match) {
|
|
110
|
+
return match({
|
|
111
|
+
DoNothing: () => Effect.void,
|
|
112
|
+
DoWithDynamicKey: Effect.fnUntraced(function*() {
|
|
113
|
+
yield* Invalidation.InvalidationSet.use((_) => _.add(DynamicKey))
|
|
114
|
+
return "done"
|
|
115
|
+
}),
|
|
116
|
+
DoWithBothKeys: Effect.fnUntraced(function*() {
|
|
117
|
+
yield* Invalidation.InvalidationSet.use((_) => _.add(DynamicKey))
|
|
118
|
+
yield* Invalidation.InvalidationSet.use((_) => _.add(ExtraKey))
|
|
119
|
+
return 99
|
|
120
|
+
}),
|
|
121
|
+
DoAndFail: Effect.fnUntraced(function*() {
|
|
122
|
+
yield* Invalidation.InvalidationSet.use((_) => _.add(DynamicKey))
|
|
123
|
+
return yield* Effect.fail(new CmdBoom({ reason: "intentional failure" }))
|
|
124
|
+
}),
|
|
125
|
+
StreamWithKey: () =>
|
|
126
|
+
Stream.fromIterable([1, 2, 3]).pipe(
|
|
127
|
+
Stream.tap(() => Invalidation.InvalidationSet.use((_) => _.add(StreamKey)))
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const RpcRouterLayer = matchAll({ router })
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// HTTP wiring — fresh server on loopback per `it.live`.
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
const NodeServerLayer = NodeHttpServer.layer(() => createServer(), { port: 0 })
|
|
140
|
+
|
|
141
|
+
const ServerLayer = HttpRouter
|
|
142
|
+
.serve(RpcRouterLayer)
|
|
143
|
+
.pipe(
|
|
144
|
+
Layer.provide(NodeServerLayer),
|
|
145
|
+
Layer.provide(RpcSerialization.layerNdjson)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const ClientLayer = Layer
|
|
149
|
+
.unwrap(
|
|
150
|
+
Effect.gen(function*() {
|
|
151
|
+
const server = yield* HttpServer.HttpServer
|
|
152
|
+
const addr = server.address
|
|
153
|
+
if (addr._tag !== "TcpAddress") return yield* Effect.die(new Error("expected TcpAddress"))
|
|
154
|
+
const host = addr.hostname === "0.0.0.0" ? "127.0.0.1" : addr.hostname
|
|
155
|
+
const url = `http://${host}:${addr.port}`
|
|
156
|
+
return ApiClientFactory
|
|
157
|
+
.layer({ url, headers: Option.none() })
|
|
158
|
+
.pipe(Layer.provide(FetchHttpClient.layer))
|
|
159
|
+
})
|
|
160
|
+
)
|
|
161
|
+
.pipe(Layer.provide(NodeServerLayer))
|
|
162
|
+
|
|
163
|
+
const TestLayer = Layer.mergeAll(ServerLayer, ClientLayer)
|
|
164
|
+
|
|
165
|
+
// Helper: provide a fresh `InvalidationKeysFromServer` and capture forwarded keys.
|
|
166
|
+
const withCapture = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
|
|
167
|
+
Effect.gen(function*() {
|
|
168
|
+
const ref = yield* Ref.make<ReadonlyArray<Invalidation.InvalidationKey>>([])
|
|
169
|
+
const svc = makeInvalidationKeysService(ref)
|
|
170
|
+
const result = yield* eff.pipe(Effect.provideService(InvalidationKeysFromServer, svc), Effect.exit)
|
|
171
|
+
return { result, keys: yield* Ref.get(ref) }
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Tests
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
it.live(
|
|
179
|
+
"command with no invalidation keys: caller sees raw payload, no keys forwarded",
|
|
180
|
+
Effect.fnUntraced(function*() {
|
|
181
|
+
const client = yield* ApiClientFactory.makeFor(Layer.empty)(InvRsc)
|
|
182
|
+
const { result, keys } = yield* withCapture(client.DoNothing.handler())
|
|
183
|
+
expect(Exit.isSuccess(result)).toBe(true)
|
|
184
|
+
expect(keys).toStrictEqual([])
|
|
185
|
+
}, Effect.provide(TestLayer)),
|
|
186
|
+
{ timeout: 10_000 }
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
it.live(
|
|
190
|
+
"command with dynamic InvalidationSet.use: payload + key forwarded",
|
|
191
|
+
Effect.fnUntraced(function*() {
|
|
192
|
+
const client = yield* ApiClientFactory.makeFor(Layer.empty)(InvRsc)
|
|
193
|
+
const { result, keys } = yield* withCapture(client.DoWithDynamicKey.handler())
|
|
194
|
+
expect(Exit.isSuccess(result) && result.value).toBe("done")
|
|
195
|
+
expect(keys).toStrictEqual([DynamicKey])
|
|
196
|
+
}, Effect.provide(TestLayer)),
|
|
197
|
+
{ timeout: 10_000 }
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
it.live(
|
|
201
|
+
"command accumulating multiple dynamic keys: all keys forwarded in order",
|
|
202
|
+
Effect.fnUntraced(function*() {
|
|
203
|
+
const client = yield* ApiClientFactory.makeFor(Layer.empty)(InvRsc)
|
|
204
|
+
const { result, keys } = yield* withCapture(client.DoWithBothKeys.handler())
|
|
205
|
+
expect(Exit.isSuccess(result) && result.value).toBe(99)
|
|
206
|
+
expect(keys).toStrictEqual([DynamicKey, ExtraKey])
|
|
207
|
+
}, Effect.provide(TestLayer)),
|
|
208
|
+
{ timeout: 10_000 }
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
it.live(
|
|
212
|
+
"per-request isolation: each command call starts with a fresh InvalidationSet",
|
|
213
|
+
Effect.fnUntraced(function*() {
|
|
214
|
+
const client = yield* ApiClientFactory.makeFor(Layer.empty)(InvRsc)
|
|
215
|
+
const r1 = yield* withCapture(client.DoWithDynamicKey.handler())
|
|
216
|
+
const r2 = yield* withCapture(client.DoWithDynamicKey.handler())
|
|
217
|
+
// Each call must have exactly one key — no accumulation across calls
|
|
218
|
+
expect(r1.keys).toStrictEqual([DynamicKey])
|
|
219
|
+
expect(r2.keys).toStrictEqual([DynamicKey])
|
|
220
|
+
}, Effect.provide(TestLayer)),
|
|
221
|
+
{ timeout: 10_000 }
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
it.live(
|
|
225
|
+
"command failure (V2): keys accumulated before fail still reach the client; original error re-thrown",
|
|
226
|
+
Effect.fnUntraced(function*() {
|
|
227
|
+
const client = yield* ApiClientFactory.makeFor(Layer.empty)(InvRsc)
|
|
228
|
+
const { result, keys } = yield* withCapture(client.DoAndFail.handler())
|
|
229
|
+
expect(Exit.isFailure(result)).toBe(true)
|
|
230
|
+
if (Exit.isFailure(result)) {
|
|
231
|
+
const failures = (result.cause as any).reasons as ReadonlyArray<{ _tag: "Fail"; error: any }>
|
|
232
|
+
expect(failures[0]?.error?._tag).toBe("CmdBoom")
|
|
233
|
+
expect(failures[0]?.error?.reason).toBe("intentional failure")
|
|
234
|
+
}
|
|
235
|
+
expect(keys).toStrictEqual([DynamicKey])
|
|
236
|
+
}, Effect.provide(TestLayer)),
|
|
237
|
+
{ timeout: 10_000 }
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
it.live(
|
|
241
|
+
"stream: per-chunk metadata drains keys mid-stream",
|
|
242
|
+
Effect.fnUntraced(function*() {
|
|
243
|
+
const client = yield* ApiClientFactory.makeFor(Layer.empty)(InvRsc)
|
|
244
|
+
const ref = yield* Ref.make<ReadonlyArray<Invalidation.InvalidationKey>>([])
|
|
245
|
+
const svc = makeInvalidationKeysService(ref)
|
|
246
|
+
const values = yield* Stream.runCollect(client.StreamWithKey.handler()).pipe(
|
|
247
|
+
Effect.provideService(InvalidationKeysFromServer, svc)
|
|
248
|
+
)
|
|
249
|
+
const keys = yield* Ref.get(ref)
|
|
250
|
+
expect(values).toStrictEqual([1, 2, 3])
|
|
251
|
+
// Handler taps `InvalidationSet.use` once per emitted value; routing's V3 mid-stream
|
|
252
|
+
// metadata drain forwards each batch as it arrives.
|
|
253
|
+
expect(keys).toStrictEqual([StreamKey, StreamKey, StreamKey])
|
|
254
|
+
}, Effect.provide(TestLayer)),
|
|
255
|
+
{ timeout: 10_000 }
|
|
256
|
+
)
|
|
@@ -1,19 +1,24 @@
|
|
|
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"
|
|
5
3
|
import { NotLoggedInError } from "effect-app/client"
|
|
4
|
+
import * as Context from "effect-app/Context"
|
|
6
5
|
import { HttpRouter } from "effect-app/http"
|
|
7
6
|
import { DefaultGenericMiddlewares } from "effect-app/middleware"
|
|
8
|
-
import
|
|
9
|
-
import
|
|
7
|
+
import * as RpcX from "effect-app/rpc"
|
|
8
|
+
import * as S from "effect-app/Schema"
|
|
9
|
+
import * as Console from "effect/Console"
|
|
10
|
+
import * as Effect from "effect/Effect"
|
|
11
|
+
import * as Layer from "effect/Layer"
|
|
12
|
+
import * as Ref from "effect/Ref"
|
|
13
|
+
import * as Result from "effect/Result"
|
|
10
14
|
import { FetchHttpClient } from "effect/unstable/http"
|
|
11
|
-
import { RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc"
|
|
15
|
+
import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc"
|
|
12
16
|
import { createServer } from "http"
|
|
13
17
|
import { DefaultGenericMiddlewaresLive } from "../src/api/routing.js"
|
|
14
18
|
import { AllowAnonymous, AllowAnonymousLive, RequestContextMap, RequireRoles, RequireRolesLive, Some, SomeElseMiddleware, SomeElseMiddlewareLive, SomeMiddleware, SomeMiddlewareLive, SomeService, Test, TestLive, UserProfile } from "./fixtures.js"
|
|
15
19
|
|
|
16
|
-
const incomplete =
|
|
20
|
+
const incomplete = RpcX
|
|
21
|
+
.MiddlewareMaker
|
|
17
22
|
.Tag<middleware>()("MiddlewareMaker", RequestContextMap)
|
|
18
23
|
.middleware(RequireRoles)
|
|
19
24
|
.middleware(AllowAnonymous, Test)
|
|
@@ -21,7 +26,8 @@ const incomplete = MiddlewareMaker
|
|
|
21
26
|
// this extension is allowed otherwise the error is quite obscure
|
|
22
27
|
export class incompleteMiddleware extends incomplete {}
|
|
23
28
|
|
|
24
|
-
class middleware extends
|
|
29
|
+
class middleware extends RpcX
|
|
30
|
+
.MiddlewareMaker
|
|
25
31
|
.Tag<middleware>()("MiddlewareMaker", RequestContextMap)
|
|
26
32
|
.middleware(RequireRoles)
|
|
27
33
|
.middleware(AllowAnonymous, Test)
|
|
@@ -29,7 +35,7 @@ class middleware extends MiddlewareMaker
|
|
|
29
35
|
.middleware(...DefaultGenericMiddlewares)
|
|
30
36
|
{}
|
|
31
37
|
|
|
32
|
-
const UserRpcs = middlewareGroup(middleware)(
|
|
38
|
+
const UserRpcs = RpcX.MiddlewareMaker.middlewareGroup(middleware)(
|
|
33
39
|
RpcGroup.make(
|
|
34
40
|
middleware.rpc("getUser", {
|
|
35
41
|
success: S.Literal("awesome")
|
|
@@ -56,7 +62,7 @@ const impl = UserRpcs
|
|
|
56
62
|
|
|
57
63
|
expectTypeOf<Layer.Services<typeof impl>>().toEqualTypeOf<never>()
|
|
58
64
|
|
|
59
|
-
const UserRpcsBad = middlewareGroup(middleware)(
|
|
65
|
+
const UserRpcsBad = RpcX.MiddlewareMaker.middlewareGroup(middleware)(
|
|
60
66
|
RpcGroup.make(
|
|
61
67
|
middleware.rpc("doSomethingElse", {
|
|
62
68
|
success: S.Literal("also-awesome2"),
|
|
@@ -136,3 +142,72 @@ it.live(
|
|
|
136
142
|
Effect.provide(RpcTestLayer)
|
|
137
143
|
)
|
|
138
144
|
)
|
|
145
|
+
|
|
146
|
+
// Per-request service isolation test
|
|
147
|
+
|
|
148
|
+
class PerRequestCounter extends Context.Service<PerRequestCounter>()(
|
|
149
|
+
"PerRequestCounter",
|
|
150
|
+
{ make: Effect.sync(() => ({ a: 0 })) }
|
|
151
|
+
) {
|
|
152
|
+
static Default = Layer.effect(this, this.make)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
class GlobalCounter extends Context.Service<GlobalCounter, {
|
|
156
|
+
readonly ref: Ref.Ref<number>
|
|
157
|
+
}>()("GlobalCounter") {}
|
|
158
|
+
|
|
159
|
+
const CounterRpcs = RpcGroup.make(
|
|
160
|
+
Rpc.make("incrementA", {
|
|
161
|
+
success: S.Number
|
|
162
|
+
}),
|
|
163
|
+
Rpc.make("incrementB", {
|
|
164
|
+
success: S.Number
|
|
165
|
+
})
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
const counterImpl = CounterRpcs
|
|
169
|
+
.toLayer({
|
|
170
|
+
incrementA: Effect.fn(function*() {
|
|
171
|
+
const counter = yield* PerRequestCounter
|
|
172
|
+
counter.a++
|
|
173
|
+
const global = yield* GlobalCounter
|
|
174
|
+
yield* Ref.update(global.ref, (n) => n + 1)
|
|
175
|
+
return counter.a
|
|
176
|
+
}, Effect.provide(PerRequestCounter.Default)),
|
|
177
|
+
incrementB: Effect.fn(function*() {
|
|
178
|
+
const counter = yield* PerRequestCounter
|
|
179
|
+
counter.a++
|
|
180
|
+
const global = yield* GlobalCounter
|
|
181
|
+
yield* Ref.update(global.ref, (n) => n + 1)
|
|
182
|
+
return counter.a
|
|
183
|
+
}, Effect.provide(PerRequestCounter.Default))
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const GlobalCounterLive = Layer.effect(
|
|
187
|
+
GlobalCounter,
|
|
188
|
+
Ref.make(0).pipe(Effect.map((ref) => ({ ref })))
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const CounterTestLayer = counterImpl.pipe(Layer.provideMerge(GlobalCounterLive))
|
|
192
|
+
|
|
193
|
+
it.live(
|
|
194
|
+
"per-request service isolation with shared global counter",
|
|
195
|
+
Effect.fnUntraced(
|
|
196
|
+
function*() {
|
|
197
|
+
const client = yield* RpcTest.makeClient(CounterRpcs)
|
|
198
|
+
const global = yield* GlobalCounter
|
|
199
|
+
|
|
200
|
+
const r1 = yield* client.incrementA()
|
|
201
|
+
const r2 = yield* client.incrementB()
|
|
202
|
+
|
|
203
|
+
// per-request counter is fresh each time → both return 1
|
|
204
|
+
expect(r1).toBe(1)
|
|
205
|
+
expect(r2).toBe(1)
|
|
206
|
+
|
|
207
|
+
// global counter is shared across requests → accumulates to 2
|
|
208
|
+
const globalCount = yield* Ref.get(global.ref)
|
|
209
|
+
expect(globalCount).toBe(2)
|
|
210
|
+
},
|
|
211
|
+
Effect.provide(CounterTestLayer)
|
|
212
|
+
)
|
|
213
|
+
)
|