@effect-app/infra 4.0.0-beta.281 → 4.0.0-beta.282
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 +19 -0
- package/dist/ClusterCosmos.d.ts +15 -15
- package/dist/MainFiberSet.d.ts +1 -0
- package/dist/MainFiberSet.d.ts.map +1 -1
- package/dist/MainFiberSet.js +14 -2
- package/dist/logger.d.ts +4 -4
- package/dist/routing/utils.js +2 -2
- package/package.json +5 -5
- package/src/MainFiberSet.ts +18 -1
- package/src/routing/utils.ts +1 -1
- package/src/QueueMaker/SQLQueue.ts +0 -186
- package/src/QueueMaker/memQueue.ts +0 -136
- package/src/QueueMaker/sbqueue.ts +0 -125
- package/src/RequestFiberSet.ts +0 -110
- package/src/ServiceBus.ts +0 -219
- package/src/memQueue.ts +0 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @effect-app/infra
|
|
2
2
|
|
|
3
|
+
## 4.0.0-beta.282
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b2df3fa: Align `Schema.Void` with TypeScript `void` return-value semantics (effect-smol PR #2475 / `b7d46ab`).
|
|
8
|
+
|
|
9
|
+
`S.Void` now accepts **any present value** at runtime and discards it to `undefined`, while keeping the decoded/encoded type as `void` — matching a `void` return whose result callers never observe. This is implemented as an override of the AST node in a new `effect-app/SchemaAST` module (`export * from "effect/SchemaAST"` plus a `Void` subclass whose parser mirrors the PR's `fromAnyToConst(undefined)`), so there is a single canonical `Void` used everywhere, including RPC success schemas.
|
|
10
|
+
|
|
11
|
+
- New `effect-app/SchemaAST` module; internal `SchemaAST` imports across the libs now route through it.
|
|
12
|
+
- Removed `ForceVoid` from `effect-app/client/makeClient` — use `S.Void` directly, which now carries this behaviour.
|
|
13
|
+
- `S.Void_` remains available as effect's original (`undefined`-only) Void.
|
|
14
|
+
|
|
15
|
+
- f150cf9: Remove obsolete queue machinery left unused after the move to cluster entities: the SB/SQLite/mem `QueueMaker` implementations (`QueueMaker/{SQLQueue,memQueue,sbqueue}.ts`), their `ServiceBus.ts`/`memQueue.ts` transports, and `RequestFiberSet.ts` (its `setRootParentSpan` helper is inlined into `MainFiberSet`). `effect-app/QueueMaker` keeps only `QueueMeta`; the `QueueBase` interface and empty `QueueMaker` ops object are dropped.
|
|
16
|
+
- e8842aa: Update effect packages to `4.0.0-beta.88` (from `beta.86`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. Also bump `@effect-app/cli` to `2.1.0-beta.35`. No source changes required — typecheck and tests pass unchanged.
|
|
17
|
+
- Updated dependencies [b2df3fa]
|
|
18
|
+
- Updated dependencies [f150cf9]
|
|
19
|
+
- Updated dependencies [e8842aa]
|
|
20
|
+
- effect-app@4.0.0-beta.282
|
|
21
|
+
|
|
3
22
|
## 4.0.0-beta.281
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/dist/ClusterCosmos.d.ts
CHANGED
|
@@ -18,25 +18,25 @@ export interface ClusterCosmosConfig {
|
|
|
18
18
|
export declare const makeMessageStorage: (options?: {
|
|
19
19
|
readonly prefix?: string | undefined;
|
|
20
20
|
} | undefined) => Effect.Effect<{
|
|
21
|
-
readonly saveRequest: <R extends import("effect/unstable/rpc/Rpc").Any>(envelope: import("effect/unstable/cluster/Message").OutgoingRequest<R>) => Effect.Effect<MessageStorage.SaveResult<R>, import("effect/unstable/cluster/ClusterError").MalformedMessage
|
|
22
|
-
readonly saveEnvelope: (envelope: import("effect/unstable/cluster/Message").OutgoingEnvelope) => Effect.Effect<void, import("effect/unstable/cluster/ClusterError").MalformedMessage
|
|
23
|
-
readonly saveReply: <R extends import("effect/unstable/rpc/Rpc").Any>(reply: Reply.ReplyWithContext<R>) => Effect.Effect<void, import("effect/unstable/cluster/ClusterError").MalformedMessage
|
|
24
|
-
readonly clearReplies: (requestId: Snowflake.Snowflake) => Effect.Effect<void, PersistenceError
|
|
25
|
-
readonly repliesFor: <R extends import("effect/unstable/rpc/Rpc").Any>(requests: Iterable<import("effect/unstable/cluster/Message").OutgoingRequest<R>>) => Effect.Effect<Reply.Reply<R
|
|
26
|
-
readonly repliesForUnfiltered: (requestIds: Iterable<Snowflake.Snowflake>) => Effect.Effect<Reply.Encoded
|
|
21
|
+
readonly saveRequest: <R extends import("effect/unstable/rpc/Rpc").Any>(envelope: import("effect/unstable/cluster/Message").OutgoingRequest<R>) => Effect.Effect<MessageStorage.SaveResult<R>, PersistenceError | import("effect/unstable/cluster/ClusterError").MalformedMessage>;
|
|
22
|
+
readonly saveEnvelope: (envelope: import("effect/unstable/cluster/Message").OutgoingEnvelope) => Effect.Effect<void, PersistenceError | import("effect/unstable/cluster/ClusterError").MalformedMessage>;
|
|
23
|
+
readonly saveReply: <R extends import("effect/unstable/rpc/Rpc").Any>(reply: Reply.ReplyWithContext<R>) => Effect.Effect<void, PersistenceError | import("effect/unstable/cluster/ClusterError").MalformedMessage>;
|
|
24
|
+
readonly clearReplies: (requestId: Snowflake.Snowflake) => Effect.Effect<void, PersistenceError>;
|
|
25
|
+
readonly repliesFor: <R extends import("effect/unstable/rpc/Rpc").Any>(requests: Iterable<import("effect/unstable/cluster/Message").OutgoingRequest<R>>) => Effect.Effect<Array<Reply.Reply<R>>, PersistenceError | import("effect/unstable/cluster/ClusterError").MalformedMessage>;
|
|
26
|
+
readonly repliesForUnfiltered: (requestIds: Iterable<Snowflake.Snowflake>) => Effect.Effect<Array<Reply.Encoded>, PersistenceError | import("effect/unstable/cluster/ClusterError").MalformedMessage>;
|
|
27
27
|
readonly requestIdForPrimaryKey: (options: {
|
|
28
28
|
readonly address: import("effect/unstable/cluster/EntityAddress").EntityAddress;
|
|
29
29
|
readonly tag: string;
|
|
30
30
|
readonly id: string;
|
|
31
|
-
}) => Effect.Effect<Option.Option<Snowflake.Snowflake>, PersistenceError
|
|
32
|
-
readonly registerReplyHandler: <R extends import("effect/unstable/rpc/Rpc").Any>(message: import("effect/unstable/cluster/Message").OutgoingRequest<R> | import("effect/unstable/cluster/Message").IncomingRequest<R>) => Effect.Effect<void, import("effect/unstable/cluster/ClusterError").EntityNotAssignedToRunner
|
|
33
|
-
readonly unregisterReplyHandler: (requestId: Snowflake.Snowflake) => Effect.Effect<void
|
|
34
|
-
readonly unregisterShardReplyHandlers: (shardId: ShardId.ShardId) => Effect.Effect<void
|
|
35
|
-
readonly unprocessedMessages: (shardIds: Iterable<ShardId.ShardId>) => Effect.Effect<import("effect/unstable/cluster/Message").Incoming<any
|
|
36
|
-
readonly unprocessedMessagesById: <R extends import("effect/unstable/rpc/Rpc").Any>(messageIds: Iterable<Snowflake.Snowflake>) => Effect.Effect<import("effect/unstable/cluster/Message").Incoming<R
|
|
37
|
-
readonly resetShards: (shardIds: Iterable<ShardId.ShardId>) => Effect.Effect<void, PersistenceError
|
|
38
|
-
readonly resetAddress: (address: import("effect/unstable/cluster/EntityAddress").EntityAddress) => Effect.Effect<void, PersistenceError
|
|
39
|
-
readonly clearAddress: (address: import("effect/unstable/cluster/EntityAddress").EntityAddress) => Effect.Effect<void, PersistenceError
|
|
31
|
+
}) => Effect.Effect<Option.Option<Snowflake.Snowflake>, PersistenceError>;
|
|
32
|
+
readonly registerReplyHandler: <R extends import("effect/unstable/rpc/Rpc").Any>(message: import("effect/unstable/cluster/Message").OutgoingRequest<R> | import("effect/unstable/cluster/Message").IncomingRequest<R>) => Effect.Effect<void, import("effect/unstable/cluster/ClusterError").EntityNotAssignedToRunner>;
|
|
33
|
+
readonly unregisterReplyHandler: (requestId: Snowflake.Snowflake) => Effect.Effect<void>;
|
|
34
|
+
readonly unregisterShardReplyHandlers: (shardId: ShardId.ShardId) => Effect.Effect<void>;
|
|
35
|
+
readonly unprocessedMessages: (shardIds: Iterable<ShardId.ShardId>) => Effect.Effect<Array<import("effect/unstable/cluster/Message").Incoming<any>>, PersistenceError>;
|
|
36
|
+
readonly unprocessedMessagesById: <R extends import("effect/unstable/rpc/Rpc").Any>(messageIds: Iterable<Snowflake.Snowflake>) => Effect.Effect<Array<import("effect/unstable/cluster/Message").Incoming<R>>, PersistenceError>;
|
|
37
|
+
readonly resetShards: (shardIds: Iterable<ShardId.ShardId>) => Effect.Effect<void, PersistenceError>;
|
|
38
|
+
readonly resetAddress: (address: import("effect/unstable/cluster/EntityAddress").EntityAddress) => Effect.Effect<void, PersistenceError>;
|
|
39
|
+
readonly clearAddress: (address: import("effect/unstable/cluster/EntityAddress").EntityAddress) => Effect.Effect<void, PersistenceError>;
|
|
40
40
|
readonly withTransaction: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
41
41
|
}, never, CosmosClient | Snowflake.Generator>;
|
|
42
42
|
export declare const makeRunnerStorage: (options?: {
|
package/dist/MainFiberSet.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as Context from "effect-app/Context";
|
|
|
2
2
|
import * as Effect from "effect-app/Effect";
|
|
3
3
|
import * as Layer from "effect-app/Layer";
|
|
4
4
|
import * as Fiber from "effect/Fiber";
|
|
5
|
+
export declare const setRootParentSpan: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
5
6
|
declare const MainFiberSet_base: Context.ServiceClass<MainFiberSet, "MainFiberSet", {
|
|
6
7
|
interrupt: Effect.Effect<void, never, never>;
|
|
7
8
|
join: Effect.Effect<void, never, never>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MainFiberSet.d.ts","sourceRoot":"","sources":["../src/MainFiberSet.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,oBAAoB,CAAA;AAC7C,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAC3C,OAAO,KAAK,KAAK,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"MainFiberSet.d.ts","sourceRoot":"","sources":["../src/MainFiberSet.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,oBAAoB,CAAA;AAC7C,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAC3C,OAAO,KAAK,KAAK,MAAM,kBAAkB,CAAA;AAEzC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAoBrC,eAAO,MAAM,iBAAiB,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,2BAC4B,CAAA;;;;uBAmCvE,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;;qBA/BvC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;qBACpB,SAAS,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;;;;;;;;;;;AAiDvD;;;;GAIG;AACH,qBAAa,YAAa,SAAQ,iBAAyD;IACzF,MAAM,CAAC,QAAQ,CAAC,IAAI,0CAAgC;IACpD,MAAM,CAAC,QAAQ,CAAC,QAAQ,mCAIvB;IACD,MAAM,CAAC,QAAQ,CAAC,GAAG,GAAI,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,mEAAkD;IAC/G,MAAM,CAAC,QAAQ,CAAC,gBAAgB,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,sEACb;CAC7D"}
|
package/dist/MainFiberSet.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import * as Context from "effect-app/Context";
|
|
2
2
|
import * as Effect from "effect-app/Effect";
|
|
3
3
|
import * as Layer from "effect-app/Layer";
|
|
4
|
+
import * as Option from "effect-app/Option";
|
|
4
5
|
import * as Fiber from "effect/Fiber";
|
|
5
6
|
import * as FiberSet from "effect/FiberSet";
|
|
6
7
|
import { InfraLogger } from "./logger.js";
|
|
7
8
|
import { reportNonInterruptedFailureCause } from "./QueueMaker/errors.js";
|
|
8
|
-
|
|
9
|
+
// Walk to the root span so a daemon fiber's spans aren't parented to a span that
|
|
10
|
+
// may already be closing (e.g. the originating request).
|
|
11
|
+
const getRootParentSpan = Effect.gen(function* () {
|
|
12
|
+
let span = yield* Effect.currentSpan.pipe(Effect.catchTag("NoSuchElementError", () => Effect.succeed(null)));
|
|
13
|
+
if (!span)
|
|
14
|
+
return span;
|
|
15
|
+
while (span._tag === "Span" && Option.isSome(span.parent)) {
|
|
16
|
+
span = span.parent.value;
|
|
17
|
+
}
|
|
18
|
+
return span;
|
|
19
|
+
});
|
|
20
|
+
export const setRootParentSpan = (self) => getRootParentSpan.pipe(Effect.andThen((span) => span ? Effect.withParentSpan(self, span) : self));
|
|
9
21
|
const make = Effect.gen(function* () {
|
|
10
22
|
const set = yield* FiberSet.make();
|
|
11
23
|
const add = (...fibers) => Effect.sync(() => fibers.forEach((_) => FiberSet.addUnsafe(set, _)));
|
|
@@ -55,4 +67,4 @@ export class MainFiberSet extends Context.Service()("MainFiberSet", { make }) {
|
|
|
55
67
|
static run = (self) => this.pipe(Effect.andThen((_) => _.run(self)));
|
|
56
68
|
static forkDaemonReport = (self) => this.pipe(Effect.andThen((_) => _.forkDaemonReport(self)));
|
|
57
69
|
}
|
|
58
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
70
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTWFpbkZpYmVyU2V0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL01haW5GaWJlclNldC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLG9CQUFvQixDQUFBO0FBQzdDLE9BQU8sS0FBSyxNQUFNLE1BQU0sbUJBQW1CLENBQUE7QUFDM0MsT0FBTyxLQUFLLEtBQUssTUFBTSxrQkFBa0IsQ0FBQTtBQUN6QyxPQUFPLEtBQUssTUFBTSxNQUFNLG1CQUFtQixDQUFBO0FBQzNDLE9BQU8sS0FBSyxLQUFLLE1BQU0sY0FBYyxDQUFBO0FBQ3JDLE9BQU8sS0FBSyxRQUFRLE1BQU0saUJBQWlCLENBQUE7QUFHM0MsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLGFBQWEsQ0FBQTtBQUN6QyxPQUFPLEVBQUUsZ0NBQWdDLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQTtBQUV6RSxpRkFBaUY7QUFDakYseURBQXlEO0FBQ3pELE1BQU0saUJBQWlCLEdBQUcsTUFBTSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUM7SUFDNUMsSUFBSSxJQUFJLEdBQTBCLEtBQUssQ0FBQyxDQUFDLE1BQU0sQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUM5RCxNQUFNLENBQUMsUUFBUSxDQUFDLG9CQUFvQixFQUFFLEdBQUcsRUFBRSxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FDbEUsQ0FBQTtJQUNELElBQUksQ0FBQyxJQUFJO1FBQUUsT0FBTyxJQUFJLENBQUE7SUFDdEIsT0FBTyxJQUFJLENBQUMsSUFBSSxLQUFLLE1BQU0sSUFBSSxNQUFNLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQzFELElBQUksR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQTtJQUMxQixDQUFDO0lBQ0QsT0FBTyxJQUFJLENBQUE7QUFDYixDQUFDLENBQUMsQ0FBQTtBQUVGLE1B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
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export declare const InfraLogger: {
|
|
2
|
-
logWarning: (...message:
|
|
3
|
-
logError: (...message:
|
|
4
|
-
logFatal: (...message:
|
|
2
|
+
logWarning: (...message: readonly any[]) => import("effect/Effect").Effect<void, never, never>;
|
|
3
|
+
logError: (...message: readonly any[]) => import("effect/Effect").Effect<void, never, never>;
|
|
4
|
+
logFatal: (...message: readonly any[]) => import("effect/Effect").Effect<void, never, never>;
|
|
5
5
|
logInfo: (...message: ReadonlyArray<any>) => import("effect/Effect").Effect<void, never, never>;
|
|
6
6
|
logDebug: (...message: ReadonlyArray<any>) => import("effect/Effect").Effect<void, never, never>;
|
|
7
|
-
logWithLevel: (level: import("effect/LogLevel").Severity, ...message:
|
|
7
|
+
logWithLevel: (level: import("effect/LogLevel").Severity, ...message: readonly any[]) => import("effect/Effect").Effect<void, never, never>;
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=logger.d.ts.map
|
package/dist/routing/utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as S from "effect-app/Schema";
|
|
2
|
-
import * as SchemaAST from "effect/SchemaAST";
|
|
2
|
+
import * as SchemaAST from "effect-app/SchemaAST";
|
|
3
3
|
const get = ["Get", "Index", "List", "All", "Find", "Search"];
|
|
4
4
|
const del = ["Delete", "Remove", "Destroy"];
|
|
5
5
|
const patch = ["Patch", "Update", "Edit"];
|
|
@@ -42,4 +42,4 @@ export const determineMethod = (fullName, schema) => {
|
|
|
42
42
|
return { _tag: "command", method: "POST" };
|
|
43
43
|
};
|
|
44
44
|
export const isCommand = (method) => method._tag === "command";
|
|
45
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
45
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcm91dGluZy91dGlscy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssQ0FBQyxNQUFNLG1CQUFtQixDQUFBO0FBRXRDLE9BQU8sS0FBSyxTQUFTLE1BQU0sc0JBQXNCLENBQUE7QUFFakQsTUFBTSxHQUFHLEdBQUcsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLFFBQVEsQ0FBQyxDQUFBO0FBQzdELE1BQU0sR0FBRyxHQUFHLENBQUMsUUFBUSxFQUFFLFFBQVEsRUFBRSxTQUFTLENBQUMsQ0FBQTtBQUMzQyxNQUFNLEtBQUssR0FBRyxDQUFDLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxDQUFDLENBQUE7QUFFekMsTUFBTSxxQkFBcUIsR0FBRyxDQUFDLEdBQVksRUFBVyxFQUFFO0lBQ3RELHFHQUFxRztJQUNyRywrRUFBK0U7SUFDL0Usc0VBQXNFO0lBQ3RFLE1BQU0sT0FBTyxHQUFHLFNBQVMsQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLENBQUE7SUFDeEMsSUFBSSxPQUFPLENBQUMsSUFBSSxLQUFLLFFBQVE7UUFBRSxPQUFPLElBQUksQ0FBQTtJQUMxQyxJQUFJLE9BQU8sQ0FBQyxJQUFJLEtBQUssT0FBTyxJQUFJLE9BQU8sQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLHFCQUFxQixDQUFDLEVBQUUsQ0FBQztRQUMzRSxPQUFPLElBQUksQ0FBQTtJQUNiLENBQUM7SUFFRCxPQUFPLEtBQUssQ0FBQTtBQUNkLENBQUMsQ0FBQTtBQUVELE1BQU0sY0FBYyxHQUFHLENBQUMsR0FBWSxFQUFXLEVBQUU7SUFDL0MsSUFBSSxHQUFHLENBQUMsSUFBSSxLQUFLLE9BQU87UUFBRSxPQUFPLEdBQUcsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLGNBQWMsQ0FBQyxDQUFBO0lBQ2hFLGlDQUFpQztJQUNqQyxJQUFJLEdBQUcsQ0FBQyxJQUFJLEtBQUssU0FBUztRQUFFLE9BQU8sS0FBSyxDQUFBO0lBQ3hDLE9BQU8sR0FBRyxDQUFDLGtCQUFrQixDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMscUJBQXFCLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUE7QUFDM0UsQ0FBQyxDQUFBO0FBRUQsTUFBTSxXQUFXLEdBQUcsQ0FBQyxNQUE0QyxFQUFXLEVBQUU7SUFDNUUsSUFBSSxRQUFRLElBQUksTUFBTSxJQUFJLE1BQU0sQ0FBQyxNQUFNO1FBQUUsT0FBTyxjQUFjLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUEsQ0FBQyxtQkFBbUI7SUFDL0csT0FBTyxjQUFjLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFBO0FBQ25DLENBQUMsQ0FBQTtBQUVELE1BQU0sQ0FBQyxNQUFNLGVBQWUsR0FBRyxDQUFDLFFBQWdCLEVBQUUsTUFBYSxFQUFFLEVBQUU7SUFDakUsTUFBTSxJQUFJLEdBQUcsUUFBUSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtJQUNoQyxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUUsQ0FBQTtJQUV6QyxJQUFJLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLFVBQVUsQ0FBQyxVQUFVLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO1FBQzlDLE9BQU8sRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxXQUFXLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQWMsQ0FBQyxDQUFDLENBQUMsTUFBTSxFQUFXLENBQUE7SUFDMUYsQ0FBQztJQUNELElBQUksR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsVUFBVSxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7UUFDOUMsT0FBTyxFQUFFLElBQUksRUFBRSxTQUFTLEVBQUUsTUFBTSxFQUFFLFdBQVcsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxNQUFNLEVBQVcsQ0FBQTtJQUN0RixDQUFDO0lBQ0QsSUFBSSxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxVQUFVLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxTQUFTLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFBVyxDQUFBO0lBQ3JHLE9BQU8sRUFBRSxJQUFJLEVBQUUsU0FBUyxFQUFFLE1BQU0sRUFBRSxNQUFNLEVBQVcsQ0FBQTtBQUNyRCxDQUFDLENBQUE7QUFFRCxNQUFNLENBQUMsTUFBTSxTQUFTLEdBQUcsQ0FBQyxNQUEwQyxFQUFFLEVBQUUsQ0FBQyxNQUFNLENBQUMsSUFBSSxLQUFLLFNBQVMsQ0FBQSJ9
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-app/infra",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.282",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"proper-lockfile": "^4.1.2",
|
|
11
11
|
"pure-rand": "8.4.0",
|
|
12
12
|
"query-string": "^9.4.0",
|
|
13
|
-
"effect-app": "4.0.0-beta.
|
|
13
|
+
"effect-app": "4.0.0-beta.282"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@azure/cosmos": "^4.9.3",
|
|
17
17
|
"@azure/service-bus": "^7.9.5",
|
|
18
|
-
"@effect/sql-sqlite-node": "4.0.0-beta.
|
|
18
|
+
"@effect/sql-sqlite-node": "4.0.0-beta.88",
|
|
19
19
|
"@sentry/node": "10.55.0",
|
|
20
20
|
"@sentry/opentelemetry": "10.55.0",
|
|
21
21
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@azure/cosmos": "^4.9.3",
|
|
36
36
|
"@azure/service-bus": "^7.9.5",
|
|
37
|
-
"@effect/vitest": "^4.0.0-beta.
|
|
37
|
+
"@effect/vitest": "^4.0.0-beta.88",
|
|
38
38
|
"@sendgrid/helpers": "^8.0.0",
|
|
39
39
|
"@sendgrid/mail": "^8.1.6",
|
|
40
40
|
"@sentry/node": "10.55.0",
|
|
41
41
|
"@sentry/opentelemetry": "10.55.0",
|
|
42
|
-
"effect": "^4.0.0-beta.
|
|
42
|
+
"effect": "^4.0.0-beta.88",
|
|
43
43
|
"jwt-decode": "^4.0.0",
|
|
44
44
|
"redis": "^3.1.2",
|
|
45
45
|
"redlock": "^4.2.0"
|
package/src/MainFiberSet.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import * as Context from "effect-app/Context"
|
|
2
2
|
import * as Effect from "effect-app/Effect"
|
|
3
3
|
import * as Layer from "effect-app/Layer"
|
|
4
|
+
import * as Option from "effect-app/Option"
|
|
4
5
|
import * as Fiber from "effect/Fiber"
|
|
5
6
|
import * as FiberSet from "effect/FiberSet"
|
|
7
|
+
import type * as Tracer from "effect/Tracer"
|
|
6
8
|
|
|
7
9
|
import { InfraLogger } from "./logger.ts"
|
|
8
10
|
import { reportNonInterruptedFailureCause } from "./QueueMaker/errors.ts"
|
|
9
|
-
|
|
11
|
+
|
|
12
|
+
// Walk to the root span so a daemon fiber's spans aren't parented to a span that
|
|
13
|
+
// may already be closing (e.g. the originating request).
|
|
14
|
+
const getRootParentSpan = Effect.gen(function*() {
|
|
15
|
+
let span: Tracer.AnySpan | null = yield* Effect.currentSpan.pipe(
|
|
16
|
+
Effect.catchTag("NoSuchElementError", () => Effect.succeed(null))
|
|
17
|
+
)
|
|
18
|
+
if (!span) return span
|
|
19
|
+
while (span._tag === "Span" && Option.isSome(span.parent)) {
|
|
20
|
+
span = span.parent.value
|
|
21
|
+
}
|
|
22
|
+
return span
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export const setRootParentSpan = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
26
|
+
getRootParentSpan.pipe(Effect.andThen((span) => span ? Effect.withParentSpan(self, span) : self))
|
|
10
27
|
|
|
11
28
|
const make = Effect.gen(function*() {
|
|
12
29
|
const set = yield* FiberSet.make<unknown, never>()
|
package/src/routing/utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as S from "effect-app/Schema"
|
|
2
2
|
import type { AST } from "effect-app/Schema"
|
|
3
|
-
import * as SchemaAST from "effect/SchemaAST"
|
|
3
|
+
import * as SchemaAST from "effect-app/SchemaAST"
|
|
4
4
|
|
|
5
5
|
const get = ["Get", "Index", "List", "All", "Find", "Search"]
|
|
6
6
|
const del = ["Delete", "Remove", "Destroy"]
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { reportNonInterruptedFailure } from "@effect-app/infra/QueueMaker/errors"
|
|
2
|
-
import { subMinutes } from "date-fns"
|
|
3
|
-
import type { NonEmptyReadonlyArray } from "effect-app/Array"
|
|
4
|
-
import * as Effect from "effect-app/Effect"
|
|
5
|
-
import * as Option from "effect-app/Option"
|
|
6
|
-
import { type QueueBase, QueueMeta } from "effect-app/QueueMaker"
|
|
7
|
-
import * as S from "effect-app/Schema"
|
|
8
|
-
import { type NonEmptyString255 } from "effect-app/Schema"
|
|
9
|
-
import { getRequestContext, setupRequestContextWithCustomSpan } from "effect-app/setupRequest"
|
|
10
|
-
import { pretty } from "effect-app/utils"
|
|
11
|
-
import * as Fiber from "effect/Fiber"
|
|
12
|
-
import * as Tracer from "effect/Tracer"
|
|
13
|
-
import { SqlClient } from "effect/unstable/sql"
|
|
14
|
-
import { InfraLogger } from "../logger.ts"
|
|
15
|
-
import { messagingSpanArgs } from "../otel.ts"
|
|
16
|
-
import { SQLModel } from "../SQL.ts"
|
|
17
|
-
|
|
18
|
-
export const QueueId = S.Finite.pipe(S.brand("QueueId"))
|
|
19
|
-
export type QueueId = typeof QueueId.Type
|
|
20
|
-
|
|
21
|
-
// TODO: let the model track and Auto Generate versionColumn on every update instead
|
|
22
|
-
export const makeSQLQueue = Effect.fnUntraced(function*<
|
|
23
|
-
Evt extends { id: S.StringId; _tag: string },
|
|
24
|
-
DrainEvt extends { id: S.StringId; _tag: string },
|
|
25
|
-
EvtE,
|
|
26
|
-
DrainEvtE
|
|
27
|
-
>(
|
|
28
|
-
queueName: NonEmptyString255,
|
|
29
|
-
queueDrainName: NonEmptyString255,
|
|
30
|
-
schema: S.Codec<Evt, EvtE>,
|
|
31
|
-
drainSchema: S.Codec<DrainEvt, DrainEvtE>
|
|
32
|
-
) {
|
|
33
|
-
const base = {
|
|
34
|
-
id: SQLModel.Generated(QueueId),
|
|
35
|
-
meta: SQLModel.JsonFromString(QueueMeta),
|
|
36
|
-
name: S.NonEmptyString255,
|
|
37
|
-
createdAt: SQLModel.DateTimeInsert,
|
|
38
|
-
updatedAt: SQLModel.DateTimeUpdate,
|
|
39
|
-
// TODO: at+owner
|
|
40
|
-
processingAt: SQLModel.FieldOption(S.Date),
|
|
41
|
-
finishedAt: SQLModel.FieldOption(S.Date),
|
|
42
|
-
etag: S.String // TODO: use a SQLModel thing that auto updates it?
|
|
43
|
-
// TODO: record locking.. / optimistic locking
|
|
44
|
-
// rowVersion: SQLModel.DateTimeFromNumberWithNow
|
|
45
|
-
}
|
|
46
|
-
class Queue extends SQLModel.Class<Queue>("Queue")({
|
|
47
|
-
body: SQLModel.JsonFromString(schema),
|
|
48
|
-
...base
|
|
49
|
-
}) {}
|
|
50
|
-
class Drain extends SQLModel.Class<Drain>("Drain")({
|
|
51
|
-
body: SQLModel.JsonFromString(drainSchema),
|
|
52
|
-
...base
|
|
53
|
-
}) {}
|
|
54
|
-
const sql = yield* SqlClient.SqlClient
|
|
55
|
-
|
|
56
|
-
const queueRepo = yield* SQLModel.makeRepository(Queue, {
|
|
57
|
-
tableName: "queue",
|
|
58
|
-
spanPrefix: "QueueRepo",
|
|
59
|
-
idColumn: "id",
|
|
60
|
-
versionColumn: "etag"
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
const drainRepo = yield* SQLModel.makeRepository(Drain, {
|
|
64
|
-
tableName: "queue",
|
|
65
|
-
spanPrefix: "DrainRepo",
|
|
66
|
-
idColumn: "id",
|
|
67
|
-
versionColumn: "etag"
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const decodeDrain = S.decodeEffectConcurrently(Drain)
|
|
71
|
-
|
|
72
|
-
const drain = Effect.gen(function*() {
|
|
73
|
-
const limit = subMinutes(new Date(), 15)
|
|
74
|
-
return yield* sql<typeof Drain.Encoded>`SELECT *
|
|
75
|
-
FROM queue
|
|
76
|
-
WHERE name = ${queueDrainName} AND finishedAt IS NULL AND (processingAt IS NULL OR processingAt < ${limit.getTime()})
|
|
77
|
-
LIMIT 1`
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
const q = {
|
|
81
|
-
offer: Effect.fnUntraced(function*(body: Evt, meta: typeof QueueMeta.Type) {
|
|
82
|
-
yield* queueRepo.insertVoid(Queue.insert.make({
|
|
83
|
-
body,
|
|
84
|
-
meta,
|
|
85
|
-
name: queueName,
|
|
86
|
-
processingAt: Option.none(),
|
|
87
|
-
finishedAt: Option.none(),
|
|
88
|
-
etag: crypto.randomUUID()
|
|
89
|
-
}))
|
|
90
|
-
}),
|
|
91
|
-
take: Effect.gen(function*() {
|
|
92
|
-
while (true) {
|
|
93
|
-
const [first] = yield* drain.pipe(Effect.withTracerEnabled(false)) // disable sql tracer otherwise we spam it..
|
|
94
|
-
if (first) {
|
|
95
|
-
const dec = yield* decodeDrain(first)
|
|
96
|
-
const { createdAt, updatedAt, ...rest } = dec
|
|
97
|
-
return yield* drainRepo.update(
|
|
98
|
-
Drain.update.make({ ...rest, processingAt: Option.some(new Date()) }) // auto in lib , etag: crypto.randomUUID()
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
if (first) return first
|
|
102
|
-
yield* Effect.sleep(250)
|
|
103
|
-
}
|
|
104
|
-
}),
|
|
105
|
-
finish: Effect.fn(function*({ createdAt, updatedAt, ...q }: Drain) {
|
|
106
|
-
return yield* drainRepo.updateVoid(Drain.update.make({ ...q, finishedAt: Option.some(new Date()) })) // auto in lib , etag: crypto.randomUUID()
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
const queue = {
|
|
110
|
-
publish: Effect.fn(`publish ${queueName}`, {
|
|
111
|
-
kind: "producer",
|
|
112
|
-
attributes: {
|
|
113
|
-
"messaging.system": "sql",
|
|
114
|
-
"messaging.operation.name": "publish",
|
|
115
|
-
"messaging.destination.name": queueName
|
|
116
|
-
}
|
|
117
|
-
})(function*(
|
|
118
|
-
...messages: NonEmptyReadonlyArray<Evt>
|
|
119
|
-
) {
|
|
120
|
-
yield* Effect.annotateCurrentSpan({
|
|
121
|
-
"messaging.batch.message_count": messages.length,
|
|
122
|
-
"messaging.message.types": messages.map((_) => _._tag)
|
|
123
|
-
})
|
|
124
|
-
const requestContext = yield* getRequestContext
|
|
125
|
-
yield* Effect.forEach(messages, (m) => q.offer(m, requestContext), { discard: true })
|
|
126
|
-
}),
|
|
127
|
-
drain: <DrainE, DrainR>(
|
|
128
|
-
handleEvent: (ks: DrainEvt) => Effect.Effect<void, DrainE, DrainR>,
|
|
129
|
-
sessionId?: string
|
|
130
|
-
) => {
|
|
131
|
-
const silenceAndReportError = reportNonInterruptedFailure({ name: "MemQueue.drain." + queueDrainName })
|
|
132
|
-
const processMessage = Effect.fnUntraced(function*({ body, meta }: Drain) {
|
|
133
|
-
let effect = InfraLogger
|
|
134
|
-
.logDebug(`[${queueDrainName}] Processing incoming message`)
|
|
135
|
-
.pipe(
|
|
136
|
-
Effect.annotateLogs({ body: pretty(body), meta: pretty(meta) }),
|
|
137
|
-
Effect.andThen(handleEvent(body)),
|
|
138
|
-
silenceAndReportError,
|
|
139
|
-
(_) => {
|
|
140
|
-
const args = messagingSpanArgs({
|
|
141
|
-
operation: "process",
|
|
142
|
-
system: "sql",
|
|
143
|
-
destination: queueDrainName,
|
|
144
|
-
messageId: body.id,
|
|
145
|
-
conversationId: sessionId,
|
|
146
|
-
extra: { "messaging.message.type": body._tag, "messaging.message.body": body }
|
|
147
|
-
}, "consumer")
|
|
148
|
-
return setupRequestContextWithCustomSpan(
|
|
149
|
-
_,
|
|
150
|
-
meta,
|
|
151
|
-
args.name,
|
|
152
|
-
{
|
|
153
|
-
captureStackTrace: false,
|
|
154
|
-
kind: args.kind,
|
|
155
|
-
attributes: args.attributes
|
|
156
|
-
}
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
)
|
|
160
|
-
if (meta.span) {
|
|
161
|
-
effect = Effect.withParentSpan(effect, Tracer.externalSpan(meta.span))
|
|
162
|
-
}
|
|
163
|
-
return yield* effect
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
return Effect.fn(`receive ${queueDrainName}`, {
|
|
167
|
-
kind: "consumer",
|
|
168
|
-
attributes: {
|
|
169
|
-
"messaging.system": "sql",
|
|
170
|
-
"messaging.operation.name": "receive",
|
|
171
|
-
"messaging.destination.name": queueDrainName,
|
|
172
|
-
...(sessionId !== undefined && { "messaging.message.conversation_id": sessionId })
|
|
173
|
-
}
|
|
174
|
-
})(function*() {
|
|
175
|
-
const x = yield* q.take
|
|
176
|
-
yield* processMessage(x).pipe(
|
|
177
|
-
Effect.uninterruptible,
|
|
178
|
-
Effect.forkChild,
|
|
179
|
-
Effect.flatMap(Fiber.join),
|
|
180
|
-
Effect.tap(q.finish(x))
|
|
181
|
-
)
|
|
182
|
-
}, (effect) => effect.pipe(silenceAndReportError, Effect.forever))()
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return queue as QueueBase<Evt, DrainEvt>
|
|
186
|
-
})
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import type { NonEmptyReadonlyArray } from "effect-app/Array"
|
|
2
|
-
import * as Effect from "effect-app/Effect"
|
|
3
|
-
import { QueueMeta } from "effect-app/QueueMaker"
|
|
4
|
-
import * as S from "effect-app/Schema"
|
|
5
|
-
import { getRequestContext, setupRequestContextWithCustomSpan } from "effect-app/setupRequest"
|
|
6
|
-
import { pretty } from "effect-app/utils"
|
|
7
|
-
import * as Cause from "effect/Cause"
|
|
8
|
-
import * as Fiber from "effect/Fiber"
|
|
9
|
-
import { flow } from "effect/Function"
|
|
10
|
-
import * as Q from "effect/Queue"
|
|
11
|
-
import * as Tracer from "effect/Tracer"
|
|
12
|
-
import { InfraLogger } from "../logger.ts"
|
|
13
|
-
import { MemQueue } from "../memQueue.ts"
|
|
14
|
-
import { messagingSpanArgs } from "../otel.ts"
|
|
15
|
-
import { reportNonInterruptedFailure, reportNonInterruptedFailureCause } from "./errors.ts"
|
|
16
|
-
|
|
17
|
-
export const makeMemQueue = Effect.fnUntraced(function*<
|
|
18
|
-
Evt extends { id: S.StringId; _tag: string },
|
|
19
|
-
DrainEvt extends { id: S.StringId; _tag: string },
|
|
20
|
-
EvtE,
|
|
21
|
-
DrainEvtE
|
|
22
|
-
>(
|
|
23
|
-
queueName: string,
|
|
24
|
-
queueDrainName: string,
|
|
25
|
-
schema: S.Codec<Evt, EvtE>,
|
|
26
|
-
drainSchema: S.Codec<DrainEvt, DrainEvtE>
|
|
27
|
-
) {
|
|
28
|
-
const mem = yield* MemQueue
|
|
29
|
-
const q = yield* mem.getOrCreateQueue(queueName)
|
|
30
|
-
const qDrain = yield* mem.getOrCreateQueue(queueDrainName)
|
|
31
|
-
|
|
32
|
-
const wireSchema = S.Struct({ body: schema, meta: QueueMeta })
|
|
33
|
-
const wireSchemaJson = S.fromJsonString(S.toCodecJson(wireSchema))
|
|
34
|
-
const encodePublish = S.encodeEffect(wireSchemaJson)
|
|
35
|
-
const drainW = S.Struct({ body: drainSchema, meta: QueueMeta })
|
|
36
|
-
const drainWJson = S.fromJsonString(S.toCodecJson(drainW))
|
|
37
|
-
|
|
38
|
-
const parseDrain = flow(S.decodeUnknownEffectConcurrently(drainWJson), Effect.orDie)
|
|
39
|
-
|
|
40
|
-
const queue = {
|
|
41
|
-
publish: Effect.fn(`publish ${queueName}`, {
|
|
42
|
-
kind: "producer",
|
|
43
|
-
attributes: {
|
|
44
|
-
"messaging.system": "memory",
|
|
45
|
-
"messaging.operation.name": "publish",
|
|
46
|
-
"messaging.destination.name": queueName
|
|
47
|
-
}
|
|
48
|
-
})(function*(
|
|
49
|
-
...messages: NonEmptyReadonlyArray<Evt>
|
|
50
|
-
) {
|
|
51
|
-
yield* Effect.annotateCurrentSpan({
|
|
52
|
-
"messaging.batch.message_count": messages.length,
|
|
53
|
-
"messaging.message.types": messages.map((_) => _._tag)
|
|
54
|
-
})
|
|
55
|
-
const requestContext = yield* getRequestContext
|
|
56
|
-
// we JSON encode, because that is what the wire also does, and it reveals holes in e.g unknown encoders (Date->String)
|
|
57
|
-
yield* Effect.forEach(
|
|
58
|
-
messages,
|
|
59
|
-
(m) =>
|
|
60
|
-
encodePublish({ body: m, meta: requestContext }).pipe(
|
|
61
|
-
Effect.orDie,
|
|
62
|
-
Effect.flatMap((_) => Q.offer(q, _))
|
|
63
|
-
),
|
|
64
|
-
{ discard: true }
|
|
65
|
-
)
|
|
66
|
-
}),
|
|
67
|
-
drain: <DrainE, DrainR>(
|
|
68
|
-
handleEvent: (ks: DrainEvt) => Effect.Effect<void, DrainE, DrainR>,
|
|
69
|
-
sessionId?: string
|
|
70
|
-
) => {
|
|
71
|
-
const silenceAndReportError = reportNonInterruptedFailure({ name: "MemQueue.drain." + queueDrainName })
|
|
72
|
-
const reportError = reportNonInterruptedFailureCause({ name: "MemQueue.drain." + queueDrainName })
|
|
73
|
-
const processMessage = Effect.fnUntraced(function*(msg: string) {
|
|
74
|
-
// we JSON parse, because that is what the wire also does, and it reveals holes in e.g unknown encoders (Date->String)
|
|
75
|
-
const { body, meta } = yield* parseDrain(msg).pipe(Effect.orDie)
|
|
76
|
-
let effect = InfraLogger
|
|
77
|
-
.logDebug(`[${queueDrainName}] Processing incoming message`)
|
|
78
|
-
.pipe(
|
|
79
|
-
Effect.annotateLogs({ body: pretty(body), meta: pretty(meta) }),
|
|
80
|
-
Effect.andThen(handleEvent(body)),
|
|
81
|
-
silenceAndReportError,
|
|
82
|
-
(_) => {
|
|
83
|
-
const args = messagingSpanArgs({
|
|
84
|
-
operation: "process",
|
|
85
|
-
system: "memory",
|
|
86
|
-
destination: queueDrainName,
|
|
87
|
-
messageId: body.id,
|
|
88
|
-
conversationId: sessionId,
|
|
89
|
-
extra: { "messaging.message.type": body._tag, "messaging.message.body": body }
|
|
90
|
-
}, "consumer")
|
|
91
|
-
return setupRequestContextWithCustomSpan(
|
|
92
|
-
_,
|
|
93
|
-
meta,
|
|
94
|
-
args.name,
|
|
95
|
-
{
|
|
96
|
-
captureStackTrace: false,
|
|
97
|
-
kind: args.kind,
|
|
98
|
-
attributes: args.attributes
|
|
99
|
-
}
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
)
|
|
103
|
-
if (meta.span) {
|
|
104
|
-
effect = Effect.withParentSpan(effect, Tracer.externalSpan(meta.span))
|
|
105
|
-
}
|
|
106
|
-
return yield* effect
|
|
107
|
-
})
|
|
108
|
-
return Effect.fn(`receive ${queueDrainName}`, {
|
|
109
|
-
kind: "consumer",
|
|
110
|
-
attributes: {
|
|
111
|
-
"messaging.system": "memory",
|
|
112
|
-
"messaging.operation.name": "receive",
|
|
113
|
-
"messaging.destination.name": queueDrainName,
|
|
114
|
-
...(sessionId !== undefined && { "messaging.message.conversation_id": sessionId })
|
|
115
|
-
}
|
|
116
|
-
})(function*() {
|
|
117
|
-
const x = yield* Q.take(qDrain)
|
|
118
|
-
const exit = yield* processMessage(x).pipe(
|
|
119
|
-
Effect.uninterruptible,
|
|
120
|
-
Effect.forkChild,
|
|
121
|
-
Effect.flatMap(Fiber.join)
|
|
122
|
-
)
|
|
123
|
-
if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) {
|
|
124
|
-
// normally a failed item would be returned to the queue and retried up to X times.
|
|
125
|
-
yield* Q.offer(qDrain, x).pipe(
|
|
126
|
-
// TODO: retry count tracking and max retries.
|
|
127
|
-
Effect.delay("5 seconds"),
|
|
128
|
-
Effect.tapCause(reportError),
|
|
129
|
-
Effect.forkDetach
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
}, (effect) => effect.pipe(silenceAndReportError, Effect.forever))()
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return queue
|
|
136
|
-
})
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import type { NonEmptyReadonlyArray } from "effect-app/Array"
|
|
2
|
-
import * as Effect from "effect-app/Effect"
|
|
3
|
-
import { QueueMeta } from "effect-app/QueueMaker"
|
|
4
|
-
import * as S from "effect-app/Schema"
|
|
5
|
-
import type { StringId } from "effect-app/Schema"
|
|
6
|
-
import { getRequestContext, setupRequestContextWithCustomSpan } from "effect-app/setupRequest"
|
|
7
|
-
import { pretty } from "effect-app/utils"
|
|
8
|
-
import * as Cause from "effect/Cause"
|
|
9
|
-
import { flow } from "effect/Function"
|
|
10
|
-
import * as Tracer from "effect/Tracer"
|
|
11
|
-
import { InfraLogger } from "../logger.ts"
|
|
12
|
-
import { messagingSpanArgs } from "../otel.ts"
|
|
13
|
-
import { Receiver, Sender } from "../ServiceBus.ts"
|
|
14
|
-
import { reportNonInterruptedFailure, reportNonInterruptedFailureCause, reportQueueError } from "./errors.ts"
|
|
15
|
-
|
|
16
|
-
export function makeServiceBusQueue<
|
|
17
|
-
Evt extends { id: StringId; _tag: string },
|
|
18
|
-
DrainEvt extends { id: StringId; _tag: string },
|
|
19
|
-
EvtE,
|
|
20
|
-
DrainEvtE
|
|
21
|
-
>(
|
|
22
|
-
schema: S.Codec<Evt, EvtE>,
|
|
23
|
-
drainSchema: S.Codec<DrainEvt, DrainEvtE>
|
|
24
|
-
) {
|
|
25
|
-
const wireSchema = S.Struct({
|
|
26
|
-
body: schema,
|
|
27
|
-
meta: QueueMeta
|
|
28
|
-
})
|
|
29
|
-
const wireSchemaJson = S.fromJsonString(S.toCodecJson(wireSchema))
|
|
30
|
-
const encodePublish = S.encodeEffect(wireSchemaJson)
|
|
31
|
-
const drainW = S.Struct({ body: drainSchema, meta: QueueMeta })
|
|
32
|
-
const drainWJson = S.fromJsonString(S.toCodecJson(drainW))
|
|
33
|
-
const parseDrain = flow(S.decodeUnknownEffectConcurrently(drainWJson), Effect.orDie)
|
|
34
|
-
|
|
35
|
-
return Effect.gen(function*() {
|
|
36
|
-
const sender = yield* Sender
|
|
37
|
-
const receiver = yield* Receiver
|
|
38
|
-
const silenceAndReportError = reportNonInterruptedFailure({ name: receiver.name })
|
|
39
|
-
const reportError = reportNonInterruptedFailureCause({ name: receiver.name })
|
|
40
|
-
|
|
41
|
-
// TODO: or do async?
|
|
42
|
-
// This will make sure that the host receives the error (MainFiberSet.join), who will then interrupt everything and commence a shutdown and restart of app
|
|
43
|
-
// const deferred = yield* Deferred.make<never, ServiceBusError | Error>()
|
|
44
|
-
|
|
45
|
-
const queue = {
|
|
46
|
-
drain: <DrainE, DrainR>(
|
|
47
|
-
handleEvent: (ks: DrainEvt) => Effect.Effect<void, DrainE, DrainR>,
|
|
48
|
-
sessionId?: string
|
|
49
|
-
) => {
|
|
50
|
-
const processMessage = Effect.fnUntraced(function*(messageBody: unknown) {
|
|
51
|
-
const { body, meta } = yield* parseDrain(messageBody).pipe(Effect.orDie)
|
|
52
|
-
let effect = InfraLogger
|
|
53
|
-
.logDebug(`[${receiver.name}] Processing incoming message`)
|
|
54
|
-
.pipe(
|
|
55
|
-
Effect.annotateLogs({ body: pretty(body), meta: pretty(meta) }),
|
|
56
|
-
Effect.andThen(handleEvent(body)),
|
|
57
|
-
Effect.orDie,
|
|
58
|
-
// we silenceAndReportError here, so that the error is reported, and moves into the Exit.
|
|
59
|
-
silenceAndReportError,
|
|
60
|
-
(_) => {
|
|
61
|
-
const args = messagingSpanArgs({
|
|
62
|
-
operation: "process",
|
|
63
|
-
system: "servicebus",
|
|
64
|
-
destination: receiver.name,
|
|
65
|
-
messageId: body.id,
|
|
66
|
-
conversationId: sessionId,
|
|
67
|
-
extra: { "messaging.message.type": body._tag, "messaging.message.body": body }
|
|
68
|
-
}, "consumer")
|
|
69
|
-
return setupRequestContextWithCustomSpan(
|
|
70
|
-
_,
|
|
71
|
-
meta,
|
|
72
|
-
args.name,
|
|
73
|
-
{
|
|
74
|
-
captureStackTrace: false,
|
|
75
|
-
kind: args.kind,
|
|
76
|
-
attributes: args.attributes
|
|
77
|
-
}
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
)
|
|
81
|
-
if (meta.span) {
|
|
82
|
-
effect = Effect.withParentSpan(effect, Tracer.externalSpan(meta.span))
|
|
83
|
-
}
|
|
84
|
-
// we reportError here, so that we report the error only, and keep flowing
|
|
85
|
-
const exit = yield* Effect.tapCause(effect, reportError)
|
|
86
|
-
return yield* exit
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
return receiver
|
|
90
|
-
.subscribe({
|
|
91
|
-
processMessage: (x) => processMessage(x.body).pipe(Effect.uninterruptible),
|
|
92
|
-
processError: (err) => reportQueueError(Cause.fail(err.error))
|
|
93
|
-
}, sessionId)
|
|
94
|
-
.pipe(Effect.andThen(Effect.never))
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
publish: Effect.fn(`publish ${sender.name}`, {
|
|
98
|
-
kind: "producer",
|
|
99
|
-
attributes: {
|
|
100
|
-
"messaging.system": "servicebus",
|
|
101
|
-
"messaging.operation.name": "publish",
|
|
102
|
-
"messaging.destination.name": sender.name
|
|
103
|
-
}
|
|
104
|
-
})(function*(...messages: NonEmptyReadonlyArray<Evt>) {
|
|
105
|
-
yield* Effect.annotateCurrentSpan({
|
|
106
|
-
"messaging.batch.message_count": messages.length,
|
|
107
|
-
"messaging.message.types": messages.map((_) => _._tag)
|
|
108
|
-
})
|
|
109
|
-
const requestContext = yield* getRequestContext
|
|
110
|
-
const msgs = yield* Effect.forEach(messages, (m) =>
|
|
111
|
-
encodePublish({ body: m, meta: requestContext }).pipe(
|
|
112
|
-
Effect.orDie,
|
|
113
|
-
Effect.map((body) => ({
|
|
114
|
-
body,
|
|
115
|
-
messageId: m.id, /* correllationid: requestId */
|
|
116
|
-
contentType: "application/json",
|
|
117
|
-
sessionId: "sessionId" in m ? m.sessionId as string : undefined as unknown as string // TODO: optional
|
|
118
|
-
}))
|
|
119
|
-
))
|
|
120
|
-
yield* sender.sendMessages(msgs)
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
return queue
|
|
124
|
-
})
|
|
125
|
-
}
|
package/src/RequestFiberSet.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import * as Context from "effect-app/Context"
|
|
3
|
-
import * as Effect from "effect-app/Effect"
|
|
4
|
-
import * as Layer from "effect-app/Layer"
|
|
5
|
-
import * as Option from "effect-app/Option"
|
|
6
|
-
import * as Fiber from "effect/Fiber"
|
|
7
|
-
import * as FiberSet from "effect/FiberSet"
|
|
8
|
-
import type * as Tracer from "effect/Tracer"
|
|
9
|
-
import { InfraLogger } from "./logger.ts"
|
|
10
|
-
import { reportRequestError, reportUnknownRequestError } from "./reportError.ts"
|
|
11
|
-
|
|
12
|
-
const getRootParentSpan = Effect.gen(function*() {
|
|
13
|
-
let span: Tracer.AnySpan | null = yield* Effect.currentSpan.pipe(
|
|
14
|
-
Effect.catchTag("NoSuchElementError", () => Effect.succeed(null))
|
|
15
|
-
)
|
|
16
|
-
if (!span) return span
|
|
17
|
-
while (span._tag === "Span" && Option.isSome(span.parent)) {
|
|
18
|
-
span = span.parent.value
|
|
19
|
-
}
|
|
20
|
-
return span
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
export const setRootParentSpan = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
24
|
-
getRootParentSpan.pipe(Effect.andThen((span) => span ? Effect.withParentSpan(self, span) : self))
|
|
25
|
-
|
|
26
|
-
const make = Effect.gen(function*() {
|
|
27
|
-
const set = yield* FiberSet.make<any, any>()
|
|
28
|
-
const add = (...fibers: Fiber.Fiber<any, any>[]) =>
|
|
29
|
-
Effect.sync(() => fibers.forEach((_) => FiberSet.addUnsafe(set, _)))
|
|
30
|
-
const addAll = (fibers: readonly Fiber.Fiber<any, any>[]) =>
|
|
31
|
-
Effect.sync(() => fibers.forEach((_) => FiberSet.addUnsafe(set, _)))
|
|
32
|
-
const join = FiberSet.size(set).pipe(
|
|
33
|
-
Effect.andThen((count) => InfraLogger.logInfo(`Joining ${count} current fibers on the RequestFiberSet`)),
|
|
34
|
-
Effect.andThen(FiberSet.join(set))
|
|
35
|
-
)
|
|
36
|
-
const run = FiberSet.run(set)
|
|
37
|
-
const register = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
38
|
-
self.pipe(Effect.forkChild, Effect.tap(add), Effect.andThen(Fiber.join))
|
|
39
|
-
|
|
40
|
-
// const waitUntilEmpty = Effect.gen(function*() {
|
|
41
|
-
// const currentSize = yield* FiberSet.size(set)
|
|
42
|
-
// if (currentSize === 0) {
|
|
43
|
-
// return
|
|
44
|
-
// }
|
|
45
|
-
// yield* Effect.logInfo("Waiting RequestFiberSet to be empty: " + currentSize)
|
|
46
|
-
// while ((yield* FiberSet.size(set)) > 0) yield* Effect.sleep("250 millis")
|
|
47
|
-
// yield* Effect.logDebug("RequestFiberSet is empty")
|
|
48
|
-
// })
|
|
49
|
-
// TODO: loop and interrupt all fibers in the set continuously?
|
|
50
|
-
const interrupt = Fiber.interruptAll(set)
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Forks the effect into a new fiber attached to the RequestFiberSet scope. Because the
|
|
54
|
-
* new fiber isn't attached to the parent, when the fiber executing the
|
|
55
|
-
* returned effect terminates, the forked fiber will continue running.
|
|
56
|
-
* The fiber will be interrupted when the RequestFiberSet scope is closed.
|
|
57
|
-
*
|
|
58
|
-
* Reports errors.
|
|
59
|
-
*/
|
|
60
|
-
function forkDaemonReport<R, E, A>(self: Effect.Effect<A, E, R>) {
|
|
61
|
-
return self.pipe(
|
|
62
|
-
reportRequestError,
|
|
63
|
-
Effect.uninterruptible,
|
|
64
|
-
run
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Forks the effect into a new fiber attached to the RequestFiberSet scope. Because the
|
|
70
|
-
* new fiber isn't attached to the parent, when the fiber executing the
|
|
71
|
-
* returned effect terminates, the forked fiber will continue running.
|
|
72
|
-
* The fiber will be interrupted when the RequestFiberSet scope is closed.
|
|
73
|
-
*
|
|
74
|
-
* Reports unexpected errors.
|
|
75
|
-
*/
|
|
76
|
-
function forkDaemonReportUnexpected<R, E, A>(self: Effect.Effect<A, E, R>) {
|
|
77
|
-
return self
|
|
78
|
-
.pipe(
|
|
79
|
-
reportUnknownRequestError,
|
|
80
|
-
Effect.uninterruptible,
|
|
81
|
-
run
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
interrupt,
|
|
87
|
-
join,
|
|
88
|
-
run,
|
|
89
|
-
add,
|
|
90
|
-
addAll,
|
|
91
|
-
register,
|
|
92
|
-
forkDaemonReport,
|
|
93
|
-
forkDaemonReportUnexpected
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Whenever you fork a fiber for a Request, and you want to prevent dependent services to close prematurely on interruption,
|
|
99
|
-
* like the ServiceBus Sender, you should register these fibers in this FiberSet.
|
|
100
|
-
*/
|
|
101
|
-
export class RequestFiberSet extends Context.Service<RequestFiberSet>()("RequestFiberSet", { make }) {
|
|
102
|
-
static readonly Live = Layer.effect(this, this.make)
|
|
103
|
-
static readonly register = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
104
|
-
this.pipe(Effect.andThen((_) => _.register(self)))
|
|
105
|
-
static readonly run = <A, E, R>(self: Effect.Effect<A, E, R>) => this.pipe(Effect.andThen((_) => _.run(self)))
|
|
106
|
-
static readonly forkDaemonReport = <R, E, A>(self: Effect.Effect<A, E, R>) =>
|
|
107
|
-
this.pipe(Effect.andThen((_) => _.forkDaemonReport(self)))
|
|
108
|
-
static readonly forkDaemonReportUnexpected = <R, E, A>(self: Effect.Effect<A, E, R>) =>
|
|
109
|
-
this.pipe(Effect.andThen((_) => _.forkDaemonReportUnexpected(self)))
|
|
110
|
-
}
|
package/src/ServiceBus.ts
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */
|
|
2
|
-
import { type OperationOptionsBase, type ProcessErrorArgs, ServiceBusClient, type ServiceBusMessage, type ServiceBusMessageBatch, type ServiceBusReceivedMessage, type ServiceBusReceiver } from "@azure/service-bus"
|
|
3
|
-
import * as Context from "effect-app/Context"
|
|
4
|
-
import * as Effect from "effect-app/Effect"
|
|
5
|
-
import * as Layer from "effect-app/Layer"
|
|
6
|
-
import * as Cause from "effect/Cause"
|
|
7
|
-
import * as Exit from "effect/Exit"
|
|
8
|
-
import * as FiberSet from "effect/FiberSet"
|
|
9
|
-
import type * as Scope from "effect/Scope"
|
|
10
|
-
import { InfraLogger } from "./logger.ts"
|
|
11
|
-
|
|
12
|
-
const logged = (name: string) => <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
13
|
-
Effect.logInfo(name).pipe(
|
|
14
|
-
Effect.andThen(self),
|
|
15
|
-
Effect.tap(Effect.logInfo(name + " done")),
|
|
16
|
-
Effect.withLogSpan(name)
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
function makeClient(url: string) {
|
|
20
|
-
return Effect.acquireRelease(
|
|
21
|
-
Effect.sync(() => new ServiceBusClient(url)).pipe(logged("ServiceBus.client.create")),
|
|
22
|
-
(client) => Effect.promise(() => client.close()).pipe(logged("ServiceBus.client.close"))
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class ServiceBusClientTag
|
|
27
|
-
extends Context.Opaque<ServiceBusClientTag, ServiceBusClient>()("@services/Client", { make: makeClient })
|
|
28
|
-
{
|
|
29
|
-
static readonly layer = (url: string) => this.toLayer(this.make(url))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const makeSender_ = Effect.fnUntraced(function*(queueName: string) {
|
|
33
|
-
const serviceBusClient = yield* ServiceBusClientTag
|
|
34
|
-
|
|
35
|
-
return yield* Effect.acquireRelease(
|
|
36
|
-
Effect.sync(() => serviceBusClient.createSender(queueName)).pipe(
|
|
37
|
-
logged(`ServiceBus.sender.create ${queueName}`)
|
|
38
|
-
),
|
|
39
|
-
(sender) => Effect.promise(() => sender.close()).pipe(logged(`ServiceBus.sender.close ${queueName}`))
|
|
40
|
-
)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
const makeSender = Effect.fnUntraced(function*(name: string) {
|
|
44
|
-
const sender = yield* makeSender_(name)
|
|
45
|
-
const sendMessages = Effect.fnUntraced(function*(
|
|
46
|
-
messages: ServiceBusMessage | ServiceBusMessage[] | ServiceBusMessageBatch,
|
|
47
|
-
options?: Omit<OperationOptionsBase, "abortSignal">
|
|
48
|
-
) {
|
|
49
|
-
return yield* Effect.promise((abortSignal) => sender.sendMessages(messages, { ...options, abortSignal }))
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
return { name, sendMessages }
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
export class Sender extends Context.Opaque<Sender, {
|
|
56
|
-
name: string
|
|
57
|
-
sendMessages: (
|
|
58
|
-
messages: ServiceBusMessage | ServiceBusMessage[] | ServiceBusMessageBatch,
|
|
59
|
-
options?: Omit<OperationOptionsBase, "abortSignal">
|
|
60
|
-
) => Effect.Effect<void>
|
|
61
|
-
}>()("Sender", { make: makeSender }) {
|
|
62
|
-
static readonly layer = (name: string) => this.toLayer(this.make(name))
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export const SenderTag = <Id>() => <Key extends string>(queueName: Key) => {
|
|
66
|
-
const tag = Context.Service<Id, Sender>(`ServiceBus.Sender.${queueName}`)
|
|
67
|
-
|
|
68
|
-
return Object.assign(tag, {
|
|
69
|
-
layer: Layer.effect(
|
|
70
|
-
tag,
|
|
71
|
-
Sender.make(queueName).pipe(Effect.map(Sender.of))
|
|
72
|
-
)
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const makeReceiver = Effect.fnUntraced(function*(name: string) {
|
|
77
|
-
const serviceBusClient = yield* ServiceBusClientTag
|
|
78
|
-
|
|
79
|
-
const makeReceiver = Effect.fnUntraced(
|
|
80
|
-
function*(queueName: string, waitTillEmpty: Effect.Effect<void>, sessionId?: string) {
|
|
81
|
-
const annotate = sessionId !== undefined
|
|
82
|
-
? Effect.annotateLogs({ "messaging.session.id": sessionId })
|
|
83
|
-
: <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => self
|
|
84
|
-
return yield* Effect.acquireRelease(
|
|
85
|
-
(sessionId
|
|
86
|
-
? Effect.promise(() => serviceBusClient.acceptSession(queueName, sessionId))
|
|
87
|
-
: Effect.sync(() => serviceBusClient.createReceiver(queueName)))
|
|
88
|
-
.pipe(logged(`ServiceBus.receiver.create ${queueName}`), annotate),
|
|
89
|
-
(r) =>
|
|
90
|
-
waitTillEmpty.pipe(
|
|
91
|
-
logged(`ServiceBus.receiver.waitTillEmpty ${queueName}`),
|
|
92
|
-
Effect.andThen(
|
|
93
|
-
Effect.promise(() => r.close()).pipe(
|
|
94
|
-
logged(`ServiceBus.receiver.close ${queueName}`)
|
|
95
|
-
)
|
|
96
|
-
),
|
|
97
|
-
logged(`ServiceBus.receiver.release ${queueName}`),
|
|
98
|
-
annotate
|
|
99
|
-
)
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
const make = (waitTillEmpty: Effect.Effect<void>) => makeReceiver(name, waitTillEmpty)
|
|
105
|
-
|
|
106
|
-
const makeSession = (sessionId: string, waitTillEmpty: Effect.Effect<void>) =>
|
|
107
|
-
makeReceiver(name, waitTillEmpty, sessionId)
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
name,
|
|
111
|
-
make,
|
|
112
|
-
makeSession,
|
|
113
|
-
subscribe: Effect.fnUntraced(function*<RMsg, RErr>(hndlr: MessageHandlers<RMsg, RErr>, sessionId?: string) {
|
|
114
|
-
const fs = yield* FiberSet.make()
|
|
115
|
-
const fr = yield* FiberSet.runtime(fs)<RMsg | RErr>()
|
|
116
|
-
const wait = Effect
|
|
117
|
-
.gen(function*() {
|
|
118
|
-
if ((yield* FiberSet.size(fs)) > 0) {
|
|
119
|
-
yield* InfraLogger.logDebug("Waiting ServiceBusFiberSet to be empty: " + (yield* FiberSet.size(fs)))
|
|
120
|
-
}
|
|
121
|
-
while ((yield* FiberSet.size(fs)) > 0) yield* Effect.sleep("250 millis")
|
|
122
|
-
})
|
|
123
|
-
const r = yield* sessionId
|
|
124
|
-
? makeSession(
|
|
125
|
-
sessionId,
|
|
126
|
-
wait
|
|
127
|
-
)
|
|
128
|
-
: make(wait)
|
|
129
|
-
|
|
130
|
-
const runEffect = <E>(effect: Effect.Effect<void, E, RMsg | RErr>) =>
|
|
131
|
-
new Promise<void>((resolve, reject) =>
|
|
132
|
-
fr(effect)
|
|
133
|
-
.addObserver((exit) => {
|
|
134
|
-
if (Exit.isSuccess(exit)) {
|
|
135
|
-
resolve(exit.value)
|
|
136
|
-
} else {
|
|
137
|
-
// disable @typescript-eslint/prefer-promise-reject-errors
|
|
138
|
-
reject(Cause.pretty(exit.cause))
|
|
139
|
-
}
|
|
140
|
-
})
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
const annotate = sessionId !== undefined
|
|
144
|
-
? Effect.annotateLogs({ "messaging.session.id": sessionId })
|
|
145
|
-
: <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => self
|
|
146
|
-
yield* Effect.acquireRelease(
|
|
147
|
-
Effect
|
|
148
|
-
.sync(() => {
|
|
149
|
-
const s = r
|
|
150
|
-
.subscribe({
|
|
151
|
-
processError: (err) =>
|
|
152
|
-
runEffect(
|
|
153
|
-
hndlr
|
|
154
|
-
.processError(err)
|
|
155
|
-
.pipe(
|
|
156
|
-
Effect.catchCause((cause) => Effect.logError("ServiceBus Error", cause)),
|
|
157
|
-
annotate
|
|
158
|
-
)
|
|
159
|
-
),
|
|
160
|
-
processMessage: (msg) => runEffect(hndlr.processMessage(msg).pipe(annotate))
|
|
161
|
-
// DO NOT CATCH ERRORS here as they should return to the queue!
|
|
162
|
-
})
|
|
163
|
-
return { close: Effect.promise(() => s.close()) }
|
|
164
|
-
})
|
|
165
|
-
.pipe(logged("ServiceBus.subscription.create"), annotate),
|
|
166
|
-
(subscription) =>
|
|
167
|
-
subscription.close.pipe(
|
|
168
|
-
logged("ServiceBus.subscription.close"),
|
|
169
|
-
annotate
|
|
170
|
-
)
|
|
171
|
-
) as Effect.Effect<void, never, Scope.Scope> // wth is going on here
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
export class Receiver extends Context.Opaque<Receiver, {
|
|
177
|
-
name: string
|
|
178
|
-
make: (waitTillEmpty: Effect.Effect<void>) => Effect.Effect<ServiceBusReceiver, never, Scope.Scope>
|
|
179
|
-
makeSession: (
|
|
180
|
-
sessionId: string,
|
|
181
|
-
waitTillEmpty: Effect.Effect<void>
|
|
182
|
-
) => Effect.Effect<ServiceBusReceiver, never, Scope.Scope>
|
|
183
|
-
subscribe<RMsg, RErr>(
|
|
184
|
-
hndlr: MessageHandlers<RMsg, RErr>,
|
|
185
|
-
sessionId?: string
|
|
186
|
-
): Effect.Effect<void, never, Scope.Scope | RMsg | RErr>
|
|
187
|
-
}>()("Receiver") {
|
|
188
|
-
static readonly make = makeReceiver
|
|
189
|
-
static readonly layer = (name: string) => this.toLayer(makeReceiver(name))
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export const ReceiverTag = <Id>() => <Key extends string>(queueName: Key) => {
|
|
193
|
-
const tag = Context.Service<Id, Receiver>(`ServiceBus.Receiver.${queueName}`)
|
|
194
|
-
|
|
195
|
-
return Object.assign(tag, {
|
|
196
|
-
layer: Layer.effect(
|
|
197
|
-
tag,
|
|
198
|
-
makeReceiver(queueName).pipe(Effect.map(Receiver.of))
|
|
199
|
-
)
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export const SenderReceiver = (queue: string, queueDrain?: string) =>
|
|
204
|
-
Layer.mergeAll(Sender.layer(queue), Receiver.layer(queueDrain ?? queue))
|
|
205
|
-
|
|
206
|
-
export interface MessageHandlers<RMsg, RErr> {
|
|
207
|
-
/**
|
|
208
|
-
* Handler that processes messages from service bus.
|
|
209
|
-
*
|
|
210
|
-
* @param message - A message received from Service Bus.
|
|
211
|
-
*/
|
|
212
|
-
processMessage(message: ServiceBusReceivedMessage): Effect.Effect<void, never, RMsg>
|
|
213
|
-
/**
|
|
214
|
-
* Handler that processes errors that occur during receiving.
|
|
215
|
-
* @param args - The error and additional context to indicate where
|
|
216
|
-
* the error originated.
|
|
217
|
-
*/
|
|
218
|
-
processError(args: ProcessErrorArgs): Effect.Effect<void, never, RErr>
|
|
219
|
-
}
|
package/src/memQueue.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import * as Context from "effect-app/Context"
|
|
2
|
-
import * as Effect from "effect-app/Effect"
|
|
3
|
-
import * as Queue from "effect/Queue"
|
|
4
|
-
|
|
5
|
-
const make = Effect
|
|
6
|
-
.gen(function*() {
|
|
7
|
-
const store = yield* Effect.sync(() => new Map<string, Queue.Queue<string>>())
|
|
8
|
-
|
|
9
|
-
return {
|
|
10
|
-
getOrCreateQueue: Effect.fnUntraced(function*(k: string) {
|
|
11
|
-
const q = store.get(k)
|
|
12
|
-
if (q) return q
|
|
13
|
-
const newQ = yield* Queue.unbounded<string>()
|
|
14
|
-
store.set(k, newQ)
|
|
15
|
-
return newQ
|
|
16
|
-
})
|
|
17
|
-
}
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
export class MemQueue extends Context.Opaque<MemQueue>()("effect-app/MemQueue", { make }) {
|
|
21
|
-
static readonly Live = this.toLayer(this.make)
|
|
22
|
-
}
|