@l-etabli/events 0.2.0 → 0.3.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/dist/adapters/in-memory/InMemoryEventQueries.cjs +9 -1
- package/dist/adapters/in-memory/InMemoryEventQueries.cjs.map +1 -1
- package/dist/adapters/in-memory/InMemoryEventQueries.mjs +9 -1
- package/dist/adapters/in-memory/InMemoryEventQueries.mjs.map +1 -1
- package/dist/adapters/in-memory/InMemoryEventRepository.cjs +2 -1
- package/dist/adapters/in-memory/InMemoryEventRepository.cjs.map +1 -1
- package/dist/adapters/in-memory/InMemoryEventRepository.mjs +2 -1
- package/dist/adapters/in-memory/InMemoryEventRepository.mjs.map +1 -1
- package/dist/adapters/kysely/KyselyEventQueries.cjs +6 -0
- package/dist/adapters/kysely/KyselyEventQueries.cjs.map +1 -1
- package/dist/adapters/kysely/KyselyEventQueries.mjs +6 -0
- package/dist/adapters/kysely/KyselyEventQueries.mjs.map +1 -1
- package/dist/adapters/kysely/KyselyEventRepository.cjs +4 -1
- package/dist/adapters/kysely/KyselyEventRepository.cjs.map +1 -1
- package/dist/adapters/kysely/KyselyEventRepository.mjs +4 -1
- package/dist/adapters/kysely/KyselyEventRepository.mjs.map +1 -1
- package/dist/createEventCrawler.cjs +16 -1
- package/dist/createEventCrawler.cjs.map +1 -1
- package/dist/createEventCrawler.d.cts +13 -2
- package/dist/createEventCrawler.d.ts +13 -2
- package/dist/createEventCrawler.mjs +16 -1
- package/dist/createEventCrawler.mjs.map +1 -1
- 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 +7 -0
- package/dist/ports/EventQueries.d.ts +7 -0
- package/dist/ports/EventRepository.cjs.map +1 -1
- package/dist/ports/EventRepository.d.cts +30 -3
- package/dist/ports/EventRepository.d.ts +30 -3
- package/package.json +4 -3
- package/src/adapters/in-memory/InMemoryEventQueries.ts +15 -1
- package/src/adapters/in-memory/InMemoryEventRepository.ts +3 -1
- package/src/adapters/kysely/KyselyEventQueries.ts +9 -1
- package/src/adapters/kysely/KyselyEventRepository.ts +15 -1
- package/src/createEventCrawler.ts +37 -3
- package/src/ports/EventQueries.ts +7 -0
- package/src/ports/EventRepository.ts +33 -2
|
@@ -31,8 +31,16 @@ const createInMemoryEventQueries = (helpers) => ({
|
|
|
31
31
|
([key, value]) => event.context?.[key] === value
|
|
32
32
|
);
|
|
33
33
|
};
|
|
34
|
+
const matchesOccurredAt = (event) => {
|
|
35
|
+
if (!filters.occurredAt) return true;
|
|
36
|
+
const { from, to } = filters.occurredAt;
|
|
37
|
+
const eventTime = event.occurredAt.getTime();
|
|
38
|
+
if (from && eventTime < from.getTime()) return false;
|
|
39
|
+
if (to && eventTime > to.getTime()) return false;
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
34
42
|
return helpers.getAllEvents().filter(
|
|
35
|
-
(event) => filters.statuses.includes(event.status) && matchesContext(event)
|
|
43
|
+
(event) => filters.statuses.includes(event.status) && matchesContext(event) && matchesOccurredAt(event)
|
|
36
44
|
).slice(0, limit);
|
|
37
45
|
}
|
|
38
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventQueries.ts"],"sourcesContent":["import type { EventQueries } from \"../../ports/EventQueries.ts\";\nimport type { DefaultContext, GenericEvent } from \"../../types.ts\";\nimport type { InMemoryEventRepositoryHelpers } from \"./InMemoryEventRepository.ts\";\n\nexport const createInMemoryEventQueries = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n helpers: InMemoryEventRepositoryHelpers<Event>,\n): { eventQueries: EventQueries<Event> } => ({\n eventQueries: {\n getEvents: async ({ filters, limit }) => {\n const matchesContext = (event: Event): boolean => {\n if (!filters.context) return true;\n if (!event.context) return false;\n\n return Object.entries(filters.context).every(\n ([key, value]) => event.context?.[key] === value,\n );\n };\n\n return helpers\n .getAllEvents()\n .filter(\n (event) =>\n filters.statuses.includes(event.status)
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventQueries.ts"],"sourcesContent":["import type { EventQueries } from \"../../ports/EventQueries.ts\";\nimport type { DefaultContext, GenericEvent } from \"../../types.ts\";\nimport type { InMemoryEventRepositoryHelpers } from \"./InMemoryEventRepository.ts\";\n\nexport const createInMemoryEventQueries = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n helpers: InMemoryEventRepositoryHelpers<Event>,\n): { eventQueries: EventQueries<Event> } => ({\n eventQueries: {\n getEvents: async ({ filters, limit }) => {\n const matchesContext = (event: Event): boolean => {\n if (!filters.context) return true;\n if (!event.context) return false;\n\n return Object.entries(filters.context).every(\n ([key, value]) => event.context?.[key] === value,\n );\n };\n\n const matchesOccurredAt = (event: Event): boolean => {\n if (!filters.occurredAt) return true;\n\n const { from, to } = filters.occurredAt;\n const eventTime = event.occurredAt.getTime();\n\n if (from && eventTime < from.getTime()) return false;\n if (to && eventTime > to.getTime()) return false;\n\n return true;\n };\n\n return helpers\n .getAllEvents()\n .filter(\n (event) =>\n filters.statuses.includes(event.status) &&\n matchesContext(event) &&\n matchesOccurredAt(event),\n )\n .slice(0, limit);\n },\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,6BAA6B,CAGxC,aAC2C;AAAA,EAC3C,cAAc;AAAA,IACZ,WAAW,OAAO,EAAE,SAAS,MAAM,MAAM;AACvC,YAAM,iBAAiB,CAAC,UAA0B;AAChD,YAAI,CAAC,QAAQ,QAAS,QAAO;AAC7B,YAAI,CAAC,MAAM,QAAS,QAAO;AAE3B,eAAO,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAAA,UACrC,CAAC,CAAC,KAAK,KAAK,MAAM,MAAM,UAAU,GAAG,MAAM;AAAA,QAC7C;AAAA,MACF;AAEA,YAAM,oBAAoB,CAAC,UAA0B;AACnD,YAAI,CAAC,QAAQ,WAAY,QAAO;AAEhC,cAAM,EAAE,MAAM,GAAG,IAAI,QAAQ;AAC7B,cAAM,YAAY,MAAM,WAAW,QAAQ;AAE3C,YAAI,QAAQ,YAAY,KAAK,QAAQ,EAAG,QAAO;AAC/C,YAAI,MAAM,YAAY,GAAG,QAAQ,EAAG,QAAO;AAE3C,eAAO;AAAA,MACT;AAEA,aAAO,QACJ,aAAa,EACb;AAAA,QACC,CAAC,UACC,QAAQ,SAAS,SAAS,MAAM,MAAM,KACtC,eAAe,KAAK,KACpB,kBAAkB,KAAK;AAAA,MAC3B,EACC,MAAM,GAAG,KAAK;AAAA,IACnB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -8,8 +8,16 @@ const createInMemoryEventQueries = (helpers) => ({
|
|
|
8
8
|
([key, value]) => event.context?.[key] === value
|
|
9
9
|
);
|
|
10
10
|
};
|
|
11
|
+
const matchesOccurredAt = (event) => {
|
|
12
|
+
if (!filters.occurredAt) return true;
|
|
13
|
+
const { from, to } = filters.occurredAt;
|
|
14
|
+
const eventTime = event.occurredAt.getTime();
|
|
15
|
+
if (from && eventTime < from.getTime()) return false;
|
|
16
|
+
if (to && eventTime > to.getTime()) return false;
|
|
17
|
+
return true;
|
|
18
|
+
};
|
|
11
19
|
return helpers.getAllEvents().filter(
|
|
12
|
-
(event) => filters.statuses.includes(event.status) && matchesContext(event)
|
|
20
|
+
(event) => filters.statuses.includes(event.status) && matchesContext(event) && matchesOccurredAt(event)
|
|
13
21
|
).slice(0, limit);
|
|
14
22
|
}
|
|
15
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventQueries.ts"],"sourcesContent":["import type { EventQueries } from '../../ports/EventQueries.ts.mjs';\nimport type { DefaultContext, GenericEvent } from '../../types.ts.mjs';\nimport type { InMemoryEventRepositoryHelpers } from './InMemoryEventRepository.ts.mjs';\n\nexport const createInMemoryEventQueries = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n helpers: InMemoryEventRepositoryHelpers<Event>,\n): { eventQueries: EventQueries<Event> } => ({\n eventQueries: {\n getEvents: async ({ filters, limit }) => {\n const matchesContext = (event: Event): boolean => {\n if (!filters.context) return true;\n if (!event.context) return false;\n\n return Object.entries(filters.context).every(\n ([key, value]) => event.context?.[key] === value,\n );\n };\n\n return helpers\n .getAllEvents()\n .filter(\n (event) =>\n filters.statuses.includes(event.status)
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventQueries.ts"],"sourcesContent":["import type { EventQueries } from '../../ports/EventQueries.ts.mjs';\nimport type { DefaultContext, GenericEvent } from '../../types.ts.mjs';\nimport type { InMemoryEventRepositoryHelpers } from './InMemoryEventRepository.ts.mjs';\n\nexport const createInMemoryEventQueries = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n helpers: InMemoryEventRepositoryHelpers<Event>,\n): { eventQueries: EventQueries<Event> } => ({\n eventQueries: {\n getEvents: async ({ filters, limit }) => {\n const matchesContext = (event: Event): boolean => {\n if (!filters.context) return true;\n if (!event.context) return false;\n\n return Object.entries(filters.context).every(\n ([key, value]) => event.context?.[key] === value,\n );\n };\n\n const matchesOccurredAt = (event: Event): boolean => {\n if (!filters.occurredAt) return true;\n\n const { from, to } = filters.occurredAt;\n const eventTime = event.occurredAt.getTime();\n\n if (from && eventTime < from.getTime()) return false;\n if (to && eventTime > to.getTime()) return false;\n\n return true;\n };\n\n return helpers\n .getAllEvents()\n .filter(\n (event) =>\n filters.statuses.includes(event.status) &&\n matchesContext(event) &&\n matchesOccurredAt(event),\n )\n .slice(0, limit);\n },\n },\n});\n"],"mappings":"AAIO,MAAM,6BAA6B,CAGxC,aAC2C;AAAA,EAC3C,cAAc;AAAA,IACZ,WAAW,OAAO,EAAE,SAAS,MAAM,MAAM;AACvC,YAAM,iBAAiB,CAAC,UAA0B;AAChD,YAAI,CAAC,QAAQ,QAAS,QAAO;AAC7B,YAAI,CAAC,MAAM,QAAS,QAAO;AAE3B,eAAO,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAAA,UACrC,CAAC,CAAC,KAAK,KAAK,MAAM,MAAM,UAAU,GAAG,MAAM;AAAA,QAC7C;AAAA,MACF;AAEA,YAAM,oBAAoB,CAAC,UAA0B;AACnD,YAAI,CAAC,QAAQ,WAAY,QAAO;AAEhC,cAAM,EAAE,MAAM,GAAG,IAAI,QAAQ;AAC7B,cAAM,YAAY,MAAM,WAAW,QAAQ;AAE3C,YAAI,QAAQ,YAAY,KAAK,QAAQ,EAAG,QAAO;AAC/C,YAAI,MAAM,YAAY,GAAG,QAAQ,EAAG,QAAO;AAE3C,eAAO;AAAA,MACT;AAEA,aAAO,QACJ,aAAa,EACb;AAAA,QACC,CAAC,UACC,QAAQ,SAAS,SAAS,MAAM,MAAM,KACtC,eAAe,KAAK,KACpB,kBAAkB,KAAK;AAAA,MAC3B,EACC,MAAM,GAAG,KAAK;AAAA,IACnB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -55,8 +55,9 @@ const createInMemoryEventRepository = () => {
|
|
|
55
55
|
};
|
|
56
56
|
};
|
|
57
57
|
const createInMemoryWithUow = (eventRepository) => {
|
|
58
|
-
const withUow = async (fn) => {
|
|
58
|
+
const withUow = async (fn, options) => {
|
|
59
59
|
await fn({ eventRepository });
|
|
60
|
+
await options?.afterCommit?.();
|
|
60
61
|
};
|
|
61
62
|
return { withUow };
|
|
62
63
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventRepository.ts"],"sourcesContent":["import type {\n EventRepository,\n WithEventsUow,\n} from \"../../ports/EventRepository.ts\";\nimport type { DefaultContext, GenericEvent } from \"../../types.ts\";\n\nexport type InMemoryEventRepositoryHelpers<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = { getAllEvents: () => Event[]; setEvents: (events: Event[]) => void };\n\nexport const createInMemoryEventRepository = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(): {\n eventRepository: EventRepository<Event>;\n helpers: InMemoryEventRepositoryHelpers<Event>;\n} => {\n const eventById: Record<string, Event> = {};\n\n const eventRepository: EventRepository<Event> = {\n save: async (event) => {\n eventById[event.id] = event;\n },\n saveNewEventsBatch: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n markEventsAsInProcess: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = { ...event, status: \"in-process\" };\n });\n },\n };\n\n return {\n eventRepository,\n helpers: {\n getAllEvents: () => Object.values(eventById),\n setEvents: (events) => {\n Object.keys(eventById).forEach((key) => {\n delete eventById[key];\n });\n\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n },\n };\n};\n\nexport const createInMemoryWithUow = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n eventRepository: EventRepository<Event>,\n): { withUow: WithEventsUow<Event> } => {\n const withUow: WithEventsUow<Event> = async (fn) => {\n await fn({ eventRepository });\n };\n return { withUow };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUO,MAAM,gCAAgC,MAKxC;AACH,QAAM,YAAmC,CAAC;AAE1C,QAAM,kBAA0C;AAAA,IAC9C,MAAM,OAAO,UAAU;AACrB,gBAAU,MAAM,EAAE,IAAI;AAAA,IACxB;AAAA,IACA,oBAAoB,OAAO,WAAW;AACpC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI;AAAA,MACxB,CAAC;AAAA,IACH;AAAA,IACA,uBAAuB,OAAO,WAAW;AACvC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,QAAQ,aAAa;AAAA,MACzD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,SAAS;AAAA,MACP,cAAc,MAAM,OAAO,OAAO,SAAS;AAAA,MAC3C,WAAW,CAAC,WAAW;AACrB,eAAO,KAAK,SAAS,EAAE,QAAQ,CAAC,QAAQ;AACtC,iBAAO,UAAU,GAAG;AAAA,QACtB,CAAC;AAED,eAAO,QAAQ,CAAC,UAAU;AACxB,oBAAU,MAAM,EAAE,IAAI;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,wBAAwB,CAGnC,oBACsC;
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventRepository.ts"],"sourcesContent":["import type {\n EventRepository,\n WithEventsUow,\n} from \"../../ports/EventRepository.ts\";\nimport type { DefaultContext, GenericEvent } from \"../../types.ts\";\n\nexport type InMemoryEventRepositoryHelpers<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = { getAllEvents: () => Event[]; setEvents: (events: Event[]) => void };\n\nexport const createInMemoryEventRepository = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(): {\n eventRepository: EventRepository<Event>;\n helpers: InMemoryEventRepositoryHelpers<Event>;\n} => {\n const eventById: Record<string, Event> = {};\n\n const eventRepository: EventRepository<Event> = {\n save: async (event) => {\n eventById[event.id] = event;\n },\n saveNewEventsBatch: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n markEventsAsInProcess: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = { ...event, status: \"in-process\" };\n });\n },\n };\n\n return {\n eventRepository,\n helpers: {\n getAllEvents: () => Object.values(eventById),\n setEvents: (events) => {\n Object.keys(eventById).forEach((key) => {\n delete eventById[key];\n });\n\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n },\n };\n};\n\nexport const createInMemoryWithUow = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n eventRepository: EventRepository<Event>,\n): { withUow: WithEventsUow<Event> } => {\n // In-memory adapter awaits afterCommit for predictable test behavior\n const withUow: WithEventsUow<Event> = async (fn, options) => {\n await fn({ eventRepository });\n await options?.afterCommit?.();\n };\n return { withUow };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUO,MAAM,gCAAgC,MAKxC;AACH,QAAM,YAAmC,CAAC;AAE1C,QAAM,kBAA0C;AAAA,IAC9C,MAAM,OAAO,UAAU;AACrB,gBAAU,MAAM,EAAE,IAAI;AAAA,IACxB;AAAA,IACA,oBAAoB,OAAO,WAAW;AACpC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI;AAAA,MACxB,CAAC;AAAA,IACH;AAAA,IACA,uBAAuB,OAAO,WAAW;AACvC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,QAAQ,aAAa;AAAA,MACzD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,SAAS;AAAA,MACP,cAAc,MAAM,OAAO,OAAO,SAAS;AAAA,MAC3C,WAAW,CAAC,WAAW;AACrB,eAAO,KAAK,SAAS,EAAE,QAAQ,CAAC,QAAQ;AACtC,iBAAO,UAAU,GAAG;AAAA,QACtB,CAAC;AAED,eAAO,QAAQ,CAAC,UAAU;AACxB,oBAAU,MAAM,EAAE,IAAI;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,wBAAwB,CAGnC,oBACsC;AAEtC,QAAM,UAAgC,OAAO,IAAI,YAAY;AAC3D,UAAM,GAAG,EAAE,gBAAgB,CAAC;AAC5B,UAAM,SAAS,cAAc;AAAA,EAC/B;AACA,SAAO,EAAE,QAAQ;AACnB;","names":[]}
|
|
@@ -31,8 +31,9 @@ const createInMemoryEventRepository = () => {
|
|
|
31
31
|
};
|
|
32
32
|
};
|
|
33
33
|
const createInMemoryWithUow = (eventRepository) => {
|
|
34
|
-
const withUow = async (fn) => {
|
|
34
|
+
const withUow = async (fn, options) => {
|
|
35
35
|
await fn({ eventRepository });
|
|
36
|
+
await options?.afterCommit?.();
|
|
36
37
|
};
|
|
37
38
|
return { withUow };
|
|
38
39
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventRepository.ts"],"sourcesContent":["import type {\n EventRepository,\n WithEventsUow,\n} from '../../ports/EventRepository.ts.mjs';\nimport type { DefaultContext, GenericEvent } from '../../types.ts.mjs';\n\nexport type InMemoryEventRepositoryHelpers<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = { getAllEvents: () => Event[]; setEvents: (events: Event[]) => void };\n\nexport const createInMemoryEventRepository = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(): {\n eventRepository: EventRepository<Event>;\n helpers: InMemoryEventRepositoryHelpers<Event>;\n} => {\n const eventById: Record<string, Event> = {};\n\n const eventRepository: EventRepository<Event> = {\n save: async (event) => {\n eventById[event.id] = event;\n },\n saveNewEventsBatch: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n markEventsAsInProcess: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = { ...event, status: \"in-process\" };\n });\n },\n };\n\n return {\n eventRepository,\n helpers: {\n getAllEvents: () => Object.values(eventById),\n setEvents: (events) => {\n Object.keys(eventById).forEach((key) => {\n delete eventById[key];\n });\n\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n },\n };\n};\n\nexport const createInMemoryWithUow = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n eventRepository: EventRepository<Event>,\n): { withUow: WithEventsUow<Event> } => {\n const withUow: WithEventsUow<Event> = async (fn) => {\n await fn({ eventRepository });\n };\n return { withUow };\n};\n"],"mappings":"AAUO,MAAM,gCAAgC,MAKxC;AACH,QAAM,YAAmC,CAAC;AAE1C,QAAM,kBAA0C;AAAA,IAC9C,MAAM,OAAO,UAAU;AACrB,gBAAU,MAAM,EAAE,IAAI;AAAA,IACxB;AAAA,IACA,oBAAoB,OAAO,WAAW;AACpC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI;AAAA,MACxB,CAAC;AAAA,IACH;AAAA,IACA,uBAAuB,OAAO,WAAW;AACvC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,QAAQ,aAAa;AAAA,MACzD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,SAAS;AAAA,MACP,cAAc,MAAM,OAAO,OAAO,SAAS;AAAA,MAC3C,WAAW,CAAC,WAAW;AACrB,eAAO,KAAK,SAAS,EAAE,QAAQ,CAAC,QAAQ;AACtC,iBAAO,UAAU,GAAG;AAAA,QACtB,CAAC;AAED,eAAO,QAAQ,CAAC,UAAU;AACxB,oBAAU,MAAM,EAAE,IAAI;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,wBAAwB,CAGnC,oBACsC;
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventRepository.ts"],"sourcesContent":["import type {\n EventRepository,\n WithEventsUow,\n} from '../../ports/EventRepository.ts.mjs';\nimport type { DefaultContext, GenericEvent } from '../../types.ts.mjs';\n\nexport type InMemoryEventRepositoryHelpers<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = { getAllEvents: () => Event[]; setEvents: (events: Event[]) => void };\n\nexport const createInMemoryEventRepository = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(): {\n eventRepository: EventRepository<Event>;\n helpers: InMemoryEventRepositoryHelpers<Event>;\n} => {\n const eventById: Record<string, Event> = {};\n\n const eventRepository: EventRepository<Event> = {\n save: async (event) => {\n eventById[event.id] = event;\n },\n saveNewEventsBatch: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n markEventsAsInProcess: async (events) => {\n events.forEach((event) => {\n eventById[event.id] = { ...event, status: \"in-process\" };\n });\n },\n };\n\n return {\n eventRepository,\n helpers: {\n getAllEvents: () => Object.values(eventById),\n setEvents: (events) => {\n Object.keys(eventById).forEach((key) => {\n delete eventById[key];\n });\n\n events.forEach((event) => {\n eventById[event.id] = event;\n });\n },\n },\n };\n};\n\nexport const createInMemoryWithUow = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n eventRepository: EventRepository<Event>,\n): { withUow: WithEventsUow<Event> } => {\n // In-memory adapter awaits afterCommit for predictable test behavior\n const withUow: WithEventsUow<Event> = async (fn, options) => {\n await fn({ eventRepository });\n await options?.afterCommit?.();\n };\n return { withUow };\n};\n"],"mappings":"AAUO,MAAM,gCAAgC,MAKxC;AACH,QAAM,YAAmC,CAAC;AAE1C,QAAM,kBAA0C;AAAA,IAC9C,MAAM,OAAO,UAAU;AACrB,gBAAU,MAAM,EAAE,IAAI;AAAA,IACxB;AAAA,IACA,oBAAoB,OAAO,WAAW;AACpC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI;AAAA,MACxB,CAAC;AAAA,IACH;AAAA,IACA,uBAAuB,OAAO,WAAW;AACvC,aAAO,QAAQ,CAAC,UAAU;AACxB,kBAAU,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,QAAQ,aAAa;AAAA,MACzD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,SAAS;AAAA,MACP,cAAc,MAAM,OAAO,OAAO,SAAS;AAAA,MAC3C,WAAW,CAAC,WAAW;AACrB,eAAO,KAAK,SAAS,EAAE,QAAQ,CAAC,QAAQ;AACtC,iBAAO,UAAU,GAAG;AAAA,QACtB,CAAC;AAED,eAAO,QAAQ,CAAC,UAAU;AACxB,oBAAU,MAAM,EAAE,IAAI;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,wBAAwB,CAGnC,oBACsC;AAEtC,QAAM,UAAgC,OAAO,IAAI,YAAY;AAC3D,UAAM,GAAG,EAAE,gBAAgB,CAAC;AAC5B,UAAM,SAAS,cAAc;AAAA,EAC/B;AACA,SAAO,EAAE,QAAQ;AACnB;","names":[]}
|
|
@@ -30,6 +30,12 @@ const createKyselyEventQueries = (db) => ({
|
|
|
30
30
|
query = query.where(import_kysely.sql`context->>${key} = ${value}`);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
if (filters.occurredAt?.from) {
|
|
34
|
+
query = query.where("occurredAt", ">=", filters.occurredAt.from);
|
|
35
|
+
}
|
|
36
|
+
if (filters.occurredAt?.to) {
|
|
37
|
+
query = query.where("occurredAt", "<=", filters.occurredAt.to);
|
|
38
|
+
}
|
|
33
39
|
const rows = await query.execute();
|
|
34
40
|
return rows.map(
|
|
35
41
|
(row) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/kysely/KyselyEventQueries.ts"],"sourcesContent":["import type { Kysely, SqlBool } from \"kysely\";\nimport { sql } from \"kysely\";\nimport type { EventQueries } from \"../../ports/EventQueries.ts\";\nimport type { DefaultContext, GenericEvent } from \"../../types.ts\";\nimport type { EventsTable } from \"./types.ts\";\n\nexport const createKyselyEventQueries = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n db: Kysely<EventsTable>,\n): EventQueries<Event> => ({\n getEvents: async ({ filters, limit }) => {\n let query = db\n .selectFrom(\"events\")\n .selectAll()\n .where(\"status\", \"in\", filters.statuses)\n .limit(limit);\n\n if (filters.context) {\n for (const [key, value] of Object.entries(filters.context)) {\n query = query.where(sql<SqlBool>`context->>${key} = ${value}`);\n }\n }\n\n const rows = await query.execute();\n return rows.map(\n (row) =>\n ({\n ...row,\n context: row.context ?? undefined,\n priority: row.priority ?? undefined,\n }) as Event,\n );\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,oBAAoB;AAKb,MAAM,2BAA2B,CAGtC,QACyB;AAAA,EACzB,WAAW,OAAO,EAAE,SAAS,MAAM,MAAM;AACvC,QAAI,QAAQ,GACT,WAAW,QAAQ,EACnB,UAAU,EACV,MAAM,UAAU,MAAM,QAAQ,QAAQ,EACtC,MAAM,KAAK;AAEd,QAAI,QAAQ,SAAS;AACnB,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,OAAO,GAAG;AAC1D,gBAAQ,MAAM,MAAM,8BAAyB,GAAG,MAAM,KAAK,EAAE;AAAA,MAC/D;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,MAAM,QAAQ;AACjC,WAAO,KAAK;AAAA,MACV,CAAC,SACE;AAAA,QACC,GAAG;AAAA,QACH,SAAS,IAAI,WAAW;AAAA,QACxB,UAAU,IAAI,YAAY;AAAA,MAC5B;AAAA,IACJ;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/kysely/KyselyEventQueries.ts"],"sourcesContent":["import type { Kysely, SqlBool } from \"kysely\";\nimport { sql } from \"kysely\";\nimport type { EventQueries } from \"../../ports/EventQueries.ts\";\nimport type { DefaultContext, GenericEvent } from \"../../types.ts\";\nimport type { EventsTable } from \"./types.ts\";\n\nexport const createKyselyEventQueries = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n db: Kysely<EventsTable>,\n): EventQueries<Event> => ({\n getEvents: async ({ filters, limit }) => {\n let query = db\n .selectFrom(\"events\")\n .selectAll()\n .where(\"status\", \"in\", filters.statuses)\n .limit(limit);\n\n if (filters.context) {\n for (const [key, value] of Object.entries(filters.context)) {\n query = query.where(sql<SqlBool>`context->>${key} = ${value}`);\n }\n }\n\n if (filters.occurredAt?.from) {\n query = query.where(\"occurredAt\", \">=\", filters.occurredAt.from);\n }\n\n if (filters.occurredAt?.to) {\n query = query.where(\"occurredAt\", \"<=\", filters.occurredAt.to);\n }\n\n const rows = await query.execute();\n return rows.map(\n (row: EventsTable[\"events\"]) =>\n ({\n ...row,\n context: row.context ?? undefined,\n priority: row.priority ?? undefined,\n }) as Event,\n );\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,oBAAoB;AAKb,MAAM,2BAA2B,CAGtC,QACyB;AAAA,EACzB,WAAW,OAAO,EAAE,SAAS,MAAM,MAAM;AACvC,QAAI,QAAQ,GACT,WAAW,QAAQ,EACnB,UAAU,EACV,MAAM,UAAU,MAAM,QAAQ,QAAQ,EACtC,MAAM,KAAK;AAEd,QAAI,QAAQ,SAAS;AACnB,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,OAAO,GAAG;AAC1D,gBAAQ,MAAM,MAAM,8BAAyB,GAAG,MAAM,KAAK,EAAE;AAAA,MAC/D;AAAA,IACF;AAEA,QAAI,QAAQ,YAAY,MAAM;AAC5B,cAAQ,MAAM,MAAM,cAAc,MAAM,QAAQ,WAAW,IAAI;AAAA,IACjE;AAEA,QAAI,QAAQ,YAAY,IAAI;AAC1B,cAAQ,MAAM,MAAM,cAAc,MAAM,QAAQ,WAAW,EAAE;AAAA,IAC/D;AAEA,UAAM,OAAO,MAAM,MAAM,QAAQ;AACjC,WAAO,KAAK;AAAA,MACV,CAAC,SACE;AAAA,QACC,GAAG;AAAA,QACH,SAAS,IAAI,WAAW;AAAA,QACxB,UAAU,IAAI,YAAY;AAAA,MAC5B;AAAA,IACJ;AAAA,EACF;AACF;","names":[]}
|
|
@@ -7,6 +7,12 @@ const createKyselyEventQueries = (db) => ({
|
|
|
7
7
|
query = query.where(sql`context->>${key} = ${value}`);
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
+
if (filters.occurredAt?.from) {
|
|
11
|
+
query = query.where("occurredAt", ">=", filters.occurredAt.from);
|
|
12
|
+
}
|
|
13
|
+
if (filters.occurredAt?.to) {
|
|
14
|
+
query = query.where("occurredAt", "<=", filters.occurredAt.to);
|
|
15
|
+
}
|
|
10
16
|
const rows = await query.execute();
|
|
11
17
|
return rows.map(
|
|
12
18
|
(row) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/kysely/KyselyEventQueries.ts"],"sourcesContent":["import type { Kysely, SqlBool } from \"kysely\";\nimport { sql } from \"kysely\";\nimport type { EventQueries } from '../../ports/EventQueries.ts.mjs';\nimport type { DefaultContext, GenericEvent } from '../../types.ts.mjs';\nimport type { EventsTable } from './types.ts.mjs';\n\nexport const createKyselyEventQueries = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n db: Kysely<EventsTable>,\n): EventQueries<Event> => ({\n getEvents: async ({ filters, limit }) => {\n let query = db\n .selectFrom(\"events\")\n .selectAll()\n .where(\"status\", \"in\", filters.statuses)\n .limit(limit);\n\n if (filters.context) {\n for (const [key, value] of Object.entries(filters.context)) {\n query = query.where(sql<SqlBool>`context->>${key} = ${value}`);\n }\n }\n\n const rows = await query.execute();\n return rows.map(\n (row) =>\n ({\n ...row,\n context: row.context ?? undefined,\n priority: row.priority ?? undefined,\n }) as Event,\n );\n },\n});\n"],"mappings":"AACA,SAAS,WAAW;AAKb,MAAM,2BAA2B,CAGtC,QACyB;AAAA,EACzB,WAAW,OAAO,EAAE,SAAS,MAAM,MAAM;AACvC,QAAI,QAAQ,GACT,WAAW,QAAQ,EACnB,UAAU,EACV,MAAM,UAAU,MAAM,QAAQ,QAAQ,EACtC,MAAM,KAAK;AAEd,QAAI,QAAQ,SAAS;AACnB,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,OAAO,GAAG;AAC1D,gBAAQ,MAAM,MAAM,gBAAyB,GAAG,MAAM,KAAK,EAAE;AAAA,MAC/D;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,MAAM,QAAQ;AACjC,WAAO,KAAK;AAAA,MACV,CAAC,SACE;AAAA,QACC,GAAG;AAAA,QACH,SAAS,IAAI,WAAW;AAAA,QACxB,UAAU,IAAI,YAAY;AAAA,MAC5B;AAAA,IACJ;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/kysely/KyselyEventQueries.ts"],"sourcesContent":["import type { Kysely, SqlBool } from \"kysely\";\nimport { sql } from \"kysely\";\nimport type { EventQueries } from '../../ports/EventQueries.ts.mjs';\nimport type { DefaultContext, GenericEvent } from '../../types.ts.mjs';\nimport type { EventsTable } from './types.ts.mjs';\n\nexport const createKyselyEventQueries = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n db: Kysely<EventsTable>,\n): EventQueries<Event> => ({\n getEvents: async ({ filters, limit }) => {\n let query = db\n .selectFrom(\"events\")\n .selectAll()\n .where(\"status\", \"in\", filters.statuses)\n .limit(limit);\n\n if (filters.context) {\n for (const [key, value] of Object.entries(filters.context)) {\n query = query.where(sql<SqlBool>`context->>${key} = ${value}`);\n }\n }\n\n if (filters.occurredAt?.from) {\n query = query.where(\"occurredAt\", \">=\", filters.occurredAt.from);\n }\n\n if (filters.occurredAt?.to) {\n query = query.where(\"occurredAt\", \"<=\", filters.occurredAt.to);\n }\n\n const rows = await query.execute();\n return rows.map(\n (row: EventsTable[\"events\"]) =>\n ({\n ...row,\n context: row.context ?? undefined,\n priority: row.priority ?? undefined,\n }) as Event,\n );\n },\n});\n"],"mappings":"AACA,SAAS,WAAW;AAKb,MAAM,2BAA2B,CAGtC,QACyB;AAAA,EACzB,WAAW,OAAO,EAAE,SAAS,MAAM,MAAM;AACvC,QAAI,QAAQ,GACT,WAAW,QAAQ,EACnB,UAAU,EACV,MAAM,UAAU,MAAM,QAAQ,QAAQ,EACtC,MAAM,KAAK;AAEd,QAAI,QAAQ,SAAS;AACnB,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,OAAO,GAAG;AAC1D,gBAAQ,MAAM,MAAM,gBAAyB,GAAG,MAAM,KAAK,EAAE;AAAA,MAC/D;AAAA,IACF;AAEA,QAAI,QAAQ,YAAY,MAAM;AAC5B,cAAQ,MAAM,MAAM,cAAc,MAAM,QAAQ,WAAW,IAAI;AAAA,IACjE;AAEA,QAAI,QAAQ,YAAY,IAAI;AAC1B,cAAQ,MAAM,MAAM,cAAc,MAAM,QAAQ,WAAW,EAAE;AAAA,IAC/D;AAEA,UAAM,OAAO,MAAM,MAAM,QAAQ;AACjC,WAAO,KAAK;AAAA,MACV,CAAC,SACE;AAAA,QACC,GAAG;AAAA,QACH,SAAS,IAAI,WAAW;AAAA,QACxB,UAAU,IAAI,YAAY;AAAA,MAC5B;AAAA,IACJ;AAAA,EACF;AACF;","names":[]}
|
|
@@ -43,7 +43,10 @@ const createKyselyEventRepository = (db) => ({
|
|
|
43
43
|
markEventsAsInProcess: async (events) => {
|
|
44
44
|
if (events.length === 0) return;
|
|
45
45
|
const ids = events.map((e) => e.id);
|
|
46
|
-
await db.
|
|
46
|
+
const lockedRows = await db.selectFrom("events").select("id").where("id", "in", ids).forUpdate().skipLocked().execute();
|
|
47
|
+
if (lockedRows.length === 0) return;
|
|
48
|
+
const lockedIds = lockedRows.map((r) => r.id);
|
|
49
|
+
await db.updateTable("events").set({ status: "in-process" }).where("id", "in", lockedIds).execute();
|
|
47
50
|
}
|
|
48
51
|
});
|
|
49
52
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/kysely/KyselyEventRepository.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\nimport type { EventRepository } from \"../../ports/EventRepository.ts\";\nimport type { DefaultContext, GenericEvent } from \"../../types.ts\";\nimport type { EventsTable } from \"./types.ts\";\n\nexport const createKyselyEventRepository = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n db: Kysely<EventsTable>,\n): EventRepository<Event> => ({\n save: async (event) => {\n await db\n .insertInto(\"events\")\n .values(event)\n .onConflict((oc) =>\n oc.column(\"id\").doUpdateSet({\n topic: event.topic,\n payload: event.payload,\n context: event.context,\n status: event.status,\n triggeredByUserId: event.triggeredByUserId,\n occurredAt: event.occurredAt,\n publications: event.publications,\n priority: event.priority,\n }),\n )\n .execute();\n },\n\n saveNewEventsBatch: async (events) => {\n if (events.length === 0) return;\n await db.insertInto(\"events\").values(events).execute();\n },\n\n markEventsAsInProcess: async (events) => {\n if (events.length === 0) return;\n const ids = events.map((e) => e.id);\n await db\n .updateTable(\"events\")\n .set({ status: \"in-process\" })\n .where(\"id\", \"in\",
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/kysely/KyselyEventRepository.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\nimport type { EventRepository } from \"../../ports/EventRepository.ts\";\nimport type { DefaultContext, GenericEvent } from \"../../types.ts\";\nimport type { EventsTable } from \"./types.ts\";\n\nexport const createKyselyEventRepository = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n db: Kysely<EventsTable>,\n): EventRepository<Event> => ({\n save: async (event) => {\n await db\n .insertInto(\"events\")\n .values(event)\n .onConflict((oc) =>\n oc.column(\"id\").doUpdateSet({\n topic: event.topic,\n payload: event.payload,\n context: event.context,\n status: event.status,\n triggeredByUserId: event.triggeredByUserId,\n occurredAt: event.occurredAt,\n publications: event.publications,\n priority: event.priority,\n }),\n )\n .execute();\n },\n\n saveNewEventsBatch: async (events) => {\n if (events.length === 0) return;\n await db.insertInto(\"events\").values(events).execute();\n },\n\n markEventsAsInProcess: async (events) => {\n if (events.length === 0) return;\n const ids = events.map((e) => e.id);\n\n // Lock the rows to prevent concurrent processing\n const lockedRows = await db\n .selectFrom(\"events\")\n .select(\"id\")\n .where(\"id\", \"in\", ids)\n .forUpdate()\n .skipLocked()\n .execute();\n\n if (lockedRows.length === 0) return;\n const lockedIds = lockedRows.map((r) => r.id);\n\n // Update status to in-process (only for locked rows)\n await db\n .updateTable(\"events\")\n .set({ status: \"in-process\" })\n .where(\"id\", \"in\", lockedIds)\n .execute();\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAKO,MAAM,8BAA8B,CAGzC,QAC4B;AAAA,EAC5B,MAAM,OAAO,UAAU;AACrB,UAAM,GACH,WAAW,QAAQ,EACnB,OAAO,KAAK,EACZ;AAAA,MAAW,CAAC,OACX,GAAG,OAAO,IAAI,EAAE,YAAY;AAAA,QAC1B,OAAO,MAAM;AAAA,QACb,SAAS,MAAM;AAAA,QACf,SAAS,MAAM;AAAA,QACf,QAAQ,MAAM;AAAA,QACd,mBAAmB,MAAM;AAAA,QACzB,YAAY,MAAM;AAAA,QAClB,cAAc,MAAM;AAAA,QACpB,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,IACH,EACC,QAAQ;AAAA,EACb;AAAA,EAEA,oBAAoB,OAAO,WAAW;AACpC,QAAI,OAAO,WAAW,EAAG;AACzB,UAAM,GAAG,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,QAAQ;AAAA,EACvD;AAAA,EAEA,uBAAuB,OAAO,WAAW;AACvC,QAAI,OAAO,WAAW,EAAG;AACzB,UAAM,MAAM,OAAO,IAAI,CAAC,MAAM,EAAE,EAAE;AAGlC,UAAM,aAAa,MAAM,GACtB,WAAW,QAAQ,EACnB,OAAO,IAAI,EACX,MAAM,MAAM,MAAM,GAAG,EACrB,UAAU,EACV,WAAW,EACX,QAAQ;AAEX,QAAI,WAAW,WAAW,EAAG;AAC7B,UAAM,YAAY,WAAW,IAAI,CAAC,MAAM,EAAE,EAAE;AAG5C,UAAM,GACH,YAAY,QAAQ,EACpB,IAAI,EAAE,QAAQ,aAAa,CAAC,EAC5B,MAAM,MAAM,MAAM,SAAS,EAC3B,QAAQ;AAAA,EACb;AACF;","names":[]}
|
|
@@ -20,7 +20,10 @@ const createKyselyEventRepository = (db) => ({
|
|
|
20
20
|
markEventsAsInProcess: async (events) => {
|
|
21
21
|
if (events.length === 0) return;
|
|
22
22
|
const ids = events.map((e) => e.id);
|
|
23
|
-
await db.
|
|
23
|
+
const lockedRows = await db.selectFrom("events").select("id").where("id", "in", ids).forUpdate().skipLocked().execute();
|
|
24
|
+
if (lockedRows.length === 0) return;
|
|
25
|
+
const lockedIds = lockedRows.map((r) => r.id);
|
|
26
|
+
await db.updateTable("events").set({ status: "in-process" }).where("id", "in", lockedIds).execute();
|
|
24
27
|
}
|
|
25
28
|
});
|
|
26
29
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/kysely/KyselyEventRepository.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\nimport type { EventRepository } from '../../ports/EventRepository.ts.mjs';\nimport type { DefaultContext, GenericEvent } from '../../types.ts.mjs';\nimport type { EventsTable } from './types.ts.mjs';\n\nexport const createKyselyEventRepository = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n db: Kysely<EventsTable>,\n): EventRepository<Event> => ({\n save: async (event) => {\n await db\n .insertInto(\"events\")\n .values(event)\n .onConflict((oc) =>\n oc.column(\"id\").doUpdateSet({\n topic: event.topic,\n payload: event.payload,\n context: event.context,\n status: event.status,\n triggeredByUserId: event.triggeredByUserId,\n occurredAt: event.occurredAt,\n publications: event.publications,\n priority: event.priority,\n }),\n )\n .execute();\n },\n\n saveNewEventsBatch: async (events) => {\n if (events.length === 0) return;\n await db.insertInto(\"events\").values(events).execute();\n },\n\n markEventsAsInProcess: async (events) => {\n if (events.length === 0) return;\n const ids = events.map((e) => e.id);\n await db\n .updateTable(\"events\")\n .set({ status: \"in-process\" })\n .where(\"id\", \"in\",
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/kysely/KyselyEventRepository.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\nimport type { EventRepository } from '../../ports/EventRepository.ts.mjs';\nimport type { DefaultContext, GenericEvent } from '../../types.ts.mjs';\nimport type { EventsTable } from './types.ts.mjs';\n\nexport const createKyselyEventRepository = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n db: Kysely<EventsTable>,\n): EventRepository<Event> => ({\n save: async (event) => {\n await db\n .insertInto(\"events\")\n .values(event)\n .onConflict((oc) =>\n oc.column(\"id\").doUpdateSet({\n topic: event.topic,\n payload: event.payload,\n context: event.context,\n status: event.status,\n triggeredByUserId: event.triggeredByUserId,\n occurredAt: event.occurredAt,\n publications: event.publications,\n priority: event.priority,\n }),\n )\n .execute();\n },\n\n saveNewEventsBatch: async (events) => {\n if (events.length === 0) return;\n await db.insertInto(\"events\").values(events).execute();\n },\n\n markEventsAsInProcess: async (events) => {\n if (events.length === 0) return;\n const ids = events.map((e) => e.id);\n\n // Lock the rows to prevent concurrent processing\n const lockedRows = await db\n .selectFrom(\"events\")\n .select(\"id\")\n .where(\"id\", \"in\", ids)\n .forUpdate()\n .skipLocked()\n .execute();\n\n if (lockedRows.length === 0) return;\n const lockedIds = lockedRows.map((r) => r.id);\n\n // Update status to in-process (only for locked rows)\n await db\n .updateTable(\"events\")\n .set({ status: \"in-process\" })\n .where(\"id\", \"in\", lockedIds)\n .execute();\n },\n});\n"],"mappings":"AAKO,MAAM,8BAA8B,CAGzC,QAC4B;AAAA,EAC5B,MAAM,OAAO,UAAU;AACrB,UAAM,GACH,WAAW,QAAQ,EACnB,OAAO,KAAK,EACZ;AAAA,MAAW,CAAC,OACX,GAAG,OAAO,IAAI,EAAE,YAAY;AAAA,QAC1B,OAAO,MAAM;AAAA,QACb,SAAS,MAAM;AAAA,QACf,SAAS,MAAM;AAAA,QACf,QAAQ,MAAM;AAAA,QACd,mBAAmB,MAAM;AAAA,QACzB,YAAY,MAAM;AAAA,QAClB,cAAc,MAAM;AAAA,QACpB,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,IACH,EACC,QAAQ;AAAA,EACb;AAAA,EAEA,oBAAoB,OAAO,WAAW;AACpC,QAAI,OAAO,WAAW,EAAG;AACzB,UAAM,GAAG,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,QAAQ;AAAA,EACvD;AAAA,EAEA,uBAAuB,OAAO,WAAW;AACvC,QAAI,OAAO,WAAW,EAAG;AACzB,UAAM,MAAM,OAAO,IAAI,CAAC,MAAM,EAAE,EAAE;AAGlC,UAAM,aAAa,MAAM,GACtB,WAAW,QAAQ,EACnB,OAAO,IAAI,EACX,MAAM,MAAM,MAAM,GAAG,EACrB,UAAU,EACV,WAAW,EACX,QAAQ;AAEX,QAAI,WAAW,WAAW,EAAG;AAC7B,UAAM,YAAY,WAAW,IAAI,CAAC,MAAM,EAAE,EAAE;AAG5C,UAAM,GACH,YAAY,QAAQ,EACpB,IAAI,EAAE,QAAQ,aAAa,CAAC,EAC5B,MAAM,MAAM,MAAM,SAAS,EAC3B,QAAQ;AAAA,EACb;AACF;","names":[]}
|
|
@@ -56,13 +56,27 @@ const createEventCrawler = ({
|
|
|
56
56
|
await publishEventsInParallel(events);
|
|
57
57
|
};
|
|
58
58
|
const retryFailedEvents = async () => {
|
|
59
|
+
const oneMinuteAgo = new Date(Date.now() - 6e4);
|
|
59
60
|
const events = await eventQueries.getEvents({
|
|
60
|
-
filters: {
|
|
61
|
+
filters: {
|
|
62
|
+
statuses: ["to-republish", "failed-but-will-retry"],
|
|
63
|
+
occurredAt: { to: oneMinuteAgo }
|
|
64
|
+
},
|
|
61
65
|
limit: batchSize
|
|
62
66
|
});
|
|
63
67
|
if (events.length === 0) return;
|
|
64
68
|
await publishEventsInParallel(events);
|
|
65
69
|
};
|
|
70
|
+
const triggerProcessing = async () => {
|
|
71
|
+
const results = await Promise.allSettled([
|
|
72
|
+
processNewEvents(),
|
|
73
|
+
retryFailedEvents()
|
|
74
|
+
]);
|
|
75
|
+
const errors = results.filter((r) => r.status === "rejected").map((r) => r.reason);
|
|
76
|
+
if (errors.length > 0) {
|
|
77
|
+
throw new AggregateError(errors, "Event processing failed");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
66
80
|
const start = () => {
|
|
67
81
|
const scheduleProcessNewEvents = () => {
|
|
68
82
|
setTimeout(async () => {
|
|
@@ -92,6 +106,7 @@ const createEventCrawler = ({
|
|
|
92
106
|
return {
|
|
93
107
|
processNewEvents,
|
|
94
108
|
retryFailedEvents,
|
|
109
|
+
triggerProcessing,
|
|
95
110
|
start
|
|
96
111
|
};
|
|
97
112
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/createEventCrawler.ts"],"sourcesContent":["import type { EventBus } from \"./ports/EventBus.ts\";\nimport type { EventQueries } from \"./ports/EventQueries.ts\";\nimport type { WithEventsUow } from \"./ports/EventRepository.ts\";\nimport type { DefaultContext, GenericEvent } from \"./types.ts\";\n\n/** Configuration options for the event crawler. */\ntype CreateEventCrawlerOptions = {\n /** Max events to fetch per batch (default: 100). */\n batchSize?: number;\n /** Max events to publish in parallel (default: 1). */\n maxParallelProcessing?: number;\n /** Interval for processing new events in ms (default: 10000). */\n newEventsIntervalMs?: number;\n /** Interval for retrying failed events in ms (default: 60000). */\n failedEventsIntervalMs?: number;\n};\n\nconst splitIntoChunks = <T>(array: T[], chunkSize: number): T[][] => {\n const chunks: T[][] = [];\n for (let i = 0; i < array.length; i += chunkSize) {\n chunks.push(array.slice(i, i + chunkSize));\n }\n return chunks;\n};\n\n/**\n * Creates a background event crawler that processes and publishes events.\n *\n * The crawler runs two loops:\n * 1. Process new events: polls for \"never-published\" events and publishes them\n * 2. Retry failed events: polls for failed events and retries them\n *\n * @returns Object with:\n * - `start()`: Start the background polling loops\n * - `processNewEvents()`: Manually trigger new event processing\n * - `retryFailedEvents()`: Manually trigger failed event retry\n *\n * @example\n * ```typescript\n * const crawler = createEventCrawler({\n * withUow,\n * eventQueries,\n * eventBus,\n * options: { batchSize: 50, newEventsIntervalMs: 5000 },\n * });\n *\n * // Start background processing\n * crawler.start();\n *\n * // Or trigger manually (useful for testing)\n * await crawler.processNewEvents();\n * ```\n */\nexport const createEventCrawler = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>({\n withUow,\n eventQueries,\n eventBus,\n options = {},\n}: {\n withUow: WithEventsUow<Event>;\n eventQueries: EventQueries<Event>;\n eventBus: EventBus<Event>;\n options?: CreateEventCrawlerOptions;\n}) => {\n const batchSize = options.batchSize ?? 100;\n const maxParallelProcessing = options.maxParallelProcessing ?? 1;\n const newEventsIntervalMs = options.newEventsIntervalMs ?? 10_000;\n const failedEventsIntervalMs = options.failedEventsIntervalMs ?? 60_000;\n\n const publishEventsInParallel = async (events: Event[]) => {\n const eventChunks = splitIntoChunks(events, maxParallelProcessing);\n for (const chunk of eventChunks) {\n await Promise.all(chunk.map((event) => eventBus.publish(event)));\n }\n };\n\n const processNewEvents = async (): Promise<void> => {\n const events = await eventQueries.getEvents({\n filters: { statuses: [\"never-published\"] },\n limit: batchSize,\n });\n\n if (events.length === 0) return;\n\n await withUow(async (uow) => {\n await uow.eventRepository.markEventsAsInProcess(events);\n });\n\n await publishEventsInParallel(events);\n };\n\n const retryFailedEvents = async (): Promise<void> => {\n const events = await eventQueries.getEvents({\n filters: {
|
|
1
|
+
{"version":3,"sources":["../src/createEventCrawler.ts"],"sourcesContent":["import type { EventBus } from \"./ports/EventBus.ts\";\nimport type { EventQueries } from \"./ports/EventQueries.ts\";\nimport type { WithEventsUow } from \"./ports/EventRepository.ts\";\nimport type { DefaultContext, GenericEvent } from \"./types.ts\";\n\n/** Configuration options for the event crawler. */\ntype CreateEventCrawlerOptions = {\n /** Max events to fetch per batch (default: 100). */\n batchSize?: number;\n /** Max events to publish in parallel (default: 1). */\n maxParallelProcessing?: number;\n /** Interval for processing new events in ms (default: 10000). */\n newEventsIntervalMs?: number;\n /** Interval for retrying failed events in ms (default: 60000). */\n failedEventsIntervalMs?: number;\n};\n\nconst splitIntoChunks = <T>(array: T[], chunkSize: number): T[][] => {\n const chunks: T[][] = [];\n for (let i = 0; i < array.length; i += chunkSize) {\n chunks.push(array.slice(i, i + chunkSize));\n }\n return chunks;\n};\n\n/**\n * Creates a background event crawler that processes and publishes events.\n *\n * The crawler runs two loops:\n * 1. Process new events: polls for \"never-published\" events and publishes them\n * 2. Retry failed events: polls for failed events and retries them\n *\n * @returns Object with:\n * - `start()`: Start the background polling loops (for traditional server environments)\n * - `processNewEvents()`: Manually trigger new event processing\n * - `retryFailedEvents()`: Manually trigger failed event retry\n * - `triggerProcessing()`: Process both new and failed events (for serverless environments)\n *\n * @example\n * ```typescript\n * const crawler = createEventCrawler({\n * withUow,\n * eventQueries,\n * eventBus,\n * options: { batchSize: 50, newEventsIntervalMs: 5000 },\n * });\n *\n * // Traditional server mode: Start background processing\n * crawler.start();\n *\n * // Serverless mode: Trigger on-demand after saving events\n * await withUow(async (uow) => {\n * await uow.eventRepository.save(event);\n * }, {\n * afterCommit: () => {\n * crawler.triggerProcessing().catch(console.error);\n * }\n * });\n *\n * // Or trigger manually (useful for testing)\n * await crawler.processNewEvents();\n * ```\n */\nexport const createEventCrawler = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>({\n withUow,\n eventQueries,\n eventBus,\n options = {},\n}: {\n withUow: WithEventsUow<Event>;\n eventQueries: EventQueries<Event>;\n eventBus: EventBus<Event>;\n options?: CreateEventCrawlerOptions;\n}) => {\n const batchSize = options.batchSize ?? 100;\n const maxParallelProcessing = options.maxParallelProcessing ?? 1;\n const newEventsIntervalMs = options.newEventsIntervalMs ?? 10_000;\n const failedEventsIntervalMs = options.failedEventsIntervalMs ?? 60_000;\n\n const publishEventsInParallel = async (events: Event[]) => {\n const eventChunks = splitIntoChunks(events, maxParallelProcessing);\n for (const chunk of eventChunks) {\n await Promise.all(chunk.map((event) => eventBus.publish(event)));\n }\n };\n\n const processNewEvents = async (): Promise<void> => {\n const events = await eventQueries.getEvents({\n filters: { statuses: [\"never-published\"] },\n limit: batchSize,\n });\n\n if (events.length === 0) return;\n\n await withUow(async (uow) => {\n await uow.eventRepository.markEventsAsInProcess(events);\n });\n\n await publishEventsInParallel(events);\n };\n\n const retryFailedEvents = async (): Promise<void> => {\n const oneMinuteAgo = new Date(Date.now() - 60_000);\n\n const events = await eventQueries.getEvents({\n filters: {\n statuses: [\"to-republish\", \"failed-but-will-retry\"],\n occurredAt: { to: oneMinuteAgo },\n },\n limit: batchSize,\n });\n\n if (events.length === 0) return;\n\n await publishEventsInParallel(events);\n };\n\n const triggerProcessing = async (): Promise<void> => {\n // Use Promise.allSettled to ensure both processing steps run independently\n // If processNewEvents fails, retryFailedEvents will still execute\n const results = await Promise.allSettled([\n processNewEvents(),\n retryFailedEvents(),\n ]);\n\n // Re-throw if both failed\n const errors = results\n .filter((r) => r.status === \"rejected\")\n .map((r) => (r as PromiseRejectedResult).reason);\n\n if (errors.length > 0) {\n throw new AggregateError(errors, \"Event processing failed\");\n }\n };\n\n const start = () => {\n const scheduleProcessNewEvents = () => {\n setTimeout(async () => {\n try {\n await processNewEvents();\n } catch (error) {\n console.error(\"Error processing new events:\", error);\n } finally {\n scheduleProcessNewEvents();\n }\n }, newEventsIntervalMs);\n };\n\n const scheduleRetryFailedEvents = () => {\n setTimeout(async () => {\n try {\n await retryFailedEvents();\n } catch (error) {\n console.error(\"Error retrying failed events:\", error);\n } finally {\n scheduleRetryFailedEvents();\n }\n }, failedEventsIntervalMs);\n };\n\n scheduleProcessNewEvents();\n scheduleRetryFailedEvents();\n };\n\n return {\n processNewEvents,\n retryFailedEvents,\n triggerProcessing,\n start,\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,MAAM,kBAAkB,CAAI,OAAY,cAA6B;AACnE,QAAM,SAAgB,CAAC;AACvB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,WAAW;AAChD,WAAO,KAAK,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;AAAA,EAC3C;AACA,SAAO;AACT;AAwCO,MAAM,qBAAqB,CAEhC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU,CAAC;AACb,MAKM;AACJ,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,wBAAwB,QAAQ,yBAAyB;AAC/D,QAAM,sBAAsB,QAAQ,uBAAuB;AAC3D,QAAM,yBAAyB,QAAQ,0BAA0B;AAEjE,QAAM,0BAA0B,OAAO,WAAoB;AACzD,UAAM,cAAc,gBAAgB,QAAQ,qBAAqB;AACjE,eAAW,SAAS,aAAa;AAC/B,YAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,UAAU,SAAS,QAAQ,KAAK,CAAC,CAAC;AAAA,IACjE;AAAA,EACF;AAEA,QAAM,mBAAmB,YAA2B;AAClD,UAAM,SAAS,MAAM,aAAa,UAAU;AAAA,MAC1C,SAAS,EAAE,UAAU,CAAC,iBAAiB,EAAE;AAAA,MACzC,OAAO;AAAA,IACT,CAAC;AAED,QAAI,OAAO,WAAW,EAAG;AAEzB,UAAM,QAAQ,OAAO,QAAQ;AAC3B,YAAM,IAAI,gBAAgB,sBAAsB,MAAM;AAAA,IACxD,CAAC;AAED,UAAM,wBAAwB,MAAM;AAAA,EACtC;AAEA,QAAM,oBAAoB,YAA2B;AACnD,UAAM,eAAe,IAAI,KAAK,KAAK,IAAI,IAAI,GAAM;AAEjD,UAAM,SAAS,MAAM,aAAa,UAAU;AAAA,MAC1C,SAAS;AAAA,QACP,UAAU,CAAC,gBAAgB,uBAAuB;AAAA,QAClD,YAAY,EAAE,IAAI,aAAa;AAAA,MACjC;AAAA,MACA,OAAO;AAAA,IACT,CAAC;AAED,QAAI,OAAO,WAAW,EAAG;AAEzB,UAAM,wBAAwB,MAAM;AAAA,EACtC;AAEA,QAAM,oBAAoB,YAA2B;AAGnD,UAAM,UAAU,MAAM,QAAQ,WAAW;AAAA,MACvC,iBAAiB;AAAA,MACjB,kBAAkB;AAAA,IACpB,CAAC;AAGD,UAAM,SAAS,QACZ,OAAO,CAAC,MAAM,EAAE,WAAW,UAAU,EACrC,IAAI,CAAC,MAAO,EAA4B,MAAM;AAEjD,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,IAAI,eAAe,QAAQ,yBAAyB;AAAA,IAC5D;AAAA,EACF;AAEA,QAAM,QAAQ,MAAM;AAClB,UAAM,2BAA2B,MAAM;AACrC,iBAAW,YAAY;AACrB,YAAI;AACF,gBAAM,iBAAiB;AAAA,QACzB,SAAS,OAAO;AACd,kBAAQ,MAAM,gCAAgC,KAAK;AAAA,QACrD,UAAE;AACA,mCAAyB;AAAA,QAC3B;AAAA,MACF,GAAG,mBAAmB;AAAA,IACxB;AAEA,UAAM,4BAA4B,MAAM;AACtC,iBAAW,YAAY;AACrB,YAAI;AACF,gBAAM,kBAAkB;AAAA,QAC1B,SAAS,OAAO;AACd,kBAAQ,MAAM,iCAAiC,KAAK;AAAA,QACtD,UAAE;AACA,oCAA0B;AAAA,QAC5B;AAAA,MACF,GAAG,sBAAsB;AAAA,IAC3B;AAEA,6BAAyB;AACzB,8BAA0B;AAAA,EAC5B;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
@@ -22,9 +22,10 @@ type CreateEventCrawlerOptions = {
|
|
|
22
22
|
* 2. Retry failed events: polls for failed events and retries them
|
|
23
23
|
*
|
|
24
24
|
* @returns Object with:
|
|
25
|
-
* - `start()`: Start the background polling loops
|
|
25
|
+
* - `start()`: Start the background polling loops (for traditional server environments)
|
|
26
26
|
* - `processNewEvents()`: Manually trigger new event processing
|
|
27
27
|
* - `retryFailedEvents()`: Manually trigger failed event retry
|
|
28
|
+
* - `triggerProcessing()`: Process both new and failed events (for serverless environments)
|
|
28
29
|
*
|
|
29
30
|
* @example
|
|
30
31
|
* ```typescript
|
|
@@ -35,9 +36,18 @@ type CreateEventCrawlerOptions = {
|
|
|
35
36
|
* options: { batchSize: 50, newEventsIntervalMs: 5000 },
|
|
36
37
|
* });
|
|
37
38
|
*
|
|
38
|
-
* // Start background processing
|
|
39
|
+
* // Traditional server mode: Start background processing
|
|
39
40
|
* crawler.start();
|
|
40
41
|
*
|
|
42
|
+
* // Serverless mode: Trigger on-demand after saving events
|
|
43
|
+
* await withUow(async (uow) => {
|
|
44
|
+
* await uow.eventRepository.save(event);
|
|
45
|
+
* }, {
|
|
46
|
+
* afterCommit: () => {
|
|
47
|
+
* crawler.triggerProcessing().catch(console.error);
|
|
48
|
+
* }
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
41
51
|
* // Or trigger manually (useful for testing)
|
|
42
52
|
* await crawler.processNewEvents();
|
|
43
53
|
* ```
|
|
@@ -50,6 +60,7 @@ declare const createEventCrawler: <Event extends GenericEvent<string, unknown, D
|
|
|
50
60
|
}) => {
|
|
51
61
|
processNewEvents: () => Promise<void>;
|
|
52
62
|
retryFailedEvents: () => Promise<void>;
|
|
63
|
+
triggerProcessing: () => Promise<void>;
|
|
53
64
|
start: () => void;
|
|
54
65
|
};
|
|
55
66
|
|
|
@@ -22,9 +22,10 @@ type CreateEventCrawlerOptions = {
|
|
|
22
22
|
* 2. Retry failed events: polls for failed events and retries them
|
|
23
23
|
*
|
|
24
24
|
* @returns Object with:
|
|
25
|
-
* - `start()`: Start the background polling loops
|
|
25
|
+
* - `start()`: Start the background polling loops (for traditional server environments)
|
|
26
26
|
* - `processNewEvents()`: Manually trigger new event processing
|
|
27
27
|
* - `retryFailedEvents()`: Manually trigger failed event retry
|
|
28
|
+
* - `triggerProcessing()`: Process both new and failed events (for serverless environments)
|
|
28
29
|
*
|
|
29
30
|
* @example
|
|
30
31
|
* ```typescript
|
|
@@ -35,9 +36,18 @@ type CreateEventCrawlerOptions = {
|
|
|
35
36
|
* options: { batchSize: 50, newEventsIntervalMs: 5000 },
|
|
36
37
|
* });
|
|
37
38
|
*
|
|
38
|
-
* // Start background processing
|
|
39
|
+
* // Traditional server mode: Start background processing
|
|
39
40
|
* crawler.start();
|
|
40
41
|
*
|
|
42
|
+
* // Serverless mode: Trigger on-demand after saving events
|
|
43
|
+
* await withUow(async (uow) => {
|
|
44
|
+
* await uow.eventRepository.save(event);
|
|
45
|
+
* }, {
|
|
46
|
+
* afterCommit: () => {
|
|
47
|
+
* crawler.triggerProcessing().catch(console.error);
|
|
48
|
+
* }
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
41
51
|
* // Or trigger manually (useful for testing)
|
|
42
52
|
* await crawler.processNewEvents();
|
|
43
53
|
* ```
|
|
@@ -50,6 +60,7 @@ declare const createEventCrawler: <Event extends GenericEvent<string, unknown, D
|
|
|
50
60
|
}) => {
|
|
51
61
|
processNewEvents: () => Promise<void>;
|
|
52
62
|
retryFailedEvents: () => Promise<void>;
|
|
63
|
+
triggerProcessing: () => Promise<void>;
|
|
53
64
|
start: () => void;
|
|
54
65
|
};
|
|
55
66
|
|
|
@@ -33,13 +33,27 @@ const createEventCrawler = ({
|
|
|
33
33
|
await publishEventsInParallel(events);
|
|
34
34
|
};
|
|
35
35
|
const retryFailedEvents = async () => {
|
|
36
|
+
const oneMinuteAgo = new Date(Date.now() - 6e4);
|
|
36
37
|
const events = await eventQueries.getEvents({
|
|
37
|
-
filters: {
|
|
38
|
+
filters: {
|
|
39
|
+
statuses: ["to-republish", "failed-but-will-retry"],
|
|
40
|
+
occurredAt: { to: oneMinuteAgo }
|
|
41
|
+
},
|
|
38
42
|
limit: batchSize
|
|
39
43
|
});
|
|
40
44
|
if (events.length === 0) return;
|
|
41
45
|
await publishEventsInParallel(events);
|
|
42
46
|
};
|
|
47
|
+
const triggerProcessing = async () => {
|
|
48
|
+
const results = await Promise.allSettled([
|
|
49
|
+
processNewEvents(),
|
|
50
|
+
retryFailedEvents()
|
|
51
|
+
]);
|
|
52
|
+
const errors = results.filter((r) => r.status === "rejected").map((r) => r.reason);
|
|
53
|
+
if (errors.length > 0) {
|
|
54
|
+
throw new AggregateError(errors, "Event processing failed");
|
|
55
|
+
}
|
|
56
|
+
};
|
|
43
57
|
const start = () => {
|
|
44
58
|
const scheduleProcessNewEvents = () => {
|
|
45
59
|
setTimeout(async () => {
|
|
@@ -69,6 +83,7 @@ const createEventCrawler = ({
|
|
|
69
83
|
return {
|
|
70
84
|
processNewEvents,
|
|
71
85
|
retryFailedEvents,
|
|
86
|
+
triggerProcessing,
|
|
72
87
|
start
|
|
73
88
|
};
|
|
74
89
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/createEventCrawler.ts"],"sourcesContent":["import type { EventBus } from './ports/EventBus.ts.mjs';\nimport type { EventQueries } from './ports/EventQueries.ts.mjs';\nimport type { WithEventsUow } from './ports/EventRepository.ts.mjs';\nimport type { DefaultContext, GenericEvent } from './types.ts.mjs';\n\n/** Configuration options for the event crawler. */\ntype CreateEventCrawlerOptions = {\n /** Max events to fetch per batch (default: 100). */\n batchSize?: number;\n /** Max events to publish in parallel (default: 1). */\n maxParallelProcessing?: number;\n /** Interval for processing new events in ms (default: 10000). */\n newEventsIntervalMs?: number;\n /** Interval for retrying failed events in ms (default: 60000). */\n failedEventsIntervalMs?: number;\n};\n\nconst splitIntoChunks = <T>(array: T[], chunkSize: number): T[][] => {\n const chunks: T[][] = [];\n for (let i = 0; i < array.length; i += chunkSize) {\n chunks.push(array.slice(i, i + chunkSize));\n }\n return chunks;\n};\n\n/**\n * Creates a background event crawler that processes and publishes events.\n *\n * The crawler runs two loops:\n * 1. Process new events: polls for \"never-published\" events and publishes them\n * 2. Retry failed events: polls for failed events and retries them\n *\n * @returns Object with:\n * - `start()`: Start the background polling loops\n * - `processNewEvents()`: Manually trigger new event processing\n * - `retryFailedEvents()`: Manually trigger failed event retry\n *\n * @example\n * ```typescript\n * const crawler = createEventCrawler({\n * withUow,\n * eventQueries,\n * eventBus,\n * options: { batchSize: 50, newEventsIntervalMs: 5000 },\n * });\n *\n * // Start background processing\n * crawler.start();\n *\n * // Or trigger manually (useful for testing)\n * await crawler.processNewEvents();\n * ```\n */\nexport const createEventCrawler = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>({\n withUow,\n eventQueries,\n eventBus,\n options = {},\n}: {\n withUow: WithEventsUow<Event>;\n eventQueries: EventQueries<Event>;\n eventBus: EventBus<Event>;\n options?: CreateEventCrawlerOptions;\n}) => {\n const batchSize = options.batchSize ?? 100;\n const maxParallelProcessing = options.maxParallelProcessing ?? 1;\n const newEventsIntervalMs = options.newEventsIntervalMs ?? 10_000;\n const failedEventsIntervalMs = options.failedEventsIntervalMs ?? 60_000;\n\n const publishEventsInParallel = async (events: Event[]) => {\n const eventChunks = splitIntoChunks(events, maxParallelProcessing);\n for (const chunk of eventChunks) {\n await Promise.all(chunk.map((event) => eventBus.publish(event)));\n }\n };\n\n const processNewEvents = async (): Promise<void> => {\n const events = await eventQueries.getEvents({\n filters: { statuses: [\"never-published\"] },\n limit: batchSize,\n });\n\n if (events.length === 0) return;\n\n await withUow(async (uow) => {\n await uow.eventRepository.markEventsAsInProcess(events);\n });\n\n await publishEventsInParallel(events);\n };\n\n const retryFailedEvents = async (): Promise<void> => {\n const events = await eventQueries.getEvents({\n filters: {
|
|
1
|
+
{"version":3,"sources":["../src/createEventCrawler.ts"],"sourcesContent":["import type { EventBus } from './ports/EventBus.ts.mjs';\nimport type { EventQueries } from './ports/EventQueries.ts.mjs';\nimport type { WithEventsUow } from './ports/EventRepository.ts.mjs';\nimport type { DefaultContext, GenericEvent } from './types.ts.mjs';\n\n/** Configuration options for the event crawler. */\ntype CreateEventCrawlerOptions = {\n /** Max events to fetch per batch (default: 100). */\n batchSize?: number;\n /** Max events to publish in parallel (default: 1). */\n maxParallelProcessing?: number;\n /** Interval for processing new events in ms (default: 10000). */\n newEventsIntervalMs?: number;\n /** Interval for retrying failed events in ms (default: 60000). */\n failedEventsIntervalMs?: number;\n};\n\nconst splitIntoChunks = <T>(array: T[], chunkSize: number): T[][] => {\n const chunks: T[][] = [];\n for (let i = 0; i < array.length; i += chunkSize) {\n chunks.push(array.slice(i, i + chunkSize));\n }\n return chunks;\n};\n\n/**\n * Creates a background event crawler that processes and publishes events.\n *\n * The crawler runs two loops:\n * 1. Process new events: polls for \"never-published\" events and publishes them\n * 2. Retry failed events: polls for failed events and retries them\n *\n * @returns Object with:\n * - `start()`: Start the background polling loops (for traditional server environments)\n * - `processNewEvents()`: Manually trigger new event processing\n * - `retryFailedEvents()`: Manually trigger failed event retry\n * - `triggerProcessing()`: Process both new and failed events (for serverless environments)\n *\n * @example\n * ```typescript\n * const crawler = createEventCrawler({\n * withUow,\n * eventQueries,\n * eventBus,\n * options: { batchSize: 50, newEventsIntervalMs: 5000 },\n * });\n *\n * // Traditional server mode: Start background processing\n * crawler.start();\n *\n * // Serverless mode: Trigger on-demand after saving events\n * await withUow(async (uow) => {\n * await uow.eventRepository.save(event);\n * }, {\n * afterCommit: () => {\n * crawler.triggerProcessing().catch(console.error);\n * }\n * });\n *\n * // Or trigger manually (useful for testing)\n * await crawler.processNewEvents();\n * ```\n */\nexport const createEventCrawler = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>({\n withUow,\n eventQueries,\n eventBus,\n options = {},\n}: {\n withUow: WithEventsUow<Event>;\n eventQueries: EventQueries<Event>;\n eventBus: EventBus<Event>;\n options?: CreateEventCrawlerOptions;\n}) => {\n const batchSize = options.batchSize ?? 100;\n const maxParallelProcessing = options.maxParallelProcessing ?? 1;\n const newEventsIntervalMs = options.newEventsIntervalMs ?? 10_000;\n const failedEventsIntervalMs = options.failedEventsIntervalMs ?? 60_000;\n\n const publishEventsInParallel = async (events: Event[]) => {\n const eventChunks = splitIntoChunks(events, maxParallelProcessing);\n for (const chunk of eventChunks) {\n await Promise.all(chunk.map((event) => eventBus.publish(event)));\n }\n };\n\n const processNewEvents = async (): Promise<void> => {\n const events = await eventQueries.getEvents({\n filters: { statuses: [\"never-published\"] },\n limit: batchSize,\n });\n\n if (events.length === 0) return;\n\n await withUow(async (uow) => {\n await uow.eventRepository.markEventsAsInProcess(events);\n });\n\n await publishEventsInParallel(events);\n };\n\n const retryFailedEvents = async (): Promise<void> => {\n const oneMinuteAgo = new Date(Date.now() - 60_000);\n\n const events = await eventQueries.getEvents({\n filters: {\n statuses: [\"to-republish\", \"failed-but-will-retry\"],\n occurredAt: { to: oneMinuteAgo },\n },\n limit: batchSize,\n });\n\n if (events.length === 0) return;\n\n await publishEventsInParallel(events);\n };\n\n const triggerProcessing = async (): Promise<void> => {\n // Use Promise.allSettled to ensure both processing steps run independently\n // If processNewEvents fails, retryFailedEvents will still execute\n const results = await Promise.allSettled([\n processNewEvents(),\n retryFailedEvents(),\n ]);\n\n // Re-throw if both failed\n const errors = results\n .filter((r) => r.status === \"rejected\")\n .map((r) => (r as PromiseRejectedResult).reason);\n\n if (errors.length > 0) {\n throw new AggregateError(errors, \"Event processing failed\");\n }\n };\n\n const start = () => {\n const scheduleProcessNewEvents = () => {\n setTimeout(async () => {\n try {\n await processNewEvents();\n } catch (error) {\n console.error(\"Error processing new events:\", error);\n } finally {\n scheduleProcessNewEvents();\n }\n }, newEventsIntervalMs);\n };\n\n const scheduleRetryFailedEvents = () => {\n setTimeout(async () => {\n try {\n await retryFailedEvents();\n } catch (error) {\n console.error(\"Error retrying failed events:\", error);\n } finally {\n scheduleRetryFailedEvents();\n }\n }, failedEventsIntervalMs);\n };\n\n scheduleProcessNewEvents();\n scheduleRetryFailedEvents();\n };\n\n return {\n processNewEvents,\n retryFailedEvents,\n triggerProcessing,\n start,\n };\n};\n"],"mappings":"AAiBA,MAAM,kBAAkB,CAAI,OAAY,cAA6B;AACnE,QAAM,SAAgB,CAAC;AACvB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,WAAW;AAChD,WAAO,KAAK,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;AAAA,EAC3C;AACA,SAAO;AACT;AAwCO,MAAM,qBAAqB,CAEhC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU,CAAC;AACb,MAKM;AACJ,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,wBAAwB,QAAQ,yBAAyB;AAC/D,QAAM,sBAAsB,QAAQ,uBAAuB;AAC3D,QAAM,yBAAyB,QAAQ,0BAA0B;AAEjE,QAAM,0BAA0B,OAAO,WAAoB;AACzD,UAAM,cAAc,gBAAgB,QAAQ,qBAAqB;AACjE,eAAW,SAAS,aAAa;AAC/B,YAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,UAAU,SAAS,QAAQ,KAAK,CAAC,CAAC;AAAA,IACjE;AAAA,EACF;AAEA,QAAM,mBAAmB,YAA2B;AAClD,UAAM,SAAS,MAAM,aAAa,UAAU;AAAA,MAC1C,SAAS,EAAE,UAAU,CAAC,iBAAiB,EAAE;AAAA,MACzC,OAAO;AAAA,IACT,CAAC;AAED,QAAI,OAAO,WAAW,EAAG;AAEzB,UAAM,QAAQ,OAAO,QAAQ;AAC3B,YAAM,IAAI,gBAAgB,sBAAsB,MAAM;AAAA,IACxD,CAAC;AAED,UAAM,wBAAwB,MAAM;AAAA,EACtC;AAEA,QAAM,oBAAoB,YAA2B;AACnD,UAAM,eAAe,IAAI,KAAK,KAAK,IAAI,IAAI,GAAM;AAEjD,UAAM,SAAS,MAAM,aAAa,UAAU;AAAA,MAC1C,SAAS;AAAA,QACP,UAAU,CAAC,gBAAgB,uBAAuB;AAAA,QAClD,YAAY,EAAE,IAAI,aAAa;AAAA,MACjC;AAAA,MACA,OAAO;AAAA,IACT,CAAC;AAED,QAAI,OAAO,WAAW,EAAG;AAEzB,UAAM,wBAAwB,MAAM;AAAA,EACtC;AAEA,QAAM,oBAAoB,YAA2B;AAGnD,UAAM,UAAU,MAAM,QAAQ,WAAW;AAAA,MACvC,iBAAiB;AAAA,MACjB,kBAAkB;AAAA,IACpB,CAAC;AAGD,UAAM,SAAS,QACZ,OAAO,CAAC,MAAM,EAAE,WAAW,UAAU,EACrC,IAAI,CAAC,MAAO,EAA4B,MAAM;AAEjD,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,IAAI,eAAe,QAAQ,yBAAyB;AAAA,IAC5D;AAAA,EACF;AAEA,QAAM,QAAQ,MAAM;AAClB,UAAM,2BAA2B,MAAM;AACrC,iBAAW,YAAY;AACrB,YAAI;AACF,gBAAM,iBAAiB;AAAA,QACzB,SAAS,OAAO;AACd,kBAAQ,MAAM,gCAAgC,KAAK;AAAA,QACrD,UAAE;AACA,mCAAyB;AAAA,QAC3B;AAAA,MACF,GAAG,mBAAmB;AAAA,IACxB;AAEA,UAAM,4BAA4B,MAAM;AACtC,iBAAW,YAAY;AACrB,YAAI;AACF,gBAAM,kBAAkB;AAAA,QAC1B,SAAS,OAAO;AACd,kBAAQ,MAAM,iCAAiC,KAAK;AAAA,QACtD,UAAE;AACA,oCAA0B;AAAA,QAC5B;AAAA,MACF,GAAG,sBAAsB;AAAA,IAC3B;AAEA,6BAAyB;AACzB,8BAA0B;AAAA,EAC5B;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -3,7 +3,7 @@ export { createEventCrawler } from './createEventCrawler.cjs';
|
|
|
3
3
|
export { CreateNewEvent, makeCreateNewEvent } from './createNewEvent.cjs';
|
|
4
4
|
export { EventBus } from './ports/EventBus.cjs';
|
|
5
5
|
export { EventQueries } from './ports/EventQueries.cjs';
|
|
6
|
-
export { EventRepository, EventsUnitOfWork, WithEventsUow } from './ports/EventRepository.cjs';
|
|
6
|
+
export { EventRepository, EventsUnitOfWork, WithEventsUow, WithEventsUowOptions } from './ports/EventRepository.cjs';
|
|
7
7
|
export { DefaultContext, EventFailure, EventId, EventPublication, EventStatus, Flavor, GenericEvent, SubscriptionId, UserId } from './types.cjs';
|
|
8
8
|
export { createInMemoryEventBus } from './adapters/in-memory/InMemoryEventBus.cjs';
|
|
9
9
|
export { createInMemoryEventQueries } from './adapters/in-memory/InMemoryEventQueries.cjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { createEventCrawler } from './createEventCrawler.js';
|
|
|
3
3
|
export { CreateNewEvent, makeCreateNewEvent } from './createNewEvent.js';
|
|
4
4
|
export { EventBus } from './ports/EventBus.js';
|
|
5
5
|
export { EventQueries } from './ports/EventQueries.js';
|
|
6
|
-
export { EventRepository, EventsUnitOfWork, WithEventsUow } from './ports/EventRepository.js';
|
|
6
|
+
export { EventRepository, EventsUnitOfWork, WithEventsUow, WithEventsUowOptions } from './ports/EventRepository.js';
|
|
7
7
|
export { DefaultContext, EventFailure, EventId, EventPublication, EventStatus, Flavor, GenericEvent, SubscriptionId, UserId } from './types.js';
|
|
8
8
|
export { createInMemoryEventBus } from './adapters/in-memory/InMemoryEventBus.js';
|
|
9
9
|
export { createInMemoryEventQueries } from './adapters/in-memory/InMemoryEventQueries.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ports/EventQueries.ts"],"sourcesContent":["import type { DefaultContext, EventStatus, GenericEvent } from \"../types.ts\";\n\n/** Parameters for querying events. */\ntype GetEventsParams = {\n filters: {\n /** Filter by event status (e.g., [\"never-published\", \"failed-but-will-retry\"]). */\n statuses: EventStatus[];\n /** Optional context filter for multi-tenant scenarios. */\n context?: Partial<Record<string, string>>;\n };\n /** Maximum number of events to return. */\n limit: number;\n};\n\n/**\n * Query interface for reading events.\n * Used by the event crawler to fetch events for processing.\n * Implement this to query events from your database.\n */\nexport type EventQueries<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n /** Fetch events matching the given filters. */\n getEvents: (params: GetEventsParams) => Promise<Event[]>;\n};\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/ports/EventQueries.ts"],"sourcesContent":["import type { DefaultContext, EventStatus, GenericEvent } from \"../types.ts\";\n\n/** Parameters for querying events. */\ntype GetEventsParams = {\n filters: {\n /** Filter by event status (e.g., [\"never-published\", \"failed-but-will-retry\"]). */\n statuses: EventStatus[];\n /** Optional context filter for multi-tenant scenarios. */\n context?: Partial<Record<string, string>>;\n /** Optional time-based filter for when events occurred. */\n occurredAt?: {\n /** Include events that occurred on or after this date. */\n from?: Date;\n /** Include events that occurred on or before this date. */\n to?: Date;\n };\n };\n /** Maximum number of events to return. */\n limit: number;\n};\n\n/**\n * Query interface for reading events.\n * Used by the event crawler to fetch events for processing.\n * Implement this to query events from your database.\n */\nexport type EventQueries<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n /** Fetch events matching the given filters. */\n getEvents: (params: GetEventsParams) => Promise<Event[]>;\n};\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
|
@@ -7,6 +7,13 @@ type GetEventsParams = {
|
|
|
7
7
|
statuses: EventStatus[];
|
|
8
8
|
/** Optional context filter for multi-tenant scenarios. */
|
|
9
9
|
context?: Partial<Record<string, string>>;
|
|
10
|
+
/** Optional time-based filter for when events occurred. */
|
|
11
|
+
occurredAt?: {
|
|
12
|
+
/** Include events that occurred on or after this date. */
|
|
13
|
+
from?: Date;
|
|
14
|
+
/** Include events that occurred on or before this date. */
|
|
15
|
+
to?: Date;
|
|
16
|
+
};
|
|
10
17
|
};
|
|
11
18
|
/** Maximum number of events to return. */
|
|
12
19
|
limit: number;
|
|
@@ -7,6 +7,13 @@ type GetEventsParams = {
|
|
|
7
7
|
statuses: EventStatus[];
|
|
8
8
|
/** Optional context filter for multi-tenant scenarios. */
|
|
9
9
|
context?: Partial<Record<string, string>>;
|
|
10
|
+
/** Optional time-based filter for when events occurred. */
|
|
11
|
+
occurredAt?: {
|
|
12
|
+
/** Include events that occurred on or after this date. */
|
|
13
|
+
from?: Date;
|
|
14
|
+
/** Include events that occurred on or before this date. */
|
|
15
|
+
to?: Date;
|
|
16
|
+
};
|
|
10
17
|
};
|
|
11
18
|
/** Maximum number of events to return. */
|
|
12
19
|
limit: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ports/EventRepository.ts"],"sourcesContent":["import type { DefaultContext, GenericEvent } from \"../types.ts\";\n\n/**\n * Repository interface for persisting events.\n * Implement this to store events in your database (e.g., PostgreSQL, MongoDB).\n * Events should be saved in the same transaction as your domain changes.\n */\nexport type EventRepository<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n /** Persist a single event (typically after publication status update). */\n save: (event: Event) => Promise<void>;\n /** Persist multiple new events in a batch. */\n saveNewEventsBatch: (events: Event[]) => Promise<void>;\n /** Mark events as \"in-process\" before publishing (prevents duplicate processing). */\n markEventsAsInProcess: (events: Event[]) => Promise<void>;\n};\n\n/**\n * Unit of work containing the event repository.\n * Extend this with your own repositories for transactional consistency.\n */\nexport type EventsUnitOfWork<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n eventRepository: EventRepository<Event>;\n};\n\n/**\n * Higher-order function that provides a unit of work for transactional operations.\n * Your implementation should handle transaction begin/commit/rollback.\n *\n * @example\n * ```typescript\n * const withUow: WithEventsUow<MyEvent> = async (fn) => {\n * const tx = await db.beginTransaction();\n * try {\n * await fn({ eventRepository: createEventRepo(tx) });\n * await tx.commit();\n * } catch (e) {\n * await tx.rollback();\n * throw e;\n * }\n * };\n * ```\n */\nexport type WithEventsUow<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = (fn: (uow: EventsUnitOfWork<Event>) => Promise<void
|
|
1
|
+
{"version":3,"sources":["../../src/ports/EventRepository.ts"],"sourcesContent":["import type { DefaultContext, GenericEvent } from \"../types.ts\";\n\n/**\n * Repository interface for persisting events.\n * Implement this to store events in your database (e.g., PostgreSQL, MongoDB).\n * Events should be saved in the same transaction as your domain changes.\n */\nexport type EventRepository<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n /** Persist a single event (typically after publication status update). */\n save: (event: Event) => Promise<void>;\n /** Persist multiple new events in a batch. */\n saveNewEventsBatch: (events: Event[]) => Promise<void>;\n /** Mark events as \"in-process\" before publishing (prevents duplicate processing). */\n markEventsAsInProcess: (events: Event[]) => Promise<void>;\n};\n\n/**\n * Unit of work containing the event repository.\n * Extend this with your own repositories for transactional consistency.\n */\nexport type EventsUnitOfWork<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n eventRepository: EventRepository<Event>;\n};\n\n/**\n * Options for unit of work execution.\n */\nexport type WithEventsUowOptions = {\n /**\n * Callback executed after successful transaction commit.\n * Useful for triggering event processing in serverless environments.\n *\n * The callback should return a Promise. Whether it's awaited depends on\n * the withUow implementation:\n * - Serverless (Lambda): await to ensure completion before runtime freezes\n * - Long-running servers: fire-and-forget for faster response times\n *\n * @example\n * ```typescript\n * await withUow(async (uow) => {\n * await uow.eventRepository.save(event);\n * }, {\n * afterCommit: async () => {\n * await eventCrawler.triggerProcessing();\n * }\n * });\n * ```\n */\n afterCommit?: () => Promise<void>;\n};\n\n/**\n * Higher-order function that provides a unit of work for transactional operations.\n * Your implementation should handle transaction begin/commit/rollback.\n *\n * @example\n * ```typescript\n * const withUow: WithEventsUow<MyEvent> = async (fn, options) => {\n * const tx = await db.beginTransaction();\n * try {\n * await fn({ eventRepository: createEventRepo(tx) });\n * await tx.commit();\n * options?.afterCommit?.();\n * } catch (e) {\n * await tx.rollback();\n * throw e;\n * }\n * };\n * ```\n */\nexport type WithEventsUow<\n Event extends GenericEvent<string, unknown, DefaultContext>,\n> = (\n fn: (uow: EventsUnitOfWork<Event>) => Promise<void>,\n options?: WithEventsUowOptions,\n) => Promise<void>;\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
|
@@ -20,17 +20,44 @@ type EventRepository<Event extends GenericEvent<string, unknown, DefaultContext>
|
|
|
20
20
|
type EventsUnitOfWork<Event extends GenericEvent<string, unknown, DefaultContext>> = {
|
|
21
21
|
eventRepository: EventRepository<Event>;
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Options for unit of work execution.
|
|
25
|
+
*/
|
|
26
|
+
type WithEventsUowOptions = {
|
|
27
|
+
/**
|
|
28
|
+
* Callback executed after successful transaction commit.
|
|
29
|
+
* Useful for triggering event processing in serverless environments.
|
|
30
|
+
*
|
|
31
|
+
* The callback should return a Promise. Whether it's awaited depends on
|
|
32
|
+
* the withUow implementation:
|
|
33
|
+
* - Serverless (Lambda): await to ensure completion before runtime freezes
|
|
34
|
+
* - Long-running servers: fire-and-forget for faster response times
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* await withUow(async (uow) => {
|
|
39
|
+
* await uow.eventRepository.save(event);
|
|
40
|
+
* }, {
|
|
41
|
+
* afterCommit: async () => {
|
|
42
|
+
* await eventCrawler.triggerProcessing();
|
|
43
|
+
* }
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
afterCommit?: () => Promise<void>;
|
|
48
|
+
};
|
|
23
49
|
/**
|
|
24
50
|
* Higher-order function that provides a unit of work for transactional operations.
|
|
25
51
|
* Your implementation should handle transaction begin/commit/rollback.
|
|
26
52
|
*
|
|
27
53
|
* @example
|
|
28
54
|
* ```typescript
|
|
29
|
-
* const withUow: WithEventsUow<MyEvent> = async (fn) => {
|
|
55
|
+
* const withUow: WithEventsUow<MyEvent> = async (fn, options) => {
|
|
30
56
|
* const tx = await db.beginTransaction();
|
|
31
57
|
* try {
|
|
32
58
|
* await fn({ eventRepository: createEventRepo(tx) });
|
|
33
59
|
* await tx.commit();
|
|
60
|
+
* options?.afterCommit?.();
|
|
34
61
|
* } catch (e) {
|
|
35
62
|
* await tx.rollback();
|
|
36
63
|
* throw e;
|
|
@@ -38,6 +65,6 @@ type EventsUnitOfWork<Event extends GenericEvent<string, unknown, DefaultContext
|
|
|
38
65
|
* };
|
|
39
66
|
* ```
|
|
40
67
|
*/
|
|
41
|
-
type WithEventsUow<Event extends GenericEvent<string, unknown, DefaultContext>> = (fn: (uow: EventsUnitOfWork<Event>) => Promise<void
|
|
68
|
+
type WithEventsUow<Event extends GenericEvent<string, unknown, DefaultContext>> = (fn: (uow: EventsUnitOfWork<Event>) => Promise<void>, options?: WithEventsUowOptions) => Promise<void>;
|
|
42
69
|
|
|
43
|
-
export type { EventRepository, EventsUnitOfWork, WithEventsUow };
|
|
70
|
+
export type { EventRepository, EventsUnitOfWork, WithEventsUow, WithEventsUowOptions };
|
|
@@ -20,17 +20,44 @@ type EventRepository<Event extends GenericEvent<string, unknown, DefaultContext>
|
|
|
20
20
|
type EventsUnitOfWork<Event extends GenericEvent<string, unknown, DefaultContext>> = {
|
|
21
21
|
eventRepository: EventRepository<Event>;
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Options for unit of work execution.
|
|
25
|
+
*/
|
|
26
|
+
type WithEventsUowOptions = {
|
|
27
|
+
/**
|
|
28
|
+
* Callback executed after successful transaction commit.
|
|
29
|
+
* Useful for triggering event processing in serverless environments.
|
|
30
|
+
*
|
|
31
|
+
* The callback should return a Promise. Whether it's awaited depends on
|
|
32
|
+
* the withUow implementation:
|
|
33
|
+
* - Serverless (Lambda): await to ensure completion before runtime freezes
|
|
34
|
+
* - Long-running servers: fire-and-forget for faster response times
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* await withUow(async (uow) => {
|
|
39
|
+
* await uow.eventRepository.save(event);
|
|
40
|
+
* }, {
|
|
41
|
+
* afterCommit: async () => {
|
|
42
|
+
* await eventCrawler.triggerProcessing();
|
|
43
|
+
* }
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
afterCommit?: () => Promise<void>;
|
|
48
|
+
};
|
|
23
49
|
/**
|
|
24
50
|
* Higher-order function that provides a unit of work for transactional operations.
|
|
25
51
|
* Your implementation should handle transaction begin/commit/rollback.
|
|
26
52
|
*
|
|
27
53
|
* @example
|
|
28
54
|
* ```typescript
|
|
29
|
-
* const withUow: WithEventsUow<MyEvent> = async (fn) => {
|
|
55
|
+
* const withUow: WithEventsUow<MyEvent> = async (fn, options) => {
|
|
30
56
|
* const tx = await db.beginTransaction();
|
|
31
57
|
* try {
|
|
32
58
|
* await fn({ eventRepository: createEventRepo(tx) });
|
|
33
59
|
* await tx.commit();
|
|
60
|
+
* options?.afterCommit?.();
|
|
34
61
|
* } catch (e) {
|
|
35
62
|
* await tx.rollback();
|
|
36
63
|
* throw e;
|
|
@@ -38,6 +65,6 @@ type EventsUnitOfWork<Event extends GenericEvent<string, unknown, DefaultContext
|
|
|
38
65
|
* };
|
|
39
66
|
* ```
|
|
40
67
|
*/
|
|
41
|
-
type WithEventsUow<Event extends GenericEvent<string, unknown, DefaultContext>> = (fn: (uow: EventsUnitOfWork<Event>) => Promise<void
|
|
68
|
+
type WithEventsUow<Event extends GenericEvent<string, unknown, DefaultContext>> = (fn: (uow: EventsUnitOfWork<Event>) => Promise<void>, options?: WithEventsUowOptions) => Promise<void>;
|
|
42
69
|
|
|
43
|
-
export type { EventRepository, EventsUnitOfWork, WithEventsUow };
|
|
70
|
+
export type { EventRepository, EventsUnitOfWork, WithEventsUow, WithEventsUowOptions };
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@l-etabli/events",
|
|
3
|
-
"description": "The purpose of this
|
|
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.3.0",
|
|
7
7
|
"main": "./dist/index.mjs",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"files": [
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
"check": "biome check",
|
|
29
29
|
"check:fix": "biome check --fix --no-errors-on-unmatched --files-ignore-unknown=true",
|
|
30
30
|
"test": "bun test",
|
|
31
|
+
"test:integration": "bun test .integration.test.ts",
|
|
31
32
|
"typecheck": "tsc --noEmit",
|
|
32
|
-
"fullcheck": "bun run check:fix && bun run typecheck && bun test --bail tests
|
|
33
|
+
"fullcheck": "bun run check:fix && bun run typecheck && bun test --bail $(find tests -name '*.test.ts' ! -name '*.integration.test.ts')",
|
|
33
34
|
"release": "semantic-release"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
@@ -18,11 +18,25 @@ export const createInMemoryEventQueries = <
|
|
|
18
18
|
);
|
|
19
19
|
};
|
|
20
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
|
+
|
|
21
33
|
return helpers
|
|
22
34
|
.getAllEvents()
|
|
23
35
|
.filter(
|
|
24
36
|
(event) =>
|
|
25
|
-
filters.statuses.includes(event.status) &&
|
|
37
|
+
filters.statuses.includes(event.status) &&
|
|
38
|
+
matchesContext(event) &&
|
|
39
|
+
matchesOccurredAt(event),
|
|
26
40
|
)
|
|
27
41
|
.slice(0, limit);
|
|
28
42
|
},
|
|
@@ -54,8 +54,10 @@ export const createInMemoryWithUow = <
|
|
|
54
54
|
>(
|
|
55
55
|
eventRepository: EventRepository<Event>,
|
|
56
56
|
): { withUow: WithEventsUow<Event> } => {
|
|
57
|
-
|
|
57
|
+
// In-memory adapter awaits afterCommit for predictable test behavior
|
|
58
|
+
const withUow: WithEventsUow<Event> = async (fn, options) => {
|
|
58
59
|
await fn({ eventRepository });
|
|
60
|
+
await options?.afterCommit?.();
|
|
59
61
|
};
|
|
60
62
|
return { withUow };
|
|
61
63
|
};
|
|
@@ -22,9 +22,17 @@ export const createKyselyEventQueries = <
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
if (filters.occurredAt?.from) {
|
|
26
|
+
query = query.where("occurredAt", ">=", filters.occurredAt.from);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (filters.occurredAt?.to) {
|
|
30
|
+
query = query.where("occurredAt", "<=", filters.occurredAt.to);
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
const rows = await query.execute();
|
|
26
34
|
return rows.map(
|
|
27
|
-
(row) =>
|
|
35
|
+
(row: EventsTable["events"]) =>
|
|
28
36
|
({
|
|
29
37
|
...row,
|
|
30
38
|
context: row.context ?? undefined,
|
|
@@ -35,10 +35,24 @@ export const createKyselyEventRepository = <
|
|
|
35
35
|
markEventsAsInProcess: async (events) => {
|
|
36
36
|
if (events.length === 0) return;
|
|
37
37
|
const ids = events.map((e) => e.id);
|
|
38
|
+
|
|
39
|
+
// Lock the rows to prevent concurrent processing
|
|
40
|
+
const lockedRows = await db
|
|
41
|
+
.selectFrom("events")
|
|
42
|
+
.select("id")
|
|
43
|
+
.where("id", "in", ids)
|
|
44
|
+
.forUpdate()
|
|
45
|
+
.skipLocked()
|
|
46
|
+
.execute();
|
|
47
|
+
|
|
48
|
+
if (lockedRows.length === 0) return;
|
|
49
|
+
const lockedIds = lockedRows.map((r) => r.id);
|
|
50
|
+
|
|
51
|
+
// Update status to in-process (only for locked rows)
|
|
38
52
|
await db
|
|
39
53
|
.updateTable("events")
|
|
40
54
|
.set({ status: "in-process" })
|
|
41
|
-
.where("id", "in",
|
|
55
|
+
.where("id", "in", lockedIds)
|
|
42
56
|
.execute();
|
|
43
57
|
},
|
|
44
58
|
});
|
|
@@ -31,9 +31,10 @@ const splitIntoChunks = <T>(array: T[], chunkSize: number): T[][] => {
|
|
|
31
31
|
* 2. Retry failed events: polls for failed events and retries them
|
|
32
32
|
*
|
|
33
33
|
* @returns Object with:
|
|
34
|
-
* - `start()`: Start the background polling loops
|
|
34
|
+
* - `start()`: Start the background polling loops (for traditional server environments)
|
|
35
35
|
* - `processNewEvents()`: Manually trigger new event processing
|
|
36
36
|
* - `retryFailedEvents()`: Manually trigger failed event retry
|
|
37
|
+
* - `triggerProcessing()`: Process both new and failed events (for serverless environments)
|
|
37
38
|
*
|
|
38
39
|
* @example
|
|
39
40
|
* ```typescript
|
|
@@ -44,9 +45,18 @@ const splitIntoChunks = <T>(array: T[], chunkSize: number): T[][] => {
|
|
|
44
45
|
* options: { batchSize: 50, newEventsIntervalMs: 5000 },
|
|
45
46
|
* });
|
|
46
47
|
*
|
|
47
|
-
* // Start background processing
|
|
48
|
+
* // Traditional server mode: Start background processing
|
|
48
49
|
* crawler.start();
|
|
49
50
|
*
|
|
51
|
+
* // Serverless mode: Trigger on-demand after saving events
|
|
52
|
+
* await withUow(async (uow) => {
|
|
53
|
+
* await uow.eventRepository.save(event);
|
|
54
|
+
* }, {
|
|
55
|
+
* afterCommit: () => {
|
|
56
|
+
* crawler.triggerProcessing().catch(console.error);
|
|
57
|
+
* }
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
50
60
|
* // Or trigger manually (useful for testing)
|
|
51
61
|
* await crawler.processNewEvents();
|
|
52
62
|
* ```
|
|
@@ -92,8 +102,13 @@ export const createEventCrawler = <
|
|
|
92
102
|
};
|
|
93
103
|
|
|
94
104
|
const retryFailedEvents = async (): Promise<void> => {
|
|
105
|
+
const oneMinuteAgo = new Date(Date.now() - 60_000);
|
|
106
|
+
|
|
95
107
|
const events = await eventQueries.getEvents({
|
|
96
|
-
filters: {
|
|
108
|
+
filters: {
|
|
109
|
+
statuses: ["to-republish", "failed-but-will-retry"],
|
|
110
|
+
occurredAt: { to: oneMinuteAgo },
|
|
111
|
+
},
|
|
97
112
|
limit: batchSize,
|
|
98
113
|
});
|
|
99
114
|
|
|
@@ -102,6 +117,24 @@ export const createEventCrawler = <
|
|
|
102
117
|
await publishEventsInParallel(events);
|
|
103
118
|
};
|
|
104
119
|
|
|
120
|
+
const triggerProcessing = async (): Promise<void> => {
|
|
121
|
+
// Use Promise.allSettled to ensure both processing steps run independently
|
|
122
|
+
// If processNewEvents fails, retryFailedEvents will still execute
|
|
123
|
+
const results = await Promise.allSettled([
|
|
124
|
+
processNewEvents(),
|
|
125
|
+
retryFailedEvents(),
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
// Re-throw if both failed
|
|
129
|
+
const errors = results
|
|
130
|
+
.filter((r) => r.status === "rejected")
|
|
131
|
+
.map((r) => (r as PromiseRejectedResult).reason);
|
|
132
|
+
|
|
133
|
+
if (errors.length > 0) {
|
|
134
|
+
throw new AggregateError(errors, "Event processing failed");
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
105
138
|
const start = () => {
|
|
106
139
|
const scheduleProcessNewEvents = () => {
|
|
107
140
|
setTimeout(async () => {
|
|
@@ -134,6 +167,7 @@ export const createEventCrawler = <
|
|
|
134
167
|
return {
|
|
135
168
|
processNewEvents,
|
|
136
169
|
retryFailedEvents,
|
|
170
|
+
triggerProcessing,
|
|
137
171
|
start,
|
|
138
172
|
};
|
|
139
173
|
};
|
|
@@ -7,6 +7,13 @@ type GetEventsParams = {
|
|
|
7
7
|
statuses: EventStatus[];
|
|
8
8
|
/** Optional context filter for multi-tenant scenarios. */
|
|
9
9
|
context?: Partial<Record<string, string>>;
|
|
10
|
+
/** Optional time-based filter for when events occurred. */
|
|
11
|
+
occurredAt?: {
|
|
12
|
+
/** Include events that occurred on or after this date. */
|
|
13
|
+
from?: Date;
|
|
14
|
+
/** Include events that occurred on or before this date. */
|
|
15
|
+
to?: Date;
|
|
16
|
+
};
|
|
10
17
|
};
|
|
11
18
|
/** Maximum number of events to return. */
|
|
12
19
|
limit: number;
|
|
@@ -26,17 +26,45 @@ export type EventsUnitOfWork<
|
|
|
26
26
|
eventRepository: EventRepository<Event>;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Options for unit of work execution.
|
|
31
|
+
*/
|
|
32
|
+
export type WithEventsUowOptions = {
|
|
33
|
+
/**
|
|
34
|
+
* Callback executed after successful transaction commit.
|
|
35
|
+
* Useful for triggering event processing in serverless environments.
|
|
36
|
+
*
|
|
37
|
+
* The callback should return a Promise. Whether it's awaited depends on
|
|
38
|
+
* the withUow implementation:
|
|
39
|
+
* - Serverless (Lambda): await to ensure completion before runtime freezes
|
|
40
|
+
* - Long-running servers: fire-and-forget for faster response times
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* await withUow(async (uow) => {
|
|
45
|
+
* await uow.eventRepository.save(event);
|
|
46
|
+
* }, {
|
|
47
|
+
* afterCommit: async () => {
|
|
48
|
+
* await eventCrawler.triggerProcessing();
|
|
49
|
+
* }
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
afterCommit?: () => Promise<void>;
|
|
54
|
+
};
|
|
55
|
+
|
|
29
56
|
/**
|
|
30
57
|
* Higher-order function that provides a unit of work for transactional operations.
|
|
31
58
|
* Your implementation should handle transaction begin/commit/rollback.
|
|
32
59
|
*
|
|
33
60
|
* @example
|
|
34
61
|
* ```typescript
|
|
35
|
-
* const withUow: WithEventsUow<MyEvent> = async (fn) => {
|
|
62
|
+
* const withUow: WithEventsUow<MyEvent> = async (fn, options) => {
|
|
36
63
|
* const tx = await db.beginTransaction();
|
|
37
64
|
* try {
|
|
38
65
|
* await fn({ eventRepository: createEventRepo(tx) });
|
|
39
66
|
* await tx.commit();
|
|
67
|
+
* options?.afterCommit?.();
|
|
40
68
|
* } catch (e) {
|
|
41
69
|
* await tx.rollback();
|
|
42
70
|
* throw e;
|
|
@@ -46,4 +74,7 @@ export type EventsUnitOfWork<
|
|
|
46
74
|
*/
|
|
47
75
|
export type WithEventsUow<
|
|
48
76
|
Event extends GenericEvent<string, unknown, DefaultContext>,
|
|
49
|
-
> = (
|
|
77
|
+
> = (
|
|
78
|
+
fn: (uow: EventsUnitOfWork<Event>) => Promise<void>,
|
|
79
|
+
options?: WithEventsUowOptions,
|
|
80
|
+
) => Promise<void>;
|