@l-etabli/events 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -0
- package/dist/adapters/effect-kysely/EffectKyselyEventQueries.cjs +54 -0
- package/dist/adapters/effect-kysely/EffectKyselyEventQueries.cjs.map +1 -0
- package/dist/adapters/effect-kysely/EffectKyselyEventQueries.d.cts +10 -0
- package/dist/adapters/effect-kysely/EffectKyselyEventQueries.d.ts +10 -0
- package/dist/adapters/effect-kysely/EffectKyselyEventQueries.mjs +30 -0
- package/dist/adapters/effect-kysely/EffectKyselyEventQueries.mjs.map +1 -0
- package/dist/adapters/effect-kysely/EffectKyselyEventRepository.cjs +85 -0
- package/dist/adapters/effect-kysely/EffectKyselyEventRepository.cjs.map +1 -0
- package/dist/adapters/effect-kysely/EffectKyselyEventRepository.d.cts +9 -0
- package/dist/adapters/effect-kysely/EffectKyselyEventRepository.d.ts +9 -0
- package/dist/adapters/effect-kysely/EffectKyselyEventRepository.mjs +61 -0
- package/dist/adapters/effect-kysely/EffectKyselyEventRepository.mjs.map +1 -0
- package/dist/adapters/effect-kysely/index.cjs +32 -0
- package/dist/adapters/effect-kysely/index.cjs.map +1 -0
- package/dist/adapters/effect-kysely/index.d.cts +9 -0
- package/dist/adapters/effect-kysely/index.d.ts +9 -0
- package/dist/adapters/effect-kysely/index.mjs +7 -0
- package/dist/adapters/effect-kysely/index.mjs.map +1 -0
- package/dist/adapters/in-memory/InMemoryEventBus.cjs +3 -17
- package/dist/adapters/in-memory/InMemoryEventBus.cjs.map +1 -1
- package/dist/adapters/in-memory/InMemoryEventBus.mjs +2 -16
- package/dist/adapters/in-memory/InMemoryEventBus.mjs.map +1 -1
- package/dist/adapters/in-memory/InMemoryEventQueries.cjs +2 -20
- package/dist/adapters/in-memory/InMemoryEventQueries.cjs.map +1 -1
- package/dist/adapters/in-memory/InMemoryEventQueries.mjs +2 -20
- package/dist/adapters/in-memory/InMemoryEventQueries.mjs.map +1 -1
- package/dist/adapters/kysely/KyselyEventQueries.cjs +20 -24
- package/dist/adapters/kysely/KyselyEventQueries.cjs.map +1 -1
- package/dist/adapters/kysely/KyselyEventQueries.d.cts +1 -1
- package/dist/adapters/kysely/KyselyEventQueries.d.ts +1 -1
- package/dist/adapters/kysely/KyselyEventQueries.mjs +20 -24
- package/dist/adapters/kysely/KyselyEventQueries.mjs.map +1 -1
- package/dist/adapters/kysely/KyselyEventRepository.cjs +47 -45
- package/dist/adapters/kysely/KyselyEventRepository.cjs.map +1 -1
- package/dist/adapters/kysely/KyselyEventRepository.d.cts +1 -1
- package/dist/adapters/kysely/KyselyEventRepository.d.ts +1 -1
- package/dist/adapters/kysely/KyselyEventRepository.mjs +43 -41
- package/dist/adapters/kysely/KyselyEventRepository.mjs.map +1 -1
- package/dist/adapters/kysely/jsonb.cjs +30 -0
- package/dist/adapters/kysely/jsonb.cjs.map +1 -0
- package/dist/adapters/kysely/jsonb.d.cts +5 -0
- package/dist/adapters/kysely/jsonb.d.ts +5 -0
- package/dist/adapters/kysely/jsonb.mjs +6 -0
- package/dist/adapters/kysely/jsonb.mjs.map +1 -0
- package/dist/adapters/kysely/mapEventRow.cjs +35 -0
- package/dist/adapters/kysely/mapEventRow.cjs.map +1 -0
- package/dist/adapters/kysely/mapEventRow.d.cts +6 -0
- package/dist/adapters/kysely/mapEventRow.d.ts +6 -0
- package/dist/adapters/kysely/mapEventRow.mjs +11 -0
- package/dist/adapters/kysely/mapEventRow.mjs.map +1 -0
- package/dist/createEventCrawler.cjs +2 -8
- package/dist/createEventCrawler.cjs.map +1 -1
- package/dist/createEventCrawler.mjs +1 -7
- package/dist/createEventCrawler.mjs.map +1 -1
- package/dist/effect/EffectEventCrawler.cjs +111 -0
- package/dist/effect/EffectEventCrawler.cjs.map +1 -0
- package/dist/effect/EffectEventCrawler.d.cts +26 -0
- package/dist/effect/EffectEventCrawler.d.ts +26 -0
- package/dist/effect/EffectEventCrawler.mjs +87 -0
- package/dist/effect/EffectEventCrawler.mjs.map +1 -0
- package/dist/effect/EffectInMemoryEventBus.cjs +131 -0
- package/dist/effect/EffectInMemoryEventBus.cjs.map +1 -0
- package/dist/effect/EffectInMemoryEventBus.d.cts +31 -0
- package/dist/effect/EffectInMemoryEventBus.d.ts +31 -0
- package/dist/effect/EffectInMemoryEventBus.mjs +112 -0
- package/dist/effect/EffectInMemoryEventBus.mjs.map +1 -0
- package/dist/effect/EffectInMemoryEventQueries.cjs +35 -0
- package/dist/effect/EffectInMemoryEventQueries.cjs.map +1 -0
- package/dist/effect/EffectInMemoryEventQueries.d.cts +12 -0
- package/dist/effect/EffectInMemoryEventQueries.d.ts +12 -0
- package/dist/effect/EffectInMemoryEventQueries.mjs +11 -0
- package/dist/effect/EffectInMemoryEventQueries.mjs.map +1 -0
- package/dist/effect/EffectInMemoryEventRepository.cjs +73 -0
- package/dist/effect/EffectInMemoryEventRepository.cjs.map +1 -0
- package/dist/effect/EffectInMemoryEventRepository.d.cts +15 -0
- package/dist/effect/EffectInMemoryEventRepository.d.ts +15 -0
- package/dist/effect/EffectInMemoryEventRepository.mjs +48 -0
- package/dist/effect/EffectInMemoryEventRepository.mjs.map +1 -0
- package/dist/effect/EffectSubscriptions.cjs +61 -0
- package/dist/effect/EffectSubscriptions.cjs.map +1 -0
- package/dist/effect/EffectSubscriptions.d.cts +22 -0
- package/dist/effect/EffectSubscriptions.d.ts +22 -0
- package/dist/effect/EffectSubscriptions.mjs +36 -0
- package/dist/effect/EffectSubscriptions.mjs.map +1 -0
- package/dist/effect/index.cjs +47 -0
- package/dist/effect/index.cjs.map +1 -0
- package/dist/effect/index.d.cts +27 -0
- package/dist/effect/index.d.ts +27 -0
- package/dist/effect/index.mjs +20 -0
- package/dist/effect/index.mjs.map +1 -0
- package/dist/effect/ports/EffectEventBus.cjs +17 -0
- package/dist/effect/ports/EffectEventBus.cjs.map +1 -0
- package/dist/effect/ports/EffectEventBus.d.cts +13 -0
- package/dist/effect/ports/EffectEventBus.d.ts +13 -0
- package/dist/effect/ports/EffectEventBus.mjs +1 -0
- package/dist/effect/ports/EffectEventBus.mjs.map +1 -0
- package/dist/effect/ports/EffectEventQueries.cjs +17 -0
- package/dist/effect/ports/EffectEventQueries.cjs.map +1 -0
- package/dist/effect/ports/EffectEventQueries.d.cts +9 -0
- package/dist/effect/ports/EffectEventQueries.d.ts +9 -0
- package/dist/effect/ports/EffectEventQueries.mjs +1 -0
- package/dist/effect/ports/EffectEventQueries.mjs.map +1 -0
- package/dist/effect/ports/EffectEventRepository.cjs +17 -0
- package/dist/effect/ports/EffectEventRepository.cjs.map +1 -0
- package/dist/effect/ports/EffectEventRepository.d.cts +17 -0
- package/dist/effect/ports/EffectEventRepository.d.ts +17 -0
- package/dist/effect/ports/EffectEventRepository.mjs +1 -0
- package/dist/effect/ports/EffectEventRepository.mjs.map +1 -0
- package/dist/filterEvents.cjs +48 -0
- package/dist/filterEvents.cjs.map +1 -0
- package/dist/filterEvents.d.cts +6 -0
- package/dist/filterEvents.d.ts +6 -0
- package/dist/filterEvents.mjs +24 -0
- package/dist/filterEvents.mjs.map +1 -0
- package/dist/getSubscriptionIdsToPublish.cjs +40 -0
- package/dist/getSubscriptionIdsToPublish.cjs.map +1 -0
- package/dist/getSubscriptionIdsToPublish.d.cts +5 -0
- package/dist/getSubscriptionIdsToPublish.d.ts +5 -0
- package/dist/getSubscriptionIdsToPublish.mjs +16 -0
- package/dist/getSubscriptionIdsToPublish.mjs.map +1 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/ports/EventQueries.cjs.map +1 -1
- package/dist/ports/EventQueries.d.cts +1 -1
- package/dist/ports/EventQueries.d.ts +1 -1
- package/dist/splitIntoChunks.cjs +35 -0
- package/dist/splitIntoChunks.cjs.map +1 -0
- package/dist/splitIntoChunks.d.cts +3 -0
- package/dist/splitIntoChunks.d.ts +3 -0
- package/dist/splitIntoChunks.mjs +11 -0
- package/dist/splitIntoChunks.mjs.map +1 -0
- package/package.json +18 -3
- package/src/adapters/effect-kysely/EffectKyselyEventQueries.ts +45 -0
- package/src/adapters/effect-kysely/EffectKyselyEventRepository.ts +90 -0
- package/src/adapters/effect-kysely/index.ts +3 -0
- package/src/adapters/in-memory/InMemoryEventBus.ts +2 -23
- package/src/adapters/in-memory/InMemoryEventQueries.ts +2 -32
- package/src/adapters/kysely/KyselyEventQueries.ts +27 -31
- package/src/adapters/kysely/KyselyEventRepository.ts +66 -64
- package/src/adapters/kysely/jsonb.ts +4 -0
- package/src/adapters/kysely/mapEventRow.ts +15 -0
- package/src/createEventCrawler.ts +1 -8
- package/src/effect/EffectEventCrawler.ts +124 -0
- package/src/effect/EffectInMemoryEventBus.ts +231 -0
- package/src/effect/EffectInMemoryEventQueries.ts +16 -0
- package/src/effect/EffectInMemoryEventRepository.ts +68 -0
- package/src/effect/EffectSubscriptions.ts +74 -0
- package/src/effect/index.ts +26 -0
- package/src/effect/ports/EffectEventBus.ts +17 -0
- package/src/effect/ports/EffectEventQueries.ts +9 -0
- package/src/effect/ports/EffectEventRepository.ts +27 -0
- package/src/filterEvents.ts +39 -0
- package/src/getSubscriptionIdsToPublish.ts +21 -0
- package/src/ports/EventQueries.ts +1 -1
- package/src/splitIntoChunks.ts +7 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "The purpose of this repository is to make it easy to setup event driven architecture using outbox pattern",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.7.0",
|
|
7
7
|
"main": "./dist/index.mjs",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"files": [
|
|
@@ -39,10 +39,11 @@
|
|
|
39
39
|
"@commitlint/config-conventional": "^20.0.0",
|
|
40
40
|
"@types/bun": "latest",
|
|
41
41
|
"@types/pg": "^8.16.0",
|
|
42
|
+
"effect": "4.0.0-beta.43",
|
|
42
43
|
"kysely": "^0.28.2",
|
|
43
44
|
"lefthook": "^1.13.6",
|
|
44
45
|
"pg": "^8.16.3",
|
|
45
|
-
"semantic-release": "^
|
|
46
|
+
"semantic-release": "^25.0.3",
|
|
46
47
|
"tsup": "^8.5.0",
|
|
47
48
|
"typescript": "^5.9.3"
|
|
48
49
|
},
|
|
@@ -56,14 +57,28 @@
|
|
|
56
57
|
"types": "./dist/adapters/kysely/index.d.ts",
|
|
57
58
|
"import": "./dist/adapters/kysely/index.mjs",
|
|
58
59
|
"require": "./dist/adapters/kysely/index.cjs"
|
|
60
|
+
},
|
|
61
|
+
"./effect": {
|
|
62
|
+
"types": "./dist/effect/index.d.ts",
|
|
63
|
+
"import": "./dist/effect/index.mjs",
|
|
64
|
+
"require": "./dist/effect/index.cjs"
|
|
65
|
+
},
|
|
66
|
+
"./effect-kysely": {
|
|
67
|
+
"types": "./dist/adapters/effect-kysely/index.d.ts",
|
|
68
|
+
"import": "./dist/adapters/effect-kysely/index.mjs",
|
|
69
|
+
"require": "./dist/adapters/effect-kysely/index.cjs"
|
|
59
70
|
}
|
|
60
71
|
},
|
|
61
72
|
"peerDependencies": {
|
|
62
|
-
"kysely": "
|
|
73
|
+
"kysely": ">=0.28.0",
|
|
74
|
+
"effect": ">=4.0.0-beta.0"
|
|
63
75
|
},
|
|
64
76
|
"peerDependenciesMeta": {
|
|
65
77
|
"kysely": {
|
|
66
78
|
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"effect": {
|
|
81
|
+
"optional": true
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { Kysely, SqlBool } from "kysely";
|
|
3
|
+
import { sql } from "kysely";
|
|
4
|
+
import type { EventQueries } from "../../effect/ports/EffectEventQueries.ts";
|
|
5
|
+
import type { DefaultContext, GenericEvent } from "../../types.ts";
|
|
6
|
+
import { mapEventRow } from "../kysely/mapEventRow.ts";
|
|
7
|
+
import type { EventsTable } from "../kysely/types.ts";
|
|
8
|
+
|
|
9
|
+
export const createEffectKyselyEventQueries = <
|
|
10
|
+
Event extends GenericEvent<string, unknown, DefaultContext>,
|
|
11
|
+
DB extends EventsTable = EventsTable,
|
|
12
|
+
>(
|
|
13
|
+
db: Kysely<DB>,
|
|
14
|
+
): EventQueries<Event> => {
|
|
15
|
+
const eventsDb = db as unknown as Kysely<EventsTable>;
|
|
16
|
+
return {
|
|
17
|
+
getEvents: ({ filters, limit }) =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
let query = eventsDb
|
|
20
|
+
.selectFrom("events")
|
|
21
|
+
.selectAll()
|
|
22
|
+
.where("status", "in", filters.statuses)
|
|
23
|
+
.limit(limit);
|
|
24
|
+
|
|
25
|
+
if (filters.context) {
|
|
26
|
+
for (const [key, value] of Object.entries(filters.context)) {
|
|
27
|
+
query = query.where(sql<SqlBool>`context->>${key} = ${value}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (filters.occurredAt?.from) {
|
|
32
|
+
query = query.where("occurredAt", ">=", filters.occurredAt.from);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (filters.occurredAt?.to) {
|
|
36
|
+
query = query.where("occurredAt", "<=", filters.occurredAt.to);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rows = yield* Effect.promise(() => query.execute());
|
|
40
|
+
return rows.map((row: EventsTable["events"]) =>
|
|
41
|
+
mapEventRow<Event>(row),
|
|
42
|
+
);
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { Kysely } from "kysely";
|
|
3
|
+
import type { EventRepository } from "../../effect/ports/EffectEventRepository.ts";
|
|
4
|
+
import type { DefaultContext, GenericEvent } from "../../types.ts";
|
|
5
|
+
import { jsonb } from "../kysely/jsonb.ts";
|
|
6
|
+
import type { EventsTable } from "../kysely/types.ts";
|
|
7
|
+
|
|
8
|
+
export const createEffectKyselyEventRepository = <
|
|
9
|
+
Event extends GenericEvent<string, unknown, DefaultContext>,
|
|
10
|
+
DB extends EventsTable = EventsTable,
|
|
11
|
+
>(
|
|
12
|
+
db: Kysely<DB>,
|
|
13
|
+
): EventRepository<Event> => {
|
|
14
|
+
const eventsDb = db as unknown as Kysely<EventsTable>;
|
|
15
|
+
return {
|
|
16
|
+
save: (event) =>
|
|
17
|
+
Effect.promise(() =>
|
|
18
|
+
eventsDb
|
|
19
|
+
.insertInto("events")
|
|
20
|
+
.values({
|
|
21
|
+
...event,
|
|
22
|
+
payload: jsonb(event.payload),
|
|
23
|
+
triggeredByActor: jsonb(event.triggeredByActor),
|
|
24
|
+
context: jsonb(event.context),
|
|
25
|
+
publications: jsonb(event.publications),
|
|
26
|
+
})
|
|
27
|
+
.onConflict((oc) =>
|
|
28
|
+
oc.column("id").doUpdateSet({
|
|
29
|
+
topic: event.topic,
|
|
30
|
+
payload: jsonb(event.payload),
|
|
31
|
+
triggeredByActor: jsonb(event.triggeredByActor),
|
|
32
|
+
context: jsonb(event.context),
|
|
33
|
+
status: event.status,
|
|
34
|
+
flowId: event.flowId,
|
|
35
|
+
causedByEventId: event.causedByEventId,
|
|
36
|
+
occurredAt: event.occurredAt,
|
|
37
|
+
publications: jsonb(event.publications),
|
|
38
|
+
priority: event.priority,
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
.execute(),
|
|
42
|
+
).pipe(Effect.asVoid),
|
|
43
|
+
|
|
44
|
+
saveNewEventsBatch: (events) => {
|
|
45
|
+
if (events.length === 0) return Effect.void;
|
|
46
|
+
return Effect.promise(() =>
|
|
47
|
+
eventsDb
|
|
48
|
+
.insertInto("events")
|
|
49
|
+
.values(
|
|
50
|
+
events.map((event) => ({
|
|
51
|
+
...event,
|
|
52
|
+
payload: jsonb(event.payload),
|
|
53
|
+
triggeredByActor: jsonb(event.triggeredByActor),
|
|
54
|
+
context: jsonb(event.context),
|
|
55
|
+
publications: jsonb(event.publications),
|
|
56
|
+
})),
|
|
57
|
+
)
|
|
58
|
+
.execute(),
|
|
59
|
+
).pipe(Effect.asVoid);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
markEventsAsInProcess: (events) => {
|
|
63
|
+
if (events.length === 0) return Effect.void;
|
|
64
|
+
const ids = events.map((e) => e.id);
|
|
65
|
+
|
|
66
|
+
return Effect.gen(function* () {
|
|
67
|
+
const lockedRows = yield* Effect.promise(() =>
|
|
68
|
+
eventsDb
|
|
69
|
+
.selectFrom("events")
|
|
70
|
+
.select("id")
|
|
71
|
+
.where("id", "in", ids)
|
|
72
|
+
.forUpdate()
|
|
73
|
+
.skipLocked()
|
|
74
|
+
.execute(),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (lockedRows.length === 0) return;
|
|
78
|
+
const lockedIds = lockedRows.map((r) => r.id);
|
|
79
|
+
|
|
80
|
+
yield* Effect.promise(() =>
|
|
81
|
+
eventsDb
|
|
82
|
+
.updateTable("events")
|
|
83
|
+
.set({ status: "in-process" })
|
|
84
|
+
.where("id", "in", lockedIds)
|
|
85
|
+
.execute(),
|
|
86
|
+
).pipe(Effect.asVoid);
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
};
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
EventDefinitions,
|
|
8
8
|
InferEventsFromDefinitions,
|
|
9
9
|
} from "../../eventDefinitions.ts";
|
|
10
|
+
import { getSubscriptionIdsToPublish } from "../../getSubscriptionIdsToPublish.ts";
|
|
10
11
|
import type { EventBus } from "../../ports/EventBus.ts";
|
|
11
12
|
import type { WithEventsUow } from "../../ports/EventRepository.ts";
|
|
12
13
|
import {
|
|
@@ -113,28 +114,6 @@ export function createInMemoryEventBus<
|
|
|
113
114
|
}
|
|
114
115
|
};
|
|
115
116
|
|
|
116
|
-
const getSubscriptionIdsToPublish = (
|
|
117
|
-
event: Event,
|
|
118
|
-
callbacksBySubscriptionId: SubscriptionsForTopic,
|
|
119
|
-
): string[] => {
|
|
120
|
-
const allSubscriptionIds = Object.keys(callbacksBySubscriptionId);
|
|
121
|
-
|
|
122
|
-
if (event.publications.length === 0 || event.status === "to-republish") {
|
|
123
|
-
return allSubscriptionIds;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const lastPublication = event.publications.reduce((latest, current) =>
|
|
127
|
-
current.publishedAt > latest.publishedAt ? current : latest,
|
|
128
|
-
);
|
|
129
|
-
const failedSubscriptionIds = (lastPublication.failures ?? []).map(
|
|
130
|
-
(failure) => failure.subscriptionId,
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
return allSubscriptionIds.filter((id) =>
|
|
134
|
-
failedSubscriptionIds.includes(id),
|
|
135
|
-
);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
117
|
const eventBus: EventBus<Event> = {
|
|
139
118
|
publish: async (event) => {
|
|
140
119
|
const publishedAt = new Date();
|
|
@@ -156,7 +135,7 @@ export function createInMemoryEventBus<
|
|
|
156
135
|
|
|
157
136
|
const subscriptionIdsToPublish = getSubscriptionIdsToPublish(
|
|
158
137
|
event,
|
|
159
|
-
callbacksBySubscriptionSlug,
|
|
138
|
+
Object.keys(callbacksBySubscriptionSlug),
|
|
160
139
|
);
|
|
161
140
|
|
|
162
141
|
const failuresOrUndefined = await Promise.all(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { filterEvents } from "../../filterEvents.ts";
|
|
1
2
|
import type { EventQueries } from "../../ports/EventQueries.ts";
|
|
2
3
|
import type { DefaultContext, GenericEvent } from "../../types.ts";
|
|
3
4
|
import type { InMemoryEventRepositoryHelpers } from "./InMemoryEventRepository.ts";
|
|
@@ -8,37 +9,6 @@ export const createInMemoryEventQueries = <
|
|
|
8
9
|
helpers: InMemoryEventRepositoryHelpers<Event>,
|
|
9
10
|
): { eventQueries: EventQueries<Event> } => ({
|
|
10
11
|
eventQueries: {
|
|
11
|
-
getEvents: async (
|
|
12
|
-
const matchesContext = (event: Event): boolean => {
|
|
13
|
-
if (!filters.context) return true;
|
|
14
|
-
if (!event.context) return false;
|
|
15
|
-
|
|
16
|
-
return Object.entries(filters.context).every(
|
|
17
|
-
([key, value]) => event.context?.[key] === value,
|
|
18
|
-
);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const matchesOccurredAt = (event: Event): boolean => {
|
|
22
|
-
if (!filters.occurredAt) return true;
|
|
23
|
-
|
|
24
|
-
const { from, to } = filters.occurredAt;
|
|
25
|
-
const eventTime = event.occurredAt.getTime();
|
|
26
|
-
|
|
27
|
-
if (from && eventTime < from.getTime()) return false;
|
|
28
|
-
if (to && eventTime > to.getTime()) return false;
|
|
29
|
-
|
|
30
|
-
return true;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
return helpers
|
|
34
|
-
.getAllEvents()
|
|
35
|
-
.filter(
|
|
36
|
-
(event) =>
|
|
37
|
-
filters.statuses.includes(event.status) &&
|
|
38
|
-
matchesContext(event) &&
|
|
39
|
-
matchesOccurredAt(event),
|
|
40
|
-
)
|
|
41
|
-
.slice(0, limit);
|
|
42
|
-
},
|
|
12
|
+
getEvents: async (params) => filterEvents(helpers.getAllEvents(), params),
|
|
43
13
|
},
|
|
44
14
|
});
|
|
@@ -2,44 +2,40 @@ import type { Kysely, SqlBool } from "kysely";
|
|
|
2
2
|
import { sql } from "kysely";
|
|
3
3
|
import type { EventQueries } from "../../ports/EventQueries.ts";
|
|
4
4
|
import type { DefaultContext, GenericEvent } from "../../types.ts";
|
|
5
|
+
import { mapEventRow } from "./mapEventRow.ts";
|
|
5
6
|
import type { EventsTable } from "./types.ts";
|
|
6
7
|
|
|
7
8
|
export const createKyselyEventQueries = <
|
|
8
9
|
Event extends GenericEvent<string, unknown, DefaultContext>,
|
|
10
|
+
DB extends EventsTable = EventsTable,
|
|
9
11
|
>(
|
|
10
|
-
db: Kysely<
|
|
11
|
-
): EventQueries<Event> =>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
db: Kysely<DB>,
|
|
13
|
+
): EventQueries<Event> => {
|
|
14
|
+
const eventsDb = db as unknown as Kysely<EventsTable>;
|
|
15
|
+
return {
|
|
16
|
+
getEvents: async ({ filters, limit }) => {
|
|
17
|
+
let query = eventsDb
|
|
18
|
+
.selectFrom("events")
|
|
19
|
+
.selectAll()
|
|
20
|
+
.where("status", "in", filters.statuses)
|
|
21
|
+
.limit(limit);
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
if (filters.context) {
|
|
24
|
+
for (const [key, value] of Object.entries(filters.context)) {
|
|
25
|
+
query = query.where(sql<SqlBool>`context->>${key} = ${value}`);
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
|
-
}
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
if (filters.occurredAt?.from) {
|
|
30
|
+
query = query.where("occurredAt", ">=", filters.occurredAt.from);
|
|
31
|
+
}
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
if (filters.occurredAt?.to) {
|
|
34
|
+
query = query.where("occurredAt", "<=", filters.occurredAt.to);
|
|
35
|
+
}
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
context: row.context ?? undefined,
|
|
39
|
-
flowId: row.flowId ?? undefined,
|
|
40
|
-
causedByEventId: row.causedByEventId ?? undefined,
|
|
41
|
-
priority: row.priority ?? undefined,
|
|
42
|
-
}) as Event,
|
|
43
|
-
);
|
|
44
|
-
},
|
|
45
|
-
});
|
|
37
|
+
const rows = await query.execute();
|
|
38
|
+
return rows.map((row: EventsTable["events"]) => mapEventRow<Event>(row));
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
};
|
|
@@ -1,80 +1,82 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
2
|
import type { EventRepository } from "../../ports/EventRepository.ts";
|
|
3
3
|
import type { DefaultContext, GenericEvent } from "../../types.ts";
|
|
4
|
+
import { jsonb } from "./jsonb.ts";
|
|
4
5
|
import type { EventsTable } from "./types.ts";
|
|
5
6
|
|
|
6
|
-
const jsonb = <T>(value: T): RawBuilder<T> =>
|
|
7
|
-
sql`${JSON.stringify(value)}::jsonb`;
|
|
8
|
-
|
|
9
7
|
export const createKyselyEventRepository = <
|
|
10
8
|
Event extends GenericEvent<string, unknown, DefaultContext>,
|
|
9
|
+
DB extends EventsTable = EventsTable,
|
|
11
10
|
>(
|
|
12
|
-
db: Kysely<
|
|
13
|
-
): EventRepository<Event> =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
triggeredByActor: jsonb(event.triggeredByActor),
|
|
21
|
-
context: jsonb(event.context),
|
|
22
|
-
publications: jsonb(event.publications),
|
|
23
|
-
})
|
|
24
|
-
.onConflict((oc) =>
|
|
25
|
-
oc.column("id").doUpdateSet({
|
|
26
|
-
topic: event.topic,
|
|
27
|
-
payload: jsonb(event.payload),
|
|
28
|
-
triggeredByActor: jsonb(event.triggeredByActor),
|
|
29
|
-
context: jsonb(event.context),
|
|
30
|
-
status: event.status,
|
|
31
|
-
flowId: event.flowId,
|
|
32
|
-
causedByEventId: event.causedByEventId,
|
|
33
|
-
occurredAt: event.occurredAt,
|
|
34
|
-
publications: jsonb(event.publications),
|
|
35
|
-
priority: event.priority,
|
|
36
|
-
}),
|
|
37
|
-
)
|
|
38
|
-
.execute();
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
saveNewEventsBatch: async (events) => {
|
|
42
|
-
if (events.length === 0) return;
|
|
43
|
-
await db
|
|
44
|
-
.insertInto("events")
|
|
45
|
-
.values(
|
|
46
|
-
events.map((event) => ({
|
|
11
|
+
db: Kysely<DB>,
|
|
12
|
+
): EventRepository<Event> => {
|
|
13
|
+
const eventsDb = db as unknown as Kysely<EventsTable>;
|
|
14
|
+
return {
|
|
15
|
+
save: async (event) => {
|
|
16
|
+
await eventsDb
|
|
17
|
+
.insertInto("events")
|
|
18
|
+
.values({
|
|
47
19
|
...event,
|
|
48
20
|
payload: jsonb(event.payload),
|
|
49
21
|
triggeredByActor: jsonb(event.triggeredByActor),
|
|
50
22
|
context: jsonb(event.context),
|
|
51
23
|
publications: jsonb(event.publications),
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
24
|
+
})
|
|
25
|
+
.onConflict((oc) =>
|
|
26
|
+
oc.column("id").doUpdateSet({
|
|
27
|
+
topic: event.topic,
|
|
28
|
+
payload: jsonb(event.payload),
|
|
29
|
+
triggeredByActor: jsonb(event.triggeredByActor),
|
|
30
|
+
context: jsonb(event.context),
|
|
31
|
+
status: event.status,
|
|
32
|
+
flowId: event.flowId,
|
|
33
|
+
causedByEventId: event.causedByEventId,
|
|
34
|
+
occurredAt: event.occurredAt,
|
|
35
|
+
publications: jsonb(event.publications),
|
|
36
|
+
priority: event.priority,
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
.execute();
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
saveNewEventsBatch: async (events) => {
|
|
43
|
+
if (events.length === 0) return;
|
|
44
|
+
await eventsDb
|
|
45
|
+
.insertInto("events")
|
|
46
|
+
.values(
|
|
47
|
+
events.map((event) => ({
|
|
48
|
+
...event,
|
|
49
|
+
payload: jsonb(event.payload),
|
|
50
|
+
triggeredByActor: jsonb(event.triggeredByActor),
|
|
51
|
+
context: jsonb(event.context),
|
|
52
|
+
publications: jsonb(event.publications),
|
|
53
|
+
})),
|
|
54
|
+
)
|
|
55
|
+
.execute();
|
|
56
|
+
},
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
markEventsAsInProcess: async (events) => {
|
|
59
|
+
if (events.length === 0) return;
|
|
60
|
+
const ids = events.map((e) => e.id);
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
// Lock the rows to prevent concurrent processing
|
|
63
|
+
const lockedRows = await eventsDb
|
|
64
|
+
.selectFrom("events")
|
|
65
|
+
.select("id")
|
|
66
|
+
.where("id", "in", ids)
|
|
67
|
+
.forUpdate()
|
|
68
|
+
.skipLocked()
|
|
69
|
+
.execute();
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
if (lockedRows.length === 0) return;
|
|
72
|
+
const lockedIds = lockedRows.map((r) => r.id);
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
74
|
+
// Update status to in-process (only for locked rows)
|
|
75
|
+
await eventsDb
|
|
76
|
+
.updateTable("events")
|
|
77
|
+
.set({ status: "in-process" })
|
|
78
|
+
.where("id", "in", lockedIds)
|
|
79
|
+
.execute();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DefaultContext, GenericEvent } from "../../types.ts";
|
|
2
|
+
import type { EventsTable } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export const mapEventRow = <
|
|
5
|
+
Event extends GenericEvent<string, unknown, DefaultContext>,
|
|
6
|
+
>(
|
|
7
|
+
row: EventsTable["events"],
|
|
8
|
+
): Event =>
|
|
9
|
+
({
|
|
10
|
+
...row,
|
|
11
|
+
context: row.context ?? undefined,
|
|
12
|
+
flowId: row.flowId ?? undefined,
|
|
13
|
+
causedByEventId: row.causedByEventId ?? undefined,
|
|
14
|
+
priority: row.priority ?? undefined,
|
|
15
|
+
}) as Event;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EventBus } from "./ports/EventBus.ts";
|
|
2
2
|
import type { EventQueries } from "./ports/EventQueries.ts";
|
|
3
3
|
import type { WithEventsUow } from "./ports/EventRepository.ts";
|
|
4
|
+
import { splitIntoChunks } from "./splitIntoChunks.ts";
|
|
4
5
|
import type { DefaultContext, GenericEvent } from "./types.ts";
|
|
5
6
|
|
|
6
7
|
/** Configuration options for the event crawler. */
|
|
@@ -15,14 +16,6 @@ type CreateEventCrawlerOptions = {
|
|
|
15
16
|
failedEventsIntervalMs?: number;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
const splitIntoChunks = <T>(array: T[], chunkSize: number): T[][] => {
|
|
19
|
-
const chunks: T[][] = [];
|
|
20
|
-
for (let i = 0; i < array.length; i += chunkSize) {
|
|
21
|
-
chunks.push(array.slice(i, i + chunkSize));
|
|
22
|
-
}
|
|
23
|
-
return chunks;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
19
|
/**
|
|
27
20
|
* Creates a background event crawler that processes and publishes events.
|
|
28
21
|
*
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Cause, Effect, Exit } from "effect";
|
|
2
|
+
import type { DefaultContext, GenericEvent } from "../types.ts";
|
|
3
|
+
import type { EventBus } from "./ports/EffectEventBus.ts";
|
|
4
|
+
import type { EventQueries } from "./ports/EffectEventQueries.ts";
|
|
5
|
+
import type { WithEventsUow } from "./ports/EffectEventRepository.ts";
|
|
6
|
+
|
|
7
|
+
type CreateEffectEventCrawlerOptions = {
|
|
8
|
+
batchSize?: number;
|
|
9
|
+
maxParallelProcessing?: number;
|
|
10
|
+
newEventsIntervalMs?: number;
|
|
11
|
+
failedEventsIntervalMs?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const createEffectEventCrawler = <
|
|
15
|
+
Event extends GenericEvent<string, unknown, DefaultContext>,
|
|
16
|
+
>({
|
|
17
|
+
withUow,
|
|
18
|
+
eventQueries,
|
|
19
|
+
eventBus,
|
|
20
|
+
options = {},
|
|
21
|
+
}: {
|
|
22
|
+
withUow: WithEventsUow<Event>;
|
|
23
|
+
eventQueries: EventQueries<Event>;
|
|
24
|
+
eventBus: EventBus<Event>;
|
|
25
|
+
options?: CreateEffectEventCrawlerOptions;
|
|
26
|
+
}) => {
|
|
27
|
+
const batchSize = options.batchSize ?? 100;
|
|
28
|
+
const maxParallelProcessing = options.maxParallelProcessing ?? 1;
|
|
29
|
+
const newEventsIntervalMs = options.newEventsIntervalMs ?? 10_000;
|
|
30
|
+
const failedEventsIntervalMs = options.failedEventsIntervalMs ?? 60_000;
|
|
31
|
+
|
|
32
|
+
const publishEventsInParallel = (events: Event[]): Effect.Effect<void> =>
|
|
33
|
+
Effect.all(
|
|
34
|
+
events.map((event) => eventBus.publish(event)),
|
|
35
|
+
{ concurrency: maxParallelProcessing, discard: true },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const processNewEvents = (): Effect.Effect<void> =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const events = yield* eventQueries.getEvents({
|
|
41
|
+
filters: { statuses: ["never-published"] },
|
|
42
|
+
limit: batchSize,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (events.length === 0) return;
|
|
46
|
+
|
|
47
|
+
yield* withUow((uow) =>
|
|
48
|
+
uow.eventRepository.markEventsAsInProcess(events),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
yield* publishEventsInParallel(events);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const retryFailedEvents = (): Effect.Effect<void> =>
|
|
55
|
+
Effect.gen(function* () {
|
|
56
|
+
const oneMinuteAgo = new Date(Date.now() - 60_000);
|
|
57
|
+
|
|
58
|
+
const events = yield* eventQueries.getEvents({
|
|
59
|
+
filters: {
|
|
60
|
+
statuses: ["to-republish", "failed-but-will-retry"],
|
|
61
|
+
occurredAt: { to: oneMinuteAgo },
|
|
62
|
+
},
|
|
63
|
+
limit: batchSize,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (events.length === 0) return;
|
|
67
|
+
|
|
68
|
+
yield* publishEventsInParallel(events);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const triggerProcessing = (): Effect.Effect<void> =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const results = yield* Effect.all(
|
|
74
|
+
[Effect.exit(processNewEvents()), Effect.exit(retryFailedEvents())],
|
|
75
|
+
{ concurrency: "unbounded" },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const errors = results
|
|
79
|
+
.filter(Exit.isFailure)
|
|
80
|
+
.map((exit) => Cause.squash(exit.cause));
|
|
81
|
+
|
|
82
|
+
if (errors.length > 0) {
|
|
83
|
+
yield* Effect.die(
|
|
84
|
+
new AggregateError(errors, "Event processing failed"),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const start = () => {
|
|
90
|
+
const scheduleProcessNewEvents = () => {
|
|
91
|
+
setTimeout(async () => {
|
|
92
|
+
try {
|
|
93
|
+
await Effect.runPromise(processNewEvents());
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error("Error processing new events:", error);
|
|
96
|
+
} finally {
|
|
97
|
+
scheduleProcessNewEvents();
|
|
98
|
+
}
|
|
99
|
+
}, newEventsIntervalMs);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const scheduleRetryFailedEvents = () => {
|
|
103
|
+
setTimeout(async () => {
|
|
104
|
+
try {
|
|
105
|
+
await Effect.runPromise(retryFailedEvents());
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error("Error retrying failed events:", error);
|
|
108
|
+
} finally {
|
|
109
|
+
scheduleRetryFailedEvents();
|
|
110
|
+
}
|
|
111
|
+
}, failedEventsIntervalMs);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
scheduleProcessNewEvents();
|
|
115
|
+
scheduleRetryFailedEvents();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
processNewEvents,
|
|
120
|
+
retryFailedEvents,
|
|
121
|
+
triggerProcessing,
|
|
122
|
+
start,
|
|
123
|
+
};
|
|
124
|
+
};
|