@effect-app/infra 1.22.0 → 1.23.1

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @effect-app/infra
2
2
 
3
+ ## 1.23.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [13b7aa1]
8
+ - effect-app@1.17.2
9
+ - @effect-app/infra-adapters@1.11.4
10
+
11
+ ## 1.23.0
12
+
13
+ ### Minor Changes
14
+
15
+ - feat: add SQLQueue
16
+
3
17
  ## 1.22.0
4
18
 
5
19
  ### Minor Changes
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.QueueId = void 0;
7
+ exports.makeSQLQueue = makeSQLQueue;
8
+ var _setupRequest = require("@effect-app/infra/api/setupRequest");
9
+ var _RequestContext = require("@effect-app/infra/RequestContext");
10
+ var _errors = require("@effect-app/infra/services/QueueMaker/errors");
11
+ var _service = require("@effect-app/infra/services/QueueMaker/service");
12
+ var _RequestContextContainer = require("@effect-app/infra/services/RequestContextContainer");
13
+ var _sql = require("@effect/sql");
14
+ var _dateFns = require("date-fns");
15
+ var _effectApp = require("effect-app");
16
+ var _ids = require("effect-app/ids");
17
+ var _schema = require("effect-app/schema");
18
+ var _utils = require("effect-app/utils");
19
+ const QueueId = exports.QueueId = _effectApp.S.Number.pipe(_effectApp.S.brand("QueueId"));
20
+ /**
21
+ * Currently limited to one process draining at a time, due to in-process Semaphore instead of row-level locking.
22
+ */
23
+ function makeSQLQueue(queueName, queueDrainName, schema, drainSchema) {
24
+ return _effectApp.Effect.gen(function* () {
25
+ const base = {
26
+ id: _sql.Model.Generated(QueueId),
27
+ meta: _sql.Model.JsonFromString(_service.QueueMeta),
28
+ name: _effectApp.S.NonEmptyString255,
29
+ createdAt: _sql.Model.DateTimeInsert,
30
+ updatedAt: _sql.Model.DateTimeUpdate,
31
+ // TODO: at+owner
32
+ processingAt: _sql.Model.FieldOption(_effectApp.S.Date),
33
+ finishedAt: _sql.Model.FieldOption(_effectApp.S.Date)
34
+ // TODO: record locking.. / optimistic locking
35
+ // rowVersion: Model.DateTimeFromNumberWithNow
36
+ };
37
+ class Queue extends _sql.Model.Class("Queue")({
38
+ body: _sql.Model.JsonFromString(schema),
39
+ ...base
40
+ }) {}
41
+ class Drain extends _sql.Model.Class("Drain")({
42
+ body: _sql.Model.JsonFromString(drainSchema),
43
+ ...base
44
+ }) {}
45
+ const sql = yield* _sql.SqlClient.SqlClient;
46
+ const queueRepo = yield* _sql.Model.makeRepository(Queue, {
47
+ tableName: "queue",
48
+ spanPrefix: "QueueRepo",
49
+ idColumn: "id"
50
+ });
51
+ const drainRepo = yield* _sql.Model.makeRepository(Drain, {
52
+ tableName: "queue",
53
+ spanPrefix: "DrainRepo",
54
+ idColumn: "id"
55
+ });
56
+ const decodeDrain = _effectApp.S.decode(Drain);
57
+ const drain = () => {
58
+ const limit = (0, _dateFns.subMinutes)(new Date(), 15);
59
+ return sql`SELECT *
60
+ FROM queue
61
+ WHERE name = ${queueDrainName} AND finishedAt IS NULL AND (processingAt IS NULL OR processingAt < ${limit.getTime()})
62
+ LIMIT 1`;
63
+ };
64
+ // temporary workaround until we have a SQLite rowversion..
65
+ const lock = yield* _effectApp.Effect.makeSemaphore(1);
66
+ const q = {
67
+ offer: (body, meta) => _effectApp.Effect.gen(function* () {
68
+ yield* queueRepo.insert(Queue.insert.make({
69
+ body,
70
+ meta,
71
+ name: queueName,
72
+ processingAt: _effectApp.Option.none(),
73
+ finishedAt: _effectApp.Option.none()
74
+ }));
75
+ }),
76
+ take: _effectApp.Effect.gen(function* () {
77
+ while (true) {
78
+ const first = yield* lock.withPermits(1)(_effectApp.Effect.gen(function* () {
79
+ const [first] = yield* drain();
80
+ if (first) {
81
+ const dec = yield* decodeDrain(first);
82
+ const {
83
+ createdAt,
84
+ updatedAt,
85
+ ...rest
86
+ } = dec;
87
+ yield* drainRepo.update(Drain.update.make({
88
+ ...rest,
89
+ processingAt: _effectApp.Option.some(new Date())
90
+ }));
91
+ return dec;
92
+ }
93
+ return null;
94
+ }));
95
+ if (first) return first;
96
+ yield* _effectApp.Effect.sleep(250);
97
+ }
98
+ }),
99
+ finish: ({
100
+ createdAt,
101
+ updatedAt,
102
+ ...q
103
+ }) => drainRepo.update(Drain.update.make({
104
+ ...q,
105
+ finishedAt: _effectApp.Option.some(new Date())
106
+ }))
107
+ };
108
+ const rcc = yield* _RequestContextContainer.RequestContextContainer;
109
+ return {
110
+ publish: (...messages) => _effectApp.Effect.gen(function* ($) {
111
+ const requestContext = yield* $(rcc.requestContext);
112
+ const span = yield* $(_effectApp.Effect.serviceOption(_effectApp.Tracer.ParentSpan));
113
+ return yield* $(_effectApp.Effect.forEach(messages, m => q.offer(m, {
114
+ requestContext: new _RequestContext.RequestContext(requestContext),
115
+ // workaround Schema expecting exact class
116
+ span: _effectApp.Option.getOrUndefined(span)
117
+ }), {
118
+ discard: true
119
+ }));
120
+ }).pipe(_effectApp.Effect.withSpan("queue.publish: " + queueName, {
121
+ captureStackTrace: false,
122
+ kind: "producer",
123
+ attributes: {
124
+ "message_tags": messages.map(_ => _._tag)
125
+ }
126
+ })),
127
+ drain: (handleEvent, sessionId) => _effectApp.Effect.gen(function* () {
128
+ const silenceAndReportError = (0, _errors.reportNonInterruptedFailure)({
129
+ name: "MemQueue.drain." + queueDrainName
130
+ });
131
+ const processMessage = msg => _effectApp.Effect.succeed(msg).pipe(_effectApp.Effect.flatMap(({
132
+ body,
133
+ meta
134
+ }) => {
135
+ let effect = _effectApp.Effect.logDebug(`[${queueDrainName}] Processing incoming message`).pipe(_effectApp.Effect.annotateLogs({
136
+ body: (0, _utils.pretty)(body),
137
+ meta: (0, _utils.pretty)(meta)
138
+ }), _effectApp.Effect.zipRight(handleEvent(body)), silenceAndReportError, _ => (0, _setupRequest.setupRequestContext)(_, _RequestContext.RequestContext.inherit(meta.requestContext, {
139
+ id: (0, _ids.RequestId)(body.id),
140
+ locale: "en",
141
+ name: (0, _schema.NonEmptyString255)(`${queueDrainName}.${body._tag}`)
142
+ })), _effectApp.Effect.withSpan(`queue.drain: ${queueDrainName}.${body._tag}`, {
143
+ captureStackTrace: false,
144
+ kind: "consumer",
145
+ attributes: {
146
+ "queue.name": queueDrainName,
147
+ "queue.sessionId": sessionId,
148
+ "queue.input": body
149
+ }
150
+ }));
151
+ if (meta.span) {
152
+ effect = _effectApp.Effect.withParentSpan(effect, _effectApp.Tracer.externalSpan(meta.span));
153
+ }
154
+ return effect;
155
+ }));
156
+ return yield* q.take.pipe(_effectApp.Effect.flatMap(x => processMessage(x).pipe(_effectApp.Effect.uninterruptible, _effectApp.Effect.fork, _effectApp.Effect.flatMap(_effectApp.Fiber.join), _effectApp.Effect.tap(q.finish(x)))), silenceAndReportError, _effectApp.Effect.forever);
157
+ })
158
+ };
159
+ });
160
+ }
161
+ //# sourceMappingURL=SQLQueue.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SQLQueue.cjs","names":["_setupRequest","require","_RequestContext","_errors","_service","_RequestContextContainer","_sql","_dateFns","_effectApp","_ids","_schema","_utils","QueueId","exports","S","Number","pipe","brand","makeSQLQueue","queueName","queueDrainName","schema","drainSchema","Effect","gen","base","id","Model","Generated","meta","JsonFromString","QueueMeta","name","NonEmptyString255","createdAt","DateTimeInsert","updatedAt","DateTimeUpdate","processingAt","FieldOption","Date","finishedAt","Queue","Class","body","Drain","sql","SqlClient","queueRepo","makeRepository","tableName","spanPrefix","idColumn","drainRepo","decodeDrain","decode","drain","limit","subMinutes","getTime","lock","makeSemaphore","q","offer","insert","make","Option","none","take","first","withPermits","dec","rest","update","some","sleep","finish","rcc","RequestContextContainer","publish","messages","$","requestContext","span","serviceOption","Tracer","ParentSpan","forEach","m","RequestContext","getOrUndefined","discard","withSpan","captureStackTrace","kind","attributes","map","_","_tag","handleEvent","sessionId","silenceAndReportError","reportNonInterruptedFailure","processMessage","msg","succeed","flatMap","effect","logDebug","annotateLogs","pretty","zipRight","setupRequestContext","inherit","RequestId","locale","withParentSpan","externalSpan","x","uninterruptible","fork","Fiber","join","tap","forever"],"sources":["../../../src/services/QueueMaker/SQLQueue.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,22 @@
1
+ import { RequestContextContainer } from "@effect-app/infra/services/RequestContextContainer";
2
+ import { SqlClient } from "@effect/sql";
3
+ import { Effect, S, Tracer } from "effect-app";
4
+ import { NonEmptyString255 } from "effect-app/schema";
5
+ export declare const QueueId: S.brand<typeof import("@effect/schema/Schema").Number & {
6
+ withDefault: S.PropertySignature<":", number, never, ":", number, true, never>;
7
+ }, "QueueId">;
8
+ export type QueueId = typeof QueueId.Type;
9
+ /**
10
+ * Currently limited to one process draining at a time, due to in-process Semaphore instead of row-level locking.
11
+ */
12
+ export declare function makeSQLQueue<Evt extends {
13
+ id: S.StringId;
14
+ _tag: string;
15
+ }, DrainEvt extends {
16
+ id: S.StringId;
17
+ _tag: string;
18
+ }, EvtE, DrainEvtE>(queueName: NonEmptyString255, queueDrainName: NonEmptyString255, schema: S.Schema<Evt, EvtE>, drainSchema: S.Schema<DrainEvt, DrainEvtE>): Effect.Effect<{
19
+ publish: (messages_0: Evt, ...messages: Evt[]) => Effect.Effect<void, never, never>;
20
+ drain: <DrainE, DrainR>(handleEvent: (ks: DrainEvt) => Effect<void, DrainE, DrainR>, sessionId?: string) => Effect.Effect<never, never, RequestContextContainer | import("../Store/ContextMapContainer.js").ContextMapContainer | Exclude<Exclude<Exclude<DrainR, Tracer.ParentSpan>, never>, Tracer.ParentSpan>>;
21
+ }, never, RequestContextContainer | SqlClient.SqlClient>;
22
+ //# sourceMappingURL=SQLQueue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SQLQueue.d.ts","sourceRoot":"","sources":["../../../src/services/QueueMaker/SQLQueue.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,uBAAuB,EAAE,MAAM,oDAAoD,CAAA;AAC5F,OAAO,EAAS,SAAS,EAAE,MAAM,aAAa,CAAA;AAE9C,OAAO,EAAE,MAAM,EAAiB,CAAC,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAE7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAGrD,eAAO,MAAM,OAAO;;aAAoC,CAAA;AACxD,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAEzC;;GAEG;AACH,wBAAgB,YAAY,CAC1B,GAAG,SAAS;IAAE,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC5C,QAAQ,SAAS;IAAE,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACjD,IAAI,EACJ,SAAS,EAET,SAAS,EAAE,iBAAiB,EAC5B,cAAc,EAAE,iBAAiB,EACjC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,EAC3B,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC;;YAgH9B,MAAM,EAAE,MAAM,eACP,CAAC,EAAE,EAAE,QAAQ,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,cAC/C,MAAM;yDA0DzB"}
@@ -0,0 +1,143 @@
1
+ import { setupRequestContext } from "@effect-app/infra/api/setupRequest";
2
+ import { RequestContext } from "@effect-app/infra/RequestContext";
3
+ import { reportNonInterruptedFailure } from "@effect-app/infra/services/QueueMaker/errors";
4
+ import { QueueMeta } from "@effect-app/infra/services/QueueMaker/service";
5
+ import { RequestContextContainer } from "@effect-app/infra/services/RequestContextContainer";
6
+ import { Model, SqlClient } from "@effect/sql";
7
+ import { subMinutes } from "date-fns";
8
+ import { Effect, Fiber, Option, S, Tracer } from "effect-app";
9
+ import { RequestId } from "effect-app/ids";
10
+ import { NonEmptyString255 } from "effect-app/schema";
11
+ import { pretty } from "effect-app/utils";
12
+ export const QueueId = S.Number.pipe(S.brand("QueueId"));
13
+ /**
14
+ * Currently limited to one process draining at a time, due to in-process Semaphore instead of row-level locking.
15
+ */
16
+ export function makeSQLQueue(queueName, queueDrainName, schema, drainSchema) {
17
+ return Effect.gen(function* () {
18
+ const base = {
19
+ id: Model.Generated(QueueId),
20
+ meta: Model.JsonFromString(QueueMeta),
21
+ name: S.NonEmptyString255,
22
+ createdAt: Model.DateTimeInsert,
23
+ updatedAt: Model.DateTimeUpdate,
24
+ // TODO: at+owner
25
+ processingAt: Model.FieldOption(S.Date),
26
+ finishedAt: Model.FieldOption(S.Date)
27
+ // TODO: record locking.. / optimistic locking
28
+ // rowVersion: Model.DateTimeFromNumberWithNow
29
+ };
30
+ class Queue extends Model.Class("Queue")({
31
+ body: Model.JsonFromString(schema),
32
+ ...base
33
+ }) {
34
+ }
35
+ class Drain extends Model.Class("Drain")({
36
+ body: Model.JsonFromString(drainSchema),
37
+ ...base
38
+ }) {
39
+ }
40
+ const sql = yield* SqlClient.SqlClient;
41
+ const queueRepo = yield* Model.makeRepository(Queue, {
42
+ tableName: "queue",
43
+ spanPrefix: "QueueRepo",
44
+ idColumn: "id"
45
+ });
46
+ const drainRepo = yield* Model.makeRepository(Drain, {
47
+ tableName: "queue",
48
+ spanPrefix: "DrainRepo",
49
+ idColumn: "id"
50
+ });
51
+ const decodeDrain = S.decode(Drain);
52
+ const drain = () => {
53
+ const limit = subMinutes(new Date(), 15);
54
+ return sql `SELECT *
55
+ FROM queue
56
+ WHERE name = ${queueDrainName} AND finishedAt IS NULL AND (processingAt IS NULL OR processingAt < ${limit.getTime()})
57
+ LIMIT 1`;
58
+ };
59
+ // temporary workaround until we have a SQLite rowversion..
60
+ const lock = yield* Effect.makeSemaphore(1);
61
+ const q = {
62
+ offer: (body, meta) => Effect.gen(function* () {
63
+ yield* queueRepo.insert(Queue.insert.make({
64
+ body,
65
+ meta,
66
+ name: queueName,
67
+ processingAt: Option.none(),
68
+ finishedAt: Option.none()
69
+ }));
70
+ }),
71
+ take: Effect.gen(function* () {
72
+ while (true) {
73
+ const first = yield* lock.withPermits(1)(Effect.gen(function* () {
74
+ const [first] = yield* drain();
75
+ if (first) {
76
+ const dec = yield* decodeDrain(first);
77
+ const { createdAt, updatedAt, ...rest } = dec;
78
+ yield* drainRepo.update(Drain.update.make({ ...rest, processingAt: Option.some(new Date()) }));
79
+ return dec;
80
+ }
81
+ return null;
82
+ }));
83
+ if (first)
84
+ return first;
85
+ yield* Effect.sleep(250);
86
+ }
87
+ }),
88
+ finish: ({ createdAt, updatedAt, ...q }) => drainRepo.update(Drain.update.make({ ...q, finishedAt: Option.some(new Date()) }))
89
+ };
90
+ const rcc = yield* RequestContextContainer;
91
+ return {
92
+ publish: (...messages) => Effect
93
+ .gen(function* ($) {
94
+ const requestContext = yield* $(rcc.requestContext);
95
+ const span = yield* $(Effect.serviceOption(Tracer.ParentSpan));
96
+ return yield* $(Effect
97
+ .forEach(messages, (m) => q.offer(m, {
98
+ requestContext: new RequestContext(requestContext), // workaround Schema expecting exact class
99
+ span: Option.getOrUndefined(span)
100
+ }), {
101
+ discard: true
102
+ }));
103
+ })
104
+ .pipe(Effect.withSpan("queue.publish: " + queueName, {
105
+ captureStackTrace: false,
106
+ kind: "producer",
107
+ attributes: { "message_tags": messages.map((_) => _._tag) }
108
+ })),
109
+ drain: (handleEvent, sessionId) => Effect.gen(function* () {
110
+ const silenceAndReportError = reportNonInterruptedFailure({ name: "MemQueue.drain." + queueDrainName });
111
+ const processMessage = (msg) => Effect
112
+ .succeed(msg)
113
+ .pipe(Effect
114
+ .flatMap(({ body, meta }) => {
115
+ let effect = Effect
116
+ .logDebug(`[${queueDrainName}] Processing incoming message`)
117
+ .pipe(Effect.annotateLogs({ body: pretty(body), meta: pretty(meta) }), Effect.zipRight(handleEvent(body)), silenceAndReportError, (_) => setupRequestContext(_, RequestContext.inherit(meta.requestContext, {
118
+ id: RequestId(body.id),
119
+ locale: "en",
120
+ name: NonEmptyString255(`${queueDrainName}.${body._tag}`)
121
+ })), Effect
122
+ .withSpan(`queue.drain: ${queueDrainName}.${body._tag}`, {
123
+ captureStackTrace: false,
124
+ kind: "consumer",
125
+ attributes: {
126
+ "queue.name": queueDrainName,
127
+ "queue.sessionId": sessionId,
128
+ "queue.input": body
129
+ }
130
+ }));
131
+ if (meta.span) {
132
+ effect = Effect.withParentSpan(effect, Tracer.externalSpan(meta.span));
133
+ }
134
+ return effect;
135
+ }));
136
+ return yield* q
137
+ .take
138
+ .pipe(Effect.flatMap((x) => processMessage(x).pipe(Effect.uninterruptible, Effect.fork, Effect.flatMap(Fiber.join), Effect.tap(q.finish(x)))), silenceAndReportError, Effect.forever);
139
+ })
140
+ };
141
+ });
142
+ }
143
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiU1FMUXVldWUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvc2VydmljZXMvUXVldWVNYWtlci9TUUxRdWV1ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxvQ0FBb0MsQ0FBQTtBQUN4RSxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sa0NBQWtDLENBQUE7QUFDakUsT0FBTyxFQUFFLDJCQUEyQixFQUFFLE1BQU0sOENBQThDLENBQUE7QUFFMUYsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLCtDQUErQyxDQUFBO0FBQ3pFLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLG9EQUFvRCxDQUFBO0FBQzVGLE9BQU8sRUFBRSxLQUFLLEVBQUUsU0FBUyxFQUFFLE1BQU0sYUFBYSxDQUFBO0FBQzlDLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxVQUFVLENBQUE7QUFDckMsT0FBTyxFQUFFLE1BQU0sRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLENBQUMsRUFBRSxNQUFNLEVBQUUsTUFBTSxZQUFZLENBQUE7QUFDN0QsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGdCQUFnQixDQUFBO0FBQzFDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLG1CQUFtQixDQUFBO0FBQ3JELE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQTtBQUV6QyxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFBO0FBR3hEOztHQUVHO0FBQ0gsTUFBTSxVQUFVLFlBQVksQ0FNMUIsU0FBNEIsRUFDNUIsY0FBaUMsRUFDakMsTUFBMkIsRUFDM0IsV0FBMEM7SUFFMUMsT0FBTyxNQUFNLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQztRQUN6QixNQUFNLElBQUksR0FBRztZ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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-app/infra",
3
- "version": "1.22.0",
3
+ "version": "1.23.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -18,10 +18,10 @@
18
18
  "proper-lockfile": "^4.1.2",
19
19
  "pure-rand": "6.1.0",
20
20
  "redlock": "^4.2.0",
21
+ "@effect-app/infra-adapters": "1.11.4",
22
+ "effect-app": "1.17.2",
21
23
  "@effect-app/core": "1.10.1",
22
- "@effect-app/schema": "1.12.1",
23
- "effect-app": "1.17.1",
24
- "@effect-app/infra-adapters": "1.11.3"
24
+ "@effect-app/schema": "1.12.1"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@babel/cli": "^7.25.6",
@@ -44,6 +44,7 @@
44
44
  "express": "^4.21.0",
45
45
  "@effect/platform": "^0.66.2",
46
46
  "@effect/schema": "^0.74.1",
47
+ "@effect/sql": "^0.14.0",
47
48
  "@effect/vitest": "^0.10.5",
48
49
  "effect": "^3.8.4"
49
50
  },
@@ -415,6 +416,16 @@
415
416
  "default": "./_cjs/services/OperationsRepo.cjs"
416
417
  }
417
418
  },
419
+ "./services/QueueMaker/SQLQueue": {
420
+ "import": {
421
+ "types": "./dist/services/QueueMaker/SQLQueue.d.ts",
422
+ "default": "./dist/services/QueueMaker/SQLQueue.js"
423
+ },
424
+ "require": {
425
+ "types": "./dist/services/QueueMaker/SQLQueue.d.ts",
426
+ "default": "./_cjs/services/QueueMaker/SQLQueue.cjs"
427
+ }
428
+ },
418
429
  "./services/QueueMaker/errors": {
419
430
  "import": {
420
431
  "types": "./dist/services/QueueMaker/errors.d.ts",
@@ -0,0 +1,201 @@
1
+ import { setupRequestContext } from "@effect-app/infra/api/setupRequest"
2
+ import { RequestContext } from "@effect-app/infra/RequestContext"
3
+ import { reportNonInterruptedFailure } from "@effect-app/infra/services/QueueMaker/errors"
4
+ import type { QueueBase } from "@effect-app/infra/services/QueueMaker/service"
5
+ import { QueueMeta } from "@effect-app/infra/services/QueueMaker/service"
6
+ import { RequestContextContainer } from "@effect-app/infra/services/RequestContextContainer"
7
+ import { Model, SqlClient } from "@effect/sql"
8
+ import { subMinutes } from "date-fns"
9
+ import { Effect, Fiber, Option, S, Tracer } from "effect-app"
10
+ import { RequestId } from "effect-app/ids"
11
+ import { NonEmptyString255 } from "effect-app/schema"
12
+ import { pretty } from "effect-app/utils"
13
+
14
+ export const QueueId = S.Number.pipe(S.brand("QueueId"))
15
+ export type QueueId = typeof QueueId.Type
16
+
17
+ /**
18
+ * Currently limited to one process draining at a time, due to in-process Semaphore instead of row-level locking.
19
+ */
20
+ export function makeSQLQueue<
21
+ Evt extends { id: S.StringId; _tag: string },
22
+ DrainEvt extends { id: S.StringId; _tag: string },
23
+ EvtE,
24
+ DrainEvtE
25
+ >(
26
+ queueName: NonEmptyString255,
27
+ queueDrainName: NonEmptyString255,
28
+ schema: S.Schema<Evt, EvtE>,
29
+ drainSchema: S.Schema<DrainEvt, DrainEvtE>
30
+ ) {
31
+ return Effect.gen(function*() {
32
+ const base = {
33
+ id: Model.Generated(QueueId),
34
+ meta: Model.JsonFromString(QueueMeta),
35
+ name: S.NonEmptyString255,
36
+ createdAt: Model.DateTimeInsert,
37
+ updatedAt: Model.DateTimeUpdate,
38
+ // TODO: at+owner
39
+ processingAt: Model.FieldOption(S.Date),
40
+ finishedAt: Model.FieldOption(S.Date)
41
+ // TODO: record locking.. / optimistic locking
42
+ // rowVersion: Model.DateTimeFromNumberWithNow
43
+ }
44
+ class Queue extends Model.Class<Queue>("Queue")({
45
+ body: Model.JsonFromString(schema),
46
+ ...base
47
+ }) {}
48
+ class Drain extends Model.Class<Drain>("Drain")({
49
+ body: Model.JsonFromString(drainSchema),
50
+ ...base
51
+ }) {}
52
+ const sql = yield* SqlClient.SqlClient
53
+
54
+ const queueRepo = yield* Model.makeRepository(Queue, {
55
+ tableName: "queue",
56
+ spanPrefix: "QueueRepo",
57
+ idColumn: "id"
58
+ })
59
+
60
+ const drainRepo = yield* Model.makeRepository(Drain, {
61
+ tableName: "queue",
62
+ spanPrefix: "DrainRepo",
63
+ idColumn: "id"
64
+ })
65
+
66
+ const decodeDrain = S.decode(Drain)
67
+
68
+ const drain = () => {
69
+ const limit = subMinutes(new Date(), 15)
70
+ return sql<typeof Drain.Encoded>`SELECT *
71
+ FROM queue
72
+ WHERE name = ${queueDrainName} AND finishedAt IS NULL AND (processingAt IS NULL OR processingAt < ${limit.getTime()})
73
+ LIMIT 1`
74
+ }
75
+
76
+ // temporary workaround until we have a SQLite rowversion..
77
+ const lock = yield* Effect.makeSemaphore(1)
78
+
79
+ const q = {
80
+ offer: (body: Evt, meta: typeof QueueMeta.Type) =>
81
+ Effect.gen(function*() {
82
+ yield* queueRepo.insert(
83
+ Queue.insert.make({
84
+ body,
85
+ meta,
86
+ name: queueName,
87
+ processingAt: Option.none(),
88
+ finishedAt: Option.none()
89
+ })
90
+ )
91
+ }),
92
+ take: Effect.gen(function*() {
93
+ while (true) {
94
+ const first = yield* lock.withPermits(1)(Effect.gen(function*() {
95
+ const [first] = yield* drain()
96
+ if (first) {
97
+ const dec = yield* decodeDrain(first)
98
+ const { createdAt, updatedAt, ...rest } = dec
99
+ yield* drainRepo.update(Drain.update.make({ ...rest, processingAt: Option.some(new Date()) }))
100
+ return dec
101
+ }
102
+ return null
103
+ }))
104
+ if (first) return first
105
+ yield* Effect.sleep(250)
106
+ }
107
+ }),
108
+ finish: ({ createdAt, updatedAt, ...q }: Drain) =>
109
+ drainRepo.update(Drain.update.make({ ...q, finishedAt: Option.some(new Date()) }))
110
+ }
111
+ const rcc = yield* RequestContextContainer
112
+
113
+ return {
114
+ publish: (...messages) =>
115
+ Effect
116
+ .gen(function*($) {
117
+ const requestContext = yield* $(rcc.requestContext)
118
+ const span = yield* $(Effect.serviceOption(Tracer.ParentSpan))
119
+ return yield* $(
120
+ Effect
121
+ .forEach(
122
+ messages,
123
+ (m) =>
124
+ q.offer(m, {
125
+ requestContext: new RequestContext(requestContext), // workaround Schema expecting exact class
126
+ span: Option.getOrUndefined(span)
127
+ }),
128
+ {
129
+ discard: true
130
+ }
131
+ )
132
+ )
133
+ })
134
+ .pipe(
135
+ Effect.withSpan("queue.publish: " + queueName, {
136
+ captureStackTrace: false,
137
+ kind: "producer",
138
+ attributes: { "message_tags": messages.map((_) => _._tag) }
139
+ })
140
+ ),
141
+ drain: <DrainE, DrainR>(
142
+ handleEvent: (ks: DrainEvt) => Effect<void, DrainE, DrainR>,
143
+ sessionId?: string
144
+ ) =>
145
+ Effect.gen(function*() {
146
+ const silenceAndReportError = reportNonInterruptedFailure({ name: "MemQueue.drain." + queueDrainName })
147
+ const processMessage = (msg: Drain) =>
148
+ Effect
149
+ .succeed(msg)
150
+ .pipe(Effect
151
+ .flatMap(({ body, meta }) => {
152
+ let effect = Effect
153
+ .logDebug(`[${queueDrainName}] Processing incoming message`)
154
+ .pipe(
155
+ Effect.annotateLogs({ body: pretty(body), meta: pretty(meta) }),
156
+ Effect.zipRight(handleEvent(body)),
157
+ silenceAndReportError,
158
+ (_) =>
159
+ setupRequestContext(
160
+ _,
161
+ RequestContext.inherit(meta.requestContext, {
162
+ id: RequestId(body.id),
163
+ locale: "en" as const,
164
+ name: NonEmptyString255(`${queueDrainName}.${body._tag}`)
165
+ })
166
+ ),
167
+ Effect
168
+ .withSpan(`queue.drain: ${queueDrainName}.${body._tag}`, {
169
+ captureStackTrace: false,
170
+ kind: "consumer",
171
+ attributes: {
172
+ "queue.name": queueDrainName,
173
+ "queue.sessionId": sessionId,
174
+ "queue.input": body
175
+ }
176
+ })
177
+ )
178
+ if (meta.span) {
179
+ effect = Effect.withParentSpan(effect, Tracer.externalSpan(meta.span))
180
+ }
181
+ return effect
182
+ }))
183
+
184
+ return yield* q
185
+ .take
186
+ .pipe(
187
+ Effect.flatMap((x) =>
188
+ processMessage(x).pipe(
189
+ Effect.uninterruptible,
190
+ Effect.fork,
191
+ Effect.flatMap(Fiber.join),
192
+ Effect.tap(q.finish(x))
193
+ )
194
+ ),
195
+ silenceAndReportError,
196
+ Effect.forever
197
+ )
198
+ })
199
+ } satisfies QueueBase<Evt, DrainEvt>
200
+ })
201
+ }
@@ -1,37 +0,0 @@
1
- // packages/infra/vitest.config.ts
2
- import { defineConfig } from "file:///Users/patrickroza/pj/effect-app/libs/node_modules/.pnpm/vite@5.2.6_@types+node@20.11.30/node_modules/vite/dist/node/index.js";
3
-
4
- // vite.config.base.ts
5
- import path from "path";
6
- import fs from "fs";
7
- var __vite_injected_original_dirname = "/Users/patrickroza/pj/effect-app/libs";
8
- function makeConfig(dirName) {
9
- const prefix = path.resolve(__vite_injected_original_dirname, "packages");
10
- const packages = fs.readdirSync(prefix).map((f) => prefix + "/" + f).filter((f) => fs.lstatSync(f).isDirectory());
11
- const cfg = {
12
- // eslint-disable-next-line @typescript-eslint/no-var-requires
13
- //plugins: [autoImport],
14
- test: {
15
- include: ["./test/**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
16
- reporters: "verbose",
17
- globals: true
18
- },
19
- resolve: {
20
- alias: packages.reduce((acc, cur) => {
21
- acc[JSON.parse(fs.readFileSync(cur + "/package.json", "utf-8")).name] = path.resolve(cur, cur.endsWith("core") ? "dist" : "src");
22
- return acc;
23
- }, {})
24
- // "@effect-app/core/Prelude": path.join(__dirname, "packages/core/src/Prelude.code.ts")
25
- }
26
- };
27
- console.log(cfg);
28
- return cfg;
29
- }
30
-
31
- // packages/infra/vitest.config.ts
32
- var __vite_injected_original_dirname2 = "/Users/patrickroza/pj/effect-app/libs/packages/infra";
33
- var vitest_config_default = defineConfig(makeConfig(__vite_injected_original_dirname2));
34
- export {
35
- vitest_config_default as default
36
- };
37
- //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsicGFja2FnZXMvaW5mcmEvdml0ZXN0LmNvbmZpZy50cyIsICJ2aXRlLmNvbmZpZy5iYXNlLnRzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3BhdHJpY2tyb3phL3BqL2VmZmVjdC1hcHAvbGlicy9wYWNrYWdlcy9pbmZyYVwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3BhdHJpY2tyb3phL3BqL2VmZmVjdC1hcHAvbGlicy9wYWNrYWdlcy9pbmZyYS92aXRlc3QuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9wYXRyaWNrcm96YS9wai9lZmZlY3QtYXBwL2xpYnMvcGFja2FnZXMvaW5mcmEvdml0ZXN0LmNvbmZpZy50c1wiOy8vLyA8cmVmZXJlbmNlIHR5cGVzPVwidml0ZXN0XCIgLz5cbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCJcbmltcG9ydCBtYWtlQ29uZmlnIGZyb20gXCIuLi8uLi92aXRlLmNvbmZpZy5iYXNlXCJcblxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKG1ha2VDb25maWcoX19kaXJuYW1lKSlcbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3BhdHJpY2tyb3phL3BqL2VmZmVjdC1hcHAvbGlic1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3BhdHJpY2tyb3phL3BqL2VmZmVjdC1hcHAvbGlicy92aXRlLmNvbmZpZy5iYXNlLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9wYXRyaWNrcm96YS9wai9lZmZlY3QtYXBwL2xpYnMvdml0ZS5jb25maWcuYmFzZS50c1wiOy8vLyA8cmVmZXJlbmNlIHR5cGVzPVwidml0ZXN0XCIgLz5cbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCJcbmltcG9ydCBmcyBmcm9tIFwiZnNcIlxuaW1wb3J0IEF1dG9JbXBvcnQgZnJvbSBcInVucGx1Z2luLWF1dG8taW1wb3J0L3ZpdGVcIlxuaW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSBcInZpdGVzdC9jb25maWdcIlxuXG4vLyBjb25zdCBhdXRvSW1wb3J0ID0gQXV0b0ltcG9ydCh7XG4vLyAgIGR0czogXCIuL3Rlc3QvYXV0by1pbXBvcnRzLmQudHNcIixcbi8vICAgLy8gaW5jbHVkZTogW1xuLy8gICAvLyAgIC9cXC50ZXN0XFwuW3RqXXN4PyQvIC8vIC50cywgLnRzeCwgLmpzLCAuanN4XG4vLyAgIC8vIF0sXG4vLyAgIGltcG9ydHM6IFtcbi8vICAgICBcInZpdGVzdFwiXG4vLyAgIF1cbi8vIH0pXG5cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIG1ha2VDb25maWcoZGlyTmFtZT86IHN0cmluZykge1xuICBjb25zdCBwcmVmaXggPSBwYXRoLnJlc29sdmUoX19kaXJuYW1lLCBcInBhY2thZ2VzXCIpXG4gIGNvbnN0IHBhY2thZ2VzID0gZnMucmVhZGRpclN5bmMocHJlZml4KS5tYXAoZiA9PiBwcmVmaXggKyBcIi9cIiArIGYpLmZpbHRlcihmID0+IGZzLmxzdGF0U3luYyhmKS5pc0RpcmVjdG9yeSgpIClcbiAgY29uc3QgY2ZnID0ge1xuICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBAdHlwZXNjcmlwdC1lc2xpbnQvbm8tdmFyLXJlcXVpcmVzXG4gICAgLy9wbHVnaW5zOiBbYXV0b0ltcG9ydF0sXG4gICAgdGVzdDoge1xuICAgICAgaW5jbHVkZTogIFtcIi4vdGVzdC8qKi8qLnRlc3Que2pzLG1qcyxjanMsdHMsbXRzLGN0cyxqc3gsdHN4fVwiXSxcbiAgICAgIHJlcG9ydGVyczogXCJ2ZXJib3NlXCIsXG4gICAgICBnbG9iYWxzOiB0cnVlXG4gICAgfSxcbiAgICByZXNvbHZlOiB7XG4gICAgICBhbGlhczogcGFja2FnZXMucmVkdWNlKChhY2MsIGN1cikgPT4geyAvLyB3b3JrYXJvdW5kIGZvciAvUHJlbHVkZSBpc3N1ZVxuICAgICAgYWNjW0pTT04ucGFyc2UoZnMucmVhZEZpbGVTeW5jKGN1ciArIFwiL3BhY2thZ2UuanNvblwiLCBcInV0Zi04XCIpKS5uYW1lXSA9IHBhdGgucmVzb2x2ZShjdXIsIGN1ci5lbmRzV2l0aChcImNvcmVcIikgPyBcImRpc3RcIiA6IFwic3JjXCIpXG4gICAgICByZXR1cm4gYWNjXG4gICAgfSwgeyB9KSAvLyBcIkBlZmZlY3QtYXBwL2NvcmUvUHJlbHVkZVwiOiBwYXRoLmpvaW4oX19kaXJuYW1lLCBcInBhY2thZ2VzL2NvcmUvc3JjL1ByZWx1ZGUuY29kZS50c1wiKVxuICB9XG4gIH1cbiAgY29uc29sZS5sb2coY2ZnKVxuICByZXR1cm4gY2ZnXG59XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQ0EsU0FBUyxvQkFBb0I7OztBQ0E3QixPQUFPLFVBQVU7QUFDakIsT0FBTyxRQUFRO0FBRmYsSUFBTSxtQ0FBbUM7QUFnQjFCLFNBQVIsV0FBNEIsU0FBa0I7QUFDbkQsUUFBTSxTQUFTLEtBQUssUUFBUSxrQ0FBVyxVQUFVO0FBQ2pELFFBQU0sV0FBVyxHQUFHLFlBQVksTUFBTSxFQUFFLElBQUksT0FBSyxTQUFTLE1BQU0sQ0FBQyxFQUFFLE9BQU8sT0FBSyxHQUFHLFVBQVUsQ0FBQyxFQUFFLFlBQVksQ0FBRTtBQUM3RyxRQUFNLE1BQU07QUFBQTtBQUFBO0FBQUEsSUFHVixNQUFNO0FBQUEsTUFDSixTQUFVLENBQUMsa0RBQWtEO0FBQUEsTUFDN0QsV0FBVztBQUFBLE1BQ1gsU0FBUztBQUFBLElBQ1g7QUFBQSxJQUNBLFNBQVM7QUFBQSxNQUNQLE9BQU8sU0FBUyxPQUFPLENBQUMsS0FBSyxRQUFRO0FBQ3JDLFlBQUksS0FBSyxNQUFNLEdBQUcsYUFBYSxNQUFNLGlCQUFpQixPQUFPLENBQUMsRUFBRSxJQUFJLElBQUksS0FBSyxRQUFRLEtBQUssSUFBSSxTQUFTLE1BQU0sSUFBSSxTQUFTLEtBQUs7QUFDL0gsZUFBTztBQUFBLE1BQ1QsR0FBRyxDQUFFLENBQUM7QUFBQTtBQUFBLElBQ1I7QUFBQSxFQUNBO0FBQ0EsVUFBUSxJQUFJLEdBQUc7QUFDZixTQUFPO0FBQ1Q7OztBRHBDQSxJQUFNQSxvQ0FBbUM7QUFJekMsSUFBTyx3QkFBUSxhQUFhLFdBQVdDLGlDQUFTLENBQUM7IiwKICAibmFtZXMiOiBbIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIl0KfQo=