@event-driven-io/emmett-postgresql 0.43.0-beta.17 → 0.43.0-beta.18

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/index.js CHANGED
@@ -1,9 +1,20 @@
1
- import { AssertionError, EmmettError, ExpectedVersionConflictError, NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS, assertDeepEqual, assertEqual, assertExpectedVersionMatchesCurrent, assertFails, assertIsNotNull, assertIsNull, assertThatArray, assertTrue, asyncAwaiter, asyncRetry, bigInt, bigIntProcessorCheckpoint, defaultProcessorPartition, defaultProcessorVersion, downcastRecordedMessages, getCheckpoint, getProcessorInstanceId, getProjectorId, getWorkflowId, hashText, isBigint, isErrorConstructor, parseBigIntProcessorCheckpoint, projection, projector, reactor, reduceAsync, unknownTag, upcastRecordedMessage, workflowProcessor } from "@event-driven-io/emmett";
2
- import { DumboError, JSONSerializer, SQL, UniqueConstraintError, dumbo, fromDatabaseDriverType, getFormatter, mapRows, runSQLMigrations, single, singleOrNull, sqlMigration } from "@event-driven-io/dumbo";
3
- import { v4, v7 } from "uuid";
1
+ import { AssertionError, EmmettError, ExpectedVersionConflictError, JSONSerializer, NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS, assertDeepEqual, assertEqual, assertExpectedVersionMatchesCurrent, assertFails, assertIsNotNull, assertIsNull, assertThatArray, assertTrue, asyncAwaiter, asyncRetry, bigInt, bigIntProcessorCheckpoint, defaultProcessorPartition, defaultProcessorVersion, downcastRecordedMessages, getCheckpoint, getProcessorInstanceId, getProjectorId, getWorkflowId, hashText, isBigint, isErrorConstructor, projection, projector, reactor, reduceAsync, unknownTag, upcastRecordedMessage, workflowProcessor } from "@event-driven-io/emmett";
2
+ import { DumboError, JSONSerializer as JSONSerializer$1, SQL, UniqueConstraintError, dumbo, fromDatabaseDriverType, getFormatter, mapRows, runSQLMigrations, single, singleOrNull, sqlMigration } from "@event-driven-io/dumbo";
4
3
  import { pongoClient } from "@event-driven-io/pongo";
5
4
  import { pgDriver } from "@event-driven-io/pongo/pg";
5
+ import { v4, v7 } from "uuid";
6
+
7
+ //#region src/eventStore/schema/createFunctionIfDoesNotExist.ts
8
+ const createFunctionIfDoesNotExistSQL = (functionName, functionDefinition) => SQL`
9
+ DO $$
10
+ BEGIN
11
+ IF NOT EXISTS (SELECT 1 FROM pg_proc WHERE proname = '${SQL.plain(functionName)}') THEN
12
+ ${functionDefinition}
13
+ END IF;
14
+ END $$;
15
+ `;
6
16
 
17
+ //#endregion
7
18
  //#region src/eventStore/schema/typing.ts
8
19
  const emmettPrefix = "emt";
9
20
  const globalTag = "global";
@@ -34,145 +45,6 @@ const messagesTable = {
34
45
  const processorsTable = { name: `${"emt"}_processors` };
35
46
  const projectionsTable = { name: `${"emt"}_projections` };
36
47
 
37
- //#endregion
38
- //#region src/eventStore/schema/readLastMessageGlobalPosition.ts
39
- const readLastMessageGlobalPosition = async (execute, options) => {
40
- const result = await singleOrNull(execute.query(SQL`SELECT global_position
41
- FROM ${SQL.identifier(messagesTable.name)}
42
- WHERE partition = ${options?.partition ?? defaultTag} AND is_archived = FALSE AND transaction_id < pg_snapshot_xmin(pg_current_snapshot())
43
- ORDER BY transaction_id DESC, global_position DESC
44
- LIMIT 1`));
45
- return { currentGlobalPosition: result !== null ? BigInt(result.global_position) : null };
46
- };
47
-
48
- //#endregion
49
- //#region src/eventStore/schema/readMessagesBatch.ts
50
- const readMessagesBatch = async (execute, options) => {
51
- const from = "from" in options ? options.from : void 0;
52
- const after = "after" in options ? options.after : void 0;
53
- const batchSize = "batchSize" in options ? options.batchSize : options.to - options.from;
54
- const fromCondition = from !== void 0 ? SQL`AND global_position >= ${from}` : after !== void 0 ? SQL`AND global_position > ${after}` : SQL.EMPTY;
55
- const toCondition = "to" in options ? SQL`AND global_position <= ${options.to}` : SQL.EMPTY;
56
- const limitCondition = "batchSize" in options ? SQL`LIMIT ${options.batchSize}` : SQL.EMPTY;
57
- const messages = await mapRows(execute.query(SQL`
58
- SELECT stream_id, stream_position, global_position, message_data, message_metadata, message_schema_version, message_type, message_id
59
- FROM ${SQL.identifier(messagesTable.name)}
60
- WHERE partition = ${options?.partition ?? defaultTag} AND is_archived = FALSE AND transaction_id < pg_snapshot_xmin(pg_current_snapshot()) ${fromCondition} ${toCondition}
61
- ORDER BY transaction_id, global_position
62
- ${limitCondition}`), (row) => {
63
- const rawEvent = {
64
- type: row.message_type,
65
- data: row.message_data,
66
- metadata: row.message_metadata
67
- };
68
- const metadata = {
69
- ..."metadata" in rawEvent ? rawEvent.metadata ?? {} : {},
70
- messageId: row.message_id,
71
- streamName: row.stream_id,
72
- streamPosition: BigInt(row.stream_position),
73
- globalPosition: BigInt(row.global_position),
74
- checkpoint: bigIntProcessorCheckpoint(BigInt(row.global_position))
75
- };
76
- return {
77
- ...rawEvent,
78
- kind: "Event",
79
- metadata
80
- };
81
- });
82
- return messages.length > 0 ? {
83
- currentGlobalPosition: messages[messages.length - 1].metadata.globalPosition,
84
- messages,
85
- areMessagesLeft: messages.length === batchSize
86
- } : {
87
- currentGlobalPosition: "from" in options ? options.from : "after" in options ? options.after : 0n,
88
- messages: [],
89
- areMessagesLeft: false
90
- };
91
- };
92
-
93
- //#endregion
94
- //#region src/eventStore/consumers/messageBatchProcessing/index.ts
95
- const DefaultPostgreSQLEventStoreProcessorBatchSize = 100;
96
- const DefaultPostgreSQLEventStoreProcessorPullingFrequencyInMs = 50;
97
- const postgreSQLEventStoreMessageBatchPuller = ({ executor, batchSize, eachBatch, pullingFrequencyInMs, stopWhen, signal }) => {
98
- let isRunning = false;
99
- let start;
100
- const pullMessages = async (options) => {
101
- try {
102
- let after;
103
- try {
104
- after = options.startFrom === "BEGINNING" ? 0n : options.startFrom === "END" ? (await readLastMessageGlobalPosition(executor)).currentGlobalPosition ?? 0n : parseBigIntProcessorCheckpoint(options.startFrom.lastCheckpoint);
105
- } catch (error) {
106
- console.log("Error occurred during reading last processor checkpoint:", error);
107
- options.started?.reject(error);
108
- throw error;
109
- }
110
- options.started?.resolve();
111
- const readMessagesOptions = {
112
- after,
113
- batchSize
114
- };
115
- let waitTime = 100;
116
- while (isRunning && !signal?.aborted) {
117
- const { messages, currentGlobalPosition, areMessagesLeft } = await readMessagesBatch(executor, readMessagesOptions);
118
- if (messages.length > 0) {
119
- const result = await eachBatch(messages);
120
- if (result && result.type === "STOP") {
121
- isRunning = false;
122
- break;
123
- }
124
- }
125
- readMessagesOptions.after = currentGlobalPosition;
126
- await new Promise((resolve) => setTimeout(resolve, waitTime));
127
- if (stopWhen?.noMessagesLeft === true && !areMessagesLeft) {
128
- console.log(`No messages left to process after reaching global position ${currentGlobalPosition}. Stopping the puller.`);
129
- isRunning = false;
130
- break;
131
- }
132
- if (!areMessagesLeft) waitTime = Math.min(waitTime * 2, 1e3);
133
- else waitTime = pullingFrequencyInMs;
134
- }
135
- } catch (error) {
136
- console.log("Error occurred during message pulling:", error);
137
- throw error;
138
- }
139
- };
140
- return {
141
- get isRunning() {
142
- return isRunning;
143
- },
144
- start: (options) => {
145
- if (isRunning) return start;
146
- isRunning = true;
147
- start = (async () => {
148
- return pullMessages(options);
149
- })();
150
- return start;
151
- },
152
- stop: async () => {
153
- if (!isRunning) return;
154
- isRunning = false;
155
- await start;
156
- }
157
- };
158
- };
159
- const zipPostgreSQLEventStoreMessageBatchPullerStartFrom = (options) => {
160
- if (options.length === 0 || options.some((o) => o === void 0 || o === "BEGINNING")) return "BEGINNING";
161
- if (options.every((o) => o === "END")) return "END";
162
- return options.filter((o) => o !== void 0 && o !== "BEGINNING" && o !== "END").sort((a, b) => a > b ? 1 : -1)[0];
163
- };
164
-
165
- //#endregion
166
- //#region src/eventStore/schema/createFunctionIfDoesNotExist.ts
167
- const createFunctionIfDoesNotExistSQL = (functionName, functionDefinition) => SQL`
168
- DO $$
169
- BEGIN
170
- IF NOT EXISTS (SELECT 1 FROM pg_proc WHERE proname = '${SQL.plain(functionName)}') THEN
171
- ${functionDefinition}
172
- END IF;
173
- END $$;
174
- `;
175
-
176
48
  //#endregion
177
49
  //#region src/eventStore/schema/projections/projectionsLocks.ts
178
50
  const tryAcquireProjectionLockSQL = createFunctionIfDoesNotExistSQL("emt_try_acquire_projection_lock", SQL`
@@ -580,7 +452,7 @@ const registerProjection = async (execute, options) => {
580
452
  const name = registration.projection.name;
581
453
  const version = registration.projection.version ?? 1;
582
454
  const kind = registration.projection.kind ?? registration.type;
583
- const definition = JSONSerializer.serialize(registration.projection);
455
+ const definition = JSONSerializer$1.serialize(registration.projection);
584
456
  const lockKeyBigInt = await hashText(toProjectionLockKey({
585
457
  projectionName: name,
586
458
  partition,
@@ -864,127 +736,280 @@ const expectPongoDocuments = { fromCollection: (collectionName) => {
864
736
  } };
865
737
 
866
738
  //#endregion
867
- //#region src/eventStore/projections/postgresProjectionSpec.ts
868
- const PostgreSQLProjectionSpec = { for: (options) => {
869
- {
870
- const { projection, ...restOptions } = options;
871
- const dumboOptions = {
872
- ...restOptions,
873
- serialization: projection.serialization
874
- };
875
- const { connectionString } = dumboOptions;
876
- let wasInitialised = false;
877
- const initialize = async (pool) => {
878
- const eventStore = getPostgreSQLEventStore(connectionString, { connectionOptions: { dumbo: pool } });
879
- if (wasInitialised) return;
880
- wasInitialised = true;
881
- await eventStore.schema.migrate();
882
- if (projection.init) await pool.withTransaction(async (transaction) => {
883
- await projection.init({
884
- registrationType: "async",
885
- version: projection.version ?? 1,
886
- status: "active",
887
- context: await transactionToPostgreSQLProjectionHandlerContext(connectionString, pool, transaction)
888
- });
889
- });
890
- };
891
- return (givenEvents) => {
892
- return { when: (events, options) => {
893
- const allEvents = [];
894
- const run = async (pool) => {
895
- let globalPosition = 0n;
896
- const numberOfTimes = options?.numberOfTimes ?? 1;
897
- for (const event of [...givenEvents, ...Array.from({ length: numberOfTimes }).flatMap(() => events)]) {
898
- const metadata = {
899
- checkpoint: bigIntProcessorCheckpoint(++globalPosition),
900
- globalPosition,
901
- streamPosition: globalPosition,
902
- streamName: `test-${v4()}`,
903
- messageId: v4()
904
- };
905
- allEvents.push({
906
- ...event,
907
- kind: "Event",
908
- metadata: {
909
- ...metadata,
910
- ..."metadata" in event ? event.metadata ?? {} : {}
911
- }
912
- });
913
- }
914
- await initialize(pool);
915
- await pool.withTransaction(async (transaction) => {
916
- await handleProjections({
917
- events: allEvents,
918
- projections: [projection],
919
- ...await transactionToPostgreSQLProjectionHandlerContext(connectionString, pool, transaction)
920
- });
921
- });
922
- };
923
- return {
924
- then: async (assert, message) => {
925
- const pool = dumbo(dumboOptions);
926
- try {
927
- await run(pool);
928
- const succeeded = await assert({
929
- pool,
930
- connectionString
931
- });
932
- if (succeeded !== void 0 && succeeded === false) assertFails(message ?? "Projection specification didn't match the criteria");
933
- } finally {
934
- await pool.close();
935
- }
936
- },
937
- thenThrows: async (...args) => {
938
- const pool = dumbo(dumboOptions);
939
- try {
940
- await run(pool);
941
- throw new AssertionError("Handler did not fail as expected");
942
- } catch (error) {
943
- if (error instanceof AssertionError) throw error;
944
- if (args.length === 0) return;
945
- if (!isErrorConstructor(args[0])) {
946
- assertTrue(args[0](error), `Error didn't match the error condition: ${error?.toString()}`);
947
- return;
948
- }
949
- assertTrue(error instanceof args[0], `Caught error is not an instance of the expected type: ${error?.toString()}`);
950
- if (args[1]) assertTrue(args[1](error), `Error didn't match the error condition: ${error?.toString()}`);
951
- } finally {
952
- await pool.close();
953
- }
954
- }
955
- };
956
- } };
957
- };
958
- }
959
- } };
960
- const eventInStream = (streamName, event) => {
961
- return {
962
- ...event,
963
- metadata: {
964
- ...event.metadata ?? {},
965
- streamName: event.metadata?.streamName ?? streamName
966
- }
967
- };
968
- };
969
- const eventsInStream = (streamName, events) => {
970
- return events.map((e) => eventInStream(streamName, e));
971
- };
972
- const newEventsInStream = eventsInStream;
973
- const assertSQLQueryResultMatches = (sql, rows) => async ({ pool: { execute } }) => {
974
- const result = await execute.query(sql);
975
- assertThatArray(rows).containsExactlyInAnyOrder(result.rows);
739
+ //#region src/eventStore/schema/truncateTables.ts
740
+ const truncateTables = async (execute, options) => {
741
+ await execute.command(SQL`TRUNCATE TABLE
742
+ ${SQL.identifier(streamsTable.name)},
743
+ ${SQL.identifier(messagesTable.name)},
744
+ ${SQL.identifier(processorsTable.name)},
745
+ ${SQL.identifier(projectionsTable.name)}
746
+ CASCADE${SQL.plain(options?.resetSequences ? "; ALTER SEQUENCE emt_global_message_position RESTART WITH 1" : "")};`);
976
747
  };
977
- const expectSQL = { query: (sql) => ({ resultRows: { toBeTheSame: (rows) => assertSQLQueryResultMatches(sql, rows) } }) };
978
748
 
979
749
  //#endregion
980
- //#region src/eventStore/projections/postgreSQLProjection.ts
981
- const transactionToPostgreSQLProjectionHandlerContext = async (connectionString, pool, transaction) => ({
982
- execute: transaction.execute,
983
- connection: {
984
- connectionString,
985
- client: await transaction.connection.open(),
986
- transaction,
987
- pool
750
+ //#region src/eventStore/postgreSQLEventStore.ts
751
+ const defaultPostgreSQLOptions = {
752
+ projections: [],
753
+ schema: { autoMigration: "CreateOrUpdate" }
754
+ };
755
+ const PostgreSQLEventStoreDefaultStreamVersion = 0n;
756
+ const getPostgreSQLEventStore = (connectionString, options = defaultPostgreSQLOptions) => {
757
+ const poolOptions = {
758
+ connectionString,
759
+ ...options.connectionOptions ? options.connectionOptions : {}
760
+ };
761
+ const pool = "dumbo" in poolOptions ? poolOptions.dumbo : dumbo({
762
+ ...poolOptions,
763
+ serialization: options.serialization
764
+ });
765
+ let migrateSchema = void 0;
766
+ const autoGenerateSchema = options.schema?.autoMigration === void 0 || options.schema?.autoMigration !== "None";
767
+ const inlineProjections = (options.projections ?? []).filter(({ type }) => type === "inline").map(({ projection }) => projection);
768
+ const migrate = async (migrationOptions) => {
769
+ if (!migrateSchema) {
770
+ migrateSchema = createEventStoreSchema(connectionString, pool, {
771
+ onBeforeSchemaCreated: async (context) => {
772
+ if (options.hooks?.onBeforeSchemaCreated) await options.hooks.onBeforeSchemaCreated(context);
773
+ },
774
+ onAfterSchemaCreated: async (context) => {
775
+ for (const projection of inlineProjections) if (projection.init) await projection.init({
776
+ version: projection.version ?? 1,
777
+ status: "active",
778
+ registrationType: "inline",
779
+ context: {
780
+ ...context,
781
+ migrationOptions
782
+ }
783
+ });
784
+ if (options.hooks?.onAfterSchemaCreated) await options.hooks.onAfterSchemaCreated(context);
785
+ }
786
+ }, migrationOptions);
787
+ return migrateSchema;
788
+ }
789
+ const result = await migrateSchema;
790
+ if (migrationOptions?.dryRun) migrateSchema = void 0;
791
+ return {
792
+ applied: [],
793
+ skipped: result.applied.concat(result.skipped)
794
+ };
795
+ };
796
+ const ensureSchemaExists = () => {
797
+ if (!autoGenerateSchema) return Promise.resolve();
798
+ return migrate();
799
+ };
800
+ const beforeCommitHook = inlineProjections.length > 0 ? async (events, { transaction }) => handleProjections({
801
+ projections: inlineProjections,
802
+ events,
803
+ ...await transactionToPostgreSQLProjectionHandlerContext(connectionString, pool, transaction)
804
+ }) : void 0;
805
+ return {
806
+ schema: {
807
+ sql: () => SQL.describe(schemaSQL, getFormatter(fromDatabaseDriverType(pool.driverType).databaseType)),
808
+ print: () => console.log(SQL.describe(schemaSQL, getFormatter(fromDatabaseDriverType(pool.driverType).databaseType))),
809
+ migrate,
810
+ dangerous: { truncate: (truncateOptions) => pool.withTransaction(async (transaction) => {
811
+ await ensureSchemaExists();
812
+ await truncateTables(transaction.execute, truncateOptions);
813
+ if (truncateOptions?.truncateProjections) {
814
+ const projectionContext = await transactionToPostgreSQLProjectionHandlerContext(connectionString, pool, transaction);
815
+ for (const projection of options?.projections ?? []) if (projection.projection.truncate) await projection.projection.truncate(projectionContext);
816
+ }
817
+ }) }
818
+ },
819
+ async aggregateStream(streamName, options) {
820
+ const { evolve, initialState, read } = options;
821
+ const expectedStreamVersion = read?.expectedStreamVersion;
822
+ let state = initialState();
823
+ const result = await this.readStream(streamName, read);
824
+ const currentStreamVersion = result.currentStreamVersion;
825
+ assertExpectedVersionMatchesCurrent(currentStreamVersion, expectedStreamVersion, PostgreSQLEventStoreDefaultStreamVersion);
826
+ for (const event of result.events) {
827
+ if (!event) continue;
828
+ state = evolve(state, event);
829
+ }
830
+ return {
831
+ currentStreamVersion,
832
+ state,
833
+ streamExists: result.streamExists
834
+ };
835
+ },
836
+ readStream: async (streamName, readOptions) => {
837
+ await ensureSchemaExists();
838
+ return readStream(pool.execute, streamName, {
839
+ ...readOptions,
840
+ serialization: options.serialization ?? readOptions?.serialization
841
+ });
842
+ },
843
+ appendToStream: async (streamName, events, appendOptions) => {
844
+ await ensureSchemaExists();
845
+ const [firstPart, ...rest] = streamName.split("-");
846
+ const streamType = firstPart && rest.length > 0 ? firstPart : unknownTag;
847
+ const appendResult = await pool.withConnection(async (connection) => appendToStream(connection, streamName, streamType, downcastRecordedMessages(events, appendOptions?.schema?.versioning), {
848
+ ...appendOptions,
849
+ beforeCommitHook
850
+ }));
851
+ if (!appendResult.success) throw new ExpectedVersionConflictError(-1n, appendOptions?.expectedStreamVersion ?? NO_CONCURRENCY_CHECK);
852
+ return {
853
+ nextExpectedStreamVersion: appendResult.nextStreamPosition,
854
+ lastEventGlobalPosition: PostgreSQLEventStoreCheckpoint.toProcessorCheckpoint(appendResult.checkpoints[appendResult.checkpoints.length - 1]),
855
+ createdNewStream: appendResult.nextStreamPosition >= BigInt(events.length)
856
+ };
857
+ },
858
+ streamExists: async (streamName, options) => {
859
+ await ensureSchemaExists();
860
+ return streamExists(pool.execute, streamName, options);
861
+ },
862
+ consumer: (options) => postgreSQLEventStoreConsumer({
863
+ ...options ?? {},
864
+ pool,
865
+ connectionString
866
+ }),
867
+ close: () => pool.close(),
868
+ async withSession(callback) {
869
+ return await pool.withConnection(async (connection) => {
870
+ const eventStore = getPostgreSQLEventStore(connectionString, {
871
+ ...options,
872
+ connectionOptions: { connection },
873
+ schema: {
874
+ ...options.schema ?? {},
875
+ autoMigration: "None"
876
+ }
877
+ });
878
+ return ensureSchemaExists().then(() => callback({
879
+ eventStore,
880
+ close: () => Promise.resolve()
881
+ }));
882
+ });
883
+ }
884
+ };
885
+ };
886
+
887
+ //#endregion
888
+ //#region src/eventStore/projections/postgresProjectionSpec.ts
889
+ const PostgreSQLProjectionSpec = { for: (options) => {
890
+ {
891
+ const { projection, ...restOptions } = options;
892
+ const dumboOptions = {
893
+ ...restOptions,
894
+ serialization: projection.serialization
895
+ };
896
+ const { connectionString } = dumboOptions;
897
+ let wasInitialised = false;
898
+ const initialize = async (pool) => {
899
+ const eventStore = getPostgreSQLEventStore(connectionString, { connectionOptions: { dumbo: pool } });
900
+ if (wasInitialised) return;
901
+ wasInitialised = true;
902
+ await eventStore.schema.migrate();
903
+ if (projection.init) await pool.withTransaction(async (transaction) => {
904
+ await projection.init({
905
+ registrationType: "async",
906
+ version: projection.version ?? 1,
907
+ status: "active",
908
+ context: await transactionToPostgreSQLProjectionHandlerContext(connectionString, pool, transaction)
909
+ });
910
+ });
911
+ };
912
+ return (givenEvents) => {
913
+ return { when: (events, options) => {
914
+ const allEvents = [];
915
+ const run = async (pool) => {
916
+ let globalPosition = 0n;
917
+ const numberOfTimes = options?.numberOfTimes ?? 1;
918
+ for (const event of [...givenEvents, ...Array.from({ length: numberOfTimes }).flatMap(() => events)]) {
919
+ const checkpoint = PostgreSQLEventStoreCheckpoint.toProcessorCheckpoint({
920
+ transactionId: ++globalPosition,
921
+ globalPosition
922
+ });
923
+ const metadata = {
924
+ checkpoint,
925
+ globalPosition: checkpoint,
926
+ streamPosition: globalPosition,
927
+ streamName: `test-${v4()}`,
928
+ messageId: v4()
929
+ };
930
+ allEvents.push({
931
+ ...event,
932
+ kind: "Event",
933
+ metadata: {
934
+ ...metadata,
935
+ ..."metadata" in event ? event.metadata ?? {} : {}
936
+ }
937
+ });
938
+ }
939
+ await initialize(pool);
940
+ await pool.withTransaction(async (transaction) => {
941
+ await handleProjections({
942
+ events: allEvents,
943
+ projections: [projection],
944
+ ...await transactionToPostgreSQLProjectionHandlerContext(connectionString, pool, transaction)
945
+ });
946
+ });
947
+ };
948
+ return {
949
+ then: async (assert, message) => {
950
+ const pool = dumbo(dumboOptions);
951
+ try {
952
+ await run(pool);
953
+ const succeeded = await assert({
954
+ pool,
955
+ connectionString
956
+ });
957
+ if (succeeded !== void 0 && succeeded === false) assertFails(message ?? "Projection specification didn't match the criteria");
958
+ } finally {
959
+ await pool.close();
960
+ }
961
+ },
962
+ thenThrows: async (...args) => {
963
+ const pool = dumbo(dumboOptions);
964
+ try {
965
+ await run(pool);
966
+ throw new AssertionError("Handler did not fail as expected");
967
+ } catch (error) {
968
+ if (error instanceof AssertionError) throw error;
969
+ if (args.length === 0) return;
970
+ if (!isErrorConstructor(args[0])) {
971
+ assertTrue(args[0](error), `Error didn't match the error condition: ${error?.toString()}`);
972
+ return;
973
+ }
974
+ assertTrue(error instanceof args[0], `Caught error is not an instance of the expected type: ${error?.toString()}`);
975
+ if (args[1]) assertTrue(args[1](error), `Error didn't match the error condition: ${error?.toString()}`);
976
+ } finally {
977
+ await pool.close();
978
+ }
979
+ }
980
+ };
981
+ } };
982
+ };
983
+ }
984
+ } };
985
+ const eventInStream = (streamName, event) => {
986
+ return {
987
+ ...event,
988
+ metadata: {
989
+ ...event.metadata ?? {},
990
+ streamName: event.metadata?.streamName ?? streamName
991
+ }
992
+ };
993
+ };
994
+ const eventsInStream = (streamName, events) => {
995
+ return events.map((e) => eventInStream(streamName, e));
996
+ };
997
+ const newEventsInStream = eventsInStream;
998
+ const assertSQLQueryResultMatches = (sql, rows) => async ({ pool: { execute } }) => {
999
+ const result = await execute.query(sql);
1000
+ assertThatArray(rows).containsExactlyInAnyOrder(result.rows);
1001
+ };
1002
+ const expectSQL = { query: (sql) => ({ resultRows: { toBeTheSame: (rows) => assertSQLQueryResultMatches(sql, rows) } }) };
1003
+
1004
+ //#endregion
1005
+ //#region src/eventStore/projections/postgreSQLProjection.ts
1006
+ const transactionToPostgreSQLProjectionHandlerContext = async (connectionString, pool, transaction) => ({
1007
+ execute: transaction.execute,
1008
+ connection: {
1009
+ connectionString,
1010
+ client: await transaction.connection.open(),
1011
+ transaction,
1012
+ pool
988
1013
  }
989
1014
  });
990
1015
  const handleProjections = async (options) => {
@@ -1058,6 +1083,74 @@ const postgreSQLRawSQLProjection = (options) => {
1058
1083
  });
1059
1084
  };
1060
1085
 
1086
+ //#endregion
1087
+ //#region src/eventStore/schema/readMessagesBatch.ts
1088
+ const PostgreSQLEventStoreCheckpoint = {
1089
+ default: {
1090
+ transactionId: 0n,
1091
+ globalPosition: 0n
1092
+ },
1093
+ parse: (checkPoint) => {
1094
+ if (checkPoint === void 0 || checkPoint === null) return PostgreSQLEventStoreCheckpoint.default;
1095
+ const [transactionId, globalPosition] = checkPoint.split(":");
1096
+ return {
1097
+ transactionId: BigInt(transactionId),
1098
+ globalPosition: BigInt(globalPosition)
1099
+ };
1100
+ },
1101
+ toProcessorCheckpoint: (checkPoint) => `${checkPoint.transactionId.toString().padStart(20, "0")}:${bigIntProcessorCheckpoint(checkPoint.globalPosition)}`
1102
+ };
1103
+ const readMessagesBatch = async (execute, options) => {
1104
+ const from = "from" in options ? options.from : void 0;
1105
+ const after = "after" in options ? options.after : void 0;
1106
+ const batchSize = "batchSize" in options ? options.batchSize : options.to.globalPosition - options.from.globalPosition;
1107
+ const fromCondition = from !== void 0 ? SQL`AND (transaction_id, global_position) >= (${from.transactionId}, ${from.globalPosition})` : after !== void 0 ? SQL`AND (transaction_id, global_position) > (${after.transactionId}, ${after.globalPosition})` : SQL.EMPTY;
1108
+ const toCondition = "to" in options ? SQL`AND (transaction_id, global_position) <= (${options.to.transactionId}, ${options.to.globalPosition})` : SQL.EMPTY;
1109
+ const limitCondition = "batchSize" in options ? SQL`LIMIT ${options.batchSize}` : SQL.EMPTY;
1110
+ const query = SQL`
1111
+ SELECT stream_id, stream_position, global_position, message_data, message_metadata, message_schema_version, message_type, message_id, transaction_id
1112
+ FROM ${SQL.identifier(messagesTable.name)}
1113
+ WHERE partition = ${options?.partition ?? defaultTag}
1114
+ AND is_archived = FALSE
1115
+ AND transaction_id < pg_snapshot_xmin(pg_current_snapshot())
1116
+ ${fromCondition} ${toCondition}
1117
+ ORDER BY transaction_id, global_position
1118
+ ${limitCondition}`;
1119
+ const messages = await mapRows(execute.query(query), (row) => {
1120
+ const rawEvent = {
1121
+ type: row.message_type,
1122
+ data: row.message_data,
1123
+ metadata: row.message_metadata
1124
+ };
1125
+ const globalPosition = PostgreSQLEventStoreCheckpoint.toProcessorCheckpoint({
1126
+ transactionId: BigInt(row.transaction_id),
1127
+ globalPosition: BigInt(row.global_position)
1128
+ });
1129
+ const metadata = {
1130
+ ..."metadata" in rawEvent ? rawEvent.metadata ?? {} : {},
1131
+ messageId: row.message_id,
1132
+ streamName: row.stream_id,
1133
+ streamPosition: BigInt(row.stream_position),
1134
+ globalPosition,
1135
+ checkpoint: globalPosition
1136
+ };
1137
+ return {
1138
+ ...rawEvent,
1139
+ kind: "Event",
1140
+ metadata
1141
+ };
1142
+ });
1143
+ return messages.length > 0 ? {
1144
+ currentCheckpoint: PostgreSQLEventStoreCheckpoint.parse(messages[messages.length - 1].metadata.checkpoint),
1145
+ messages,
1146
+ areMessagesLeft: messages.length === batchSize
1147
+ } : {
1148
+ currentCheckpoint: "from" in options ? options.from : "after" in options ? options.after : PostgreSQLEventStoreCheckpoint.default,
1149
+ messages: [],
1150
+ areMessagesLeft: false
1151
+ };
1152
+ };
1153
+
1061
1154
  //#endregion
1062
1155
  //#region src/eventStore/schema/appendToStream.ts
1063
1156
  const appendToStreamSQL = createFunctionIfDoesNotExistSQL("emt_append_to_stream", SQL`CREATE OR REPLACE FUNCTION emt_append_to_stream(
@@ -1163,7 +1256,7 @@ const callAppendToStream = (params) => SQL`SELECT * FROM emt_append_to_stream(
1163
1256
  ${params.expectedStreamPosition},
1164
1257
  ${params.partition}::text
1165
1258
  )`;
1166
- const appendToStream = (pool, streamName, streamType, messages, options) => pool.withTransaction(async (transaction) => {
1259
+ const appendToStream = (connection, streamName, streamType, messages, options) => connection.withTransaction(async (transaction) => {
1167
1260
  const { execute } = transaction;
1168
1261
  if (messages.length === 0) return {
1169
1262
  success: false,
@@ -1179,19 +1272,23 @@ const appendToStream = (pool, streamName, streamType, messages, options) => pool
1179
1272
  ..."metadata" in e ? e.metadata ?? {} : {}
1180
1273
  }
1181
1274
  }));
1182
- const { success, next_stream_position, global_positions, transaction_id } = await appendEventsRaw(execute, streamName, streamType, messagesToAppend, { expectedStreamVersion });
1275
+ const { success, next_stream_position, global_positions, transaction_id } = await appentToStreamRaw(execute, streamName, streamType, messagesToAppend, { expectedStreamVersion });
1183
1276
  if (!success || next_stream_position === null || global_positions === null || global_positions.length === 0 || transaction_id == null) return {
1184
1277
  success: false,
1185
1278
  result: { success: false }
1186
1279
  };
1187
1280
  const nextStreamPosition = BigInt(next_stream_position);
1188
1281
  const globalPositions = global_positions.map(BigInt);
1282
+ const transactionId = BigInt(transaction_id);
1189
1283
  globalPositions.forEach((globalPosition, index) => {
1190
1284
  messagesToAppend[index].metadata = {
1191
1285
  ...messagesToAppend[index].metadata,
1192
1286
  streamName,
1193
1287
  streamPosition: nextStreamPosition - BigInt(messagesToAppend.length) + BigInt(index + 1),
1194
- globalPosition
1288
+ globalPosition: PostgreSQLEventStoreCheckpoint.toProcessorCheckpoint({
1289
+ transactionId,
1290
+ globalPosition
1291
+ })
1195
1292
  };
1196
1293
  });
1197
1294
  if (options?.beforeCommitHook) await options.beforeCommitHook(messagesToAppend, { transaction });
@@ -1200,8 +1297,10 @@ const appendToStream = (pool, streamName, streamType, messages, options) => pool
1200
1297
  result: {
1201
1298
  success: true,
1202
1299
  nextStreamPosition,
1203
- globalPositions,
1204
- transactionId: transaction_id
1300
+ checkpoints: globalPositions.map((globalPosition) => ({
1301
+ transactionId,
1302
+ globalPosition
1303
+ }))
1205
1304
  }
1206
1305
  };
1207
1306
  } catch (error) {
@@ -1220,7 +1319,7 @@ const toExpectedVersion = (expected) => {
1220
1319
  return expected;
1221
1320
  };
1222
1321
  const isOptimisticConcurrencyError = (error) => DumboError.isInstanceOf(error, { errorType: UniqueConstraintError.ErrorType });
1223
- const appendEventsRaw = (execute, streamId, streamType, messages, options) => single(execute.command(callAppendToStream({
1322
+ const appentToStreamRaw = (execute, streamId, streamType, messages, options) => single(execute.command(callAppendToStream({
1224
1323
  messageIds: messages.map((e) => e.metadata.messageId),
1225
1324
  messagesData: messages.map((e) => e.data),
1226
1325
  messagesMetadata: messages.map((e) => {
@@ -1253,7 +1352,7 @@ DECLARE
1253
1352
  BEGIN
1254
1353
  -- Handle the case when p_check_position is provided
1255
1354
  IF p_check_position IS NOT NULL THEN
1256
- -- Try to update if the position matches p_check_position
1355
+ -- Try to update if the position matches p_check_position (new format)
1257
1356
  UPDATE "${SQL.plain(processorsTable.name)}"
1258
1357
  SET
1259
1358
  "last_processed_checkpoint" = p_position,
@@ -1268,6 +1367,38 @@ BEGIN
1268
1367
  RETURN 1; -- Successfully updated
1269
1368
  END IF;
1270
1369
 
1370
+ -- TODO: Remove once all deployments have run the 0.43.0 migration.
1371
+ -- Handles mixed-format scenarios during blue-green deployment.
1372
+ IF p_check_position LIKE '%:%' THEN
1373
+ -- new code, stored value still in old format (plain globalpos)
1374
+ UPDATE "${SQL.plain(processorsTable.name)}"
1375
+ SET
1376
+ "last_processed_checkpoint" = p_position,
1377
+ "last_processed_transaction_id" = p_transaction_id,
1378
+ "last_updated" = now()
1379
+ WHERE "processor_id" = p_processor_id
1380
+ AND "last_processed_checkpoint" = split_part(p_check_position, ':', 2)
1381
+ AND "last_processed_checkpoint" NOT LIKE '%:%'
1382
+ AND "partition" = p_partition
1383
+ AND "version" = p_version;
1384
+ ELSE
1385
+ -- old code, stored value already migrated to new format (txid:globalpos)
1386
+ UPDATE "${SQL.plain(processorsTable.name)}"
1387
+ SET
1388
+ "last_processed_checkpoint" = p_position,
1389
+ "last_processed_transaction_id" = p_transaction_id,
1390
+ "last_updated" = now()
1391
+ WHERE "processor_id" = p_processor_id
1392
+ AND split_part("last_processed_checkpoint", ':', 2) = p_check_position
1393
+ AND "last_processed_checkpoint" LIKE '%:%'
1394
+ AND "partition" = p_partition
1395
+ AND "version" = p_version;
1396
+ END IF;
1397
+
1398
+ IF FOUND THEN
1399
+ RETURN 1; -- Successfully updated (mixed-format fallback)
1400
+ END IF;
1401
+
1271
1402
  -- Retrieve the current position
1272
1403
  SELECT "last_processed_checkpoint" INTO current_position
1273
1404
  FROM "${SQL.plain(processorsTable.name)}"
@@ -2520,42 +2651,158 @@ IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'emt_subscriptions') THEN
2520
2651
  AND "partition" = p_partition
2521
2652
  AND "version" = p_version ;
2522
2653
 
2523
- IF current_position = p_position THEN
2524
- RETURN 0;
2525
- ELSIF current_position > p_position THEN
2526
- RETURN 3;
2527
- ELSE
2528
- RETURN 2;
2529
- END IF;
2654
+ IF current_position = p_position THEN
2655
+ RETURN 0;
2656
+ ELSIF current_position > p_position THEN
2657
+ RETURN 3;
2658
+ ELSE
2659
+ RETURN 2;
2660
+ END IF;
2661
+ END IF;
2662
+
2663
+ BEGIN
2664
+ INSERT INTO "emt_processors"("processor_id", "version", "last_processed_checkpoint", "partition", "last_processed_transaction_id", "created_at", "last_updated")
2665
+ VALUES (p_processor_id, p_version, p_position, p_partition, p_transaction_id, now(), now());
2666
+ RETURN 1;
2667
+ EXCEPTION WHEN unique_violation THEN
2668
+ SELECT "last_processed_checkpoint" INTO current_position
2669
+ FROM "emt_processors"
2670
+ WHERE "processor_id" = p_processor_id
2671
+ AND "partition" = p_partition
2672
+ AND "version" = p_version;
2673
+
2674
+ IF current_position = p_position THEN
2675
+ RETURN 0;
2676
+ ELSE
2677
+ RETURN 2;
2678
+ END IF;
2679
+ END;
2680
+ END;
2681
+ $fn$ LANGUAGE plpgsql;
2682
+ END IF;
2683
+ END $$;
2684
+ `;
2685
+ const migration_0_43_0_cleanupLegacySubscription = sqlMigration("emt:postgresql:eventstore:0.43.0:cleanup-legacy-subscription", [migration_0_43_0_cleanupLegacySubscriptionSQL]);
2686
+ const migration_0_43_0_updateStoreProcessorCheckpointSQL = SQL`
2687
+ CREATE OR REPLACE FUNCTION store_processor_checkpoint(
2688
+ p_processor_id TEXT,
2689
+ p_version BIGINT,
2690
+ p_position TEXT,
2691
+ p_check_position TEXT,
2692
+ p_transaction_id xid8,
2693
+ p_partition TEXT DEFAULT '${SQL.plain(defaultTag)}',
2694
+ p_processor_instance_id TEXT DEFAULT '${SQL.plain(unknownTag$1)}'
2695
+ ) RETURNS INT AS $spc$
2696
+ DECLARE
2697
+ current_position TEXT;
2698
+ BEGIN
2699
+ IF p_check_position IS NOT NULL THEN
2700
+ UPDATE "${SQL.plain(processorsTable.name)}"
2701
+ SET
2702
+ "last_processed_checkpoint" = p_position,
2703
+ "last_processed_transaction_id" = p_transaction_id,
2704
+ "last_updated" = now()
2705
+ WHERE "processor_id" = p_processor_id
2706
+ AND "last_processed_checkpoint" = p_check_position
2707
+ AND "partition" = p_partition
2708
+ AND "version" = p_version;
2709
+
2710
+ IF FOUND THEN
2711
+ RETURN 1;
2712
+ END IF;
2713
+
2714
+ -- TODO: Remove once all deployments have run the 0.43.0 migration.
2715
+ -- Handles mixed-format scenarios during blue-green deployment.
2716
+ IF p_check_position LIKE '%:%' THEN
2717
+ -- new code, stored value still in old format (plain globalpos)
2718
+ UPDATE "${SQL.plain(processorsTable.name)}"
2719
+ SET
2720
+ "last_processed_checkpoint" = p_position,
2721
+ "last_processed_transaction_id" = p_transaction_id,
2722
+ "last_updated" = now()
2723
+ WHERE "processor_id" = p_processor_id
2724
+ AND "last_processed_checkpoint" = split_part(p_check_position, ':', 2)
2725
+ AND "last_processed_checkpoint" NOT LIKE '%:%'
2726
+ AND "partition" = p_partition
2727
+ AND "version" = p_version;
2728
+ ELSE
2729
+ -- old code, stored value already migrated to new format (txid:globalpos)
2730
+ UPDATE "${SQL.plain(processorsTable.name)}"
2731
+ SET
2732
+ "last_processed_checkpoint" = p_position,
2733
+ "last_processed_transaction_id" = p_transaction_id,
2734
+ "last_updated" = now()
2735
+ WHERE "processor_id" = p_processor_id
2736
+ AND split_part("last_processed_checkpoint", ':', 2) = p_check_position
2737
+ AND "last_processed_checkpoint" LIKE '%:%'
2738
+ AND "partition" = p_partition
2739
+ AND "version" = p_version;
2740
+ END IF;
2741
+
2742
+ IF FOUND THEN
2743
+ RETURN 1;
2744
+ END IF;
2745
+
2746
+ SELECT "last_processed_checkpoint" INTO current_position
2747
+ FROM "${SQL.plain(processorsTable.name)}"
2748
+ WHERE "processor_id" = p_processor_id
2749
+ AND "partition" = p_partition
2750
+ AND "version" = p_version;
2751
+
2752
+ IF current_position = p_position THEN
2753
+ RETURN 0;
2754
+ ELSIF current_position > p_position THEN
2755
+ RETURN 3;
2756
+ ELSE
2757
+ RETURN 2;
2530
2758
  END IF;
2759
+ END IF;
2531
2760
 
2532
- BEGIN
2533
- INSERT INTO "emt_processors"("processor_id", "version", "last_processed_checkpoint", "partition", "last_processed_transaction_id", "created_at", "last_updated")
2534
- VALUES (p_processor_id, p_version, p_position, p_partition, p_transaction_id, now(), now());
2535
- RETURN 1;
2536
- EXCEPTION WHEN unique_violation THEN
2537
- SELECT "last_processed_checkpoint" INTO current_position
2538
- FROM "emt_processors"
2539
- WHERE "processor_id" = p_processor_id
2540
- AND "partition" = p_partition
2541
- AND "version" = p_version;
2761
+ BEGIN
2762
+ INSERT INTO "${SQL.plain(processorsTable.name)}"("processor_id", "version", "last_processed_checkpoint", "partition", "last_processed_transaction_id", "created_at", "last_updated")
2763
+ VALUES (p_processor_id, p_version, p_position, p_partition, p_transaction_id, now(), now());
2764
+ RETURN 1;
2765
+ EXCEPTION WHEN unique_violation THEN
2766
+ SELECT "last_processed_checkpoint" INTO current_position
2767
+ FROM "${SQL.plain(processorsTable.name)}"
2768
+ WHERE "processor_id" = p_processor_id
2769
+ AND "partition" = p_partition
2770
+ AND "version" = p_version;
2542
2771
 
2543
- IF current_position = p_position THEN
2544
- RETURN 0;
2545
- ELSE
2546
- RETURN 2;
2547
- END IF;
2548
- END;
2549
- END;
2550
- $fn$ LANGUAGE plpgsql;
2551
- END IF;
2772
+ IF current_position = p_position THEN
2773
+ RETURN 0;
2774
+ ELSIF current_position > p_position THEN
2775
+ RETURN 3;
2776
+ ELSE
2777
+ RETURN 2;
2778
+ END IF;
2779
+ END;
2780
+ END;
2781
+ $spc$ LANGUAGE plpgsql;
2782
+ `;
2783
+ const migration_0_43_0_updateStoreProcessorCheckpoint = sqlMigration("emt:postgresql:eventstore:0.43.0:update-store-processor-checkpoint", [migration_0_43_0_updateStoreProcessorCheckpointSQL]);
2784
+ const migration_0_43_0_upgradeCheckpointFormatSQL = SQL`
2785
+ DO $$
2786
+ BEGIN
2787
+ IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = '${SQL.plain(processorsTable.name)}') THEN
2788
+ UPDATE "${SQL.plain(processorsTable.name)}" p
2789
+ SET last_processed_checkpoint =
2790
+ lpad(m.transaction_id::text, 20, '0') || ':' || p.last_processed_checkpoint
2791
+ FROM "${SQL.plain(messagesTable.name)}" m
2792
+ WHERE m.global_position = p.last_processed_checkpoint::bigint
2793
+ AND p.last_processed_checkpoint NOT LIKE '%:%';
2794
+ END IF;
2552
2795
  END $$;
2553
2796
  `;
2554
- const migration_0_43_0_cleanupLegacySubscription = sqlMigration("emt:postgresql:eventstore:0.43.0:cleanup-legacy-subscription", [migration_0_43_0_cleanupLegacySubscriptionSQL]);
2797
+ const migration_0_43_0_upgradeCheckpointFormat = sqlMigration("emt:postgresql:eventstore:0.43.0:upgrade-checkpoint-format", [migration_0_43_0_upgradeCheckpointFormatSQL]);
2555
2798
 
2556
2799
  //#endregion
2557
2800
  //#region src/eventStore/schema/migrations/0_43_0/index.ts
2558
- const migrations_0_43_0 = [migration_0_43_0_cleanupLegacySubscription];
2801
+ const migrations_0_43_0 = [
2802
+ migration_0_43_0_cleanupLegacySubscription,
2803
+ migration_0_43_0_updateStoreProcessorCheckpoint,
2804
+ migration_0_43_0_upgradeCheckpointFormat
2805
+ ];
2559
2806
 
2560
2807
  //#endregion
2561
2808
  //#region src/eventStore/schema/migrations/index.ts
@@ -2585,14 +2832,42 @@ const pastEventStoreSchemaMigrations = [
2585
2832
  ];
2586
2833
  const eventStoreSchemaMigrations = [...pastEventStoreSchemaMigrations, schemaMigration$1];
2587
2834
 
2835
+ //#endregion
2836
+ //#region src/eventStore/schema/readLastMessageCheckpoint.ts
2837
+ const readLastMessageCheckpoint = async (execute, options) => {
2838
+ const result = await singleOrNull(execute.query(SQL`SELECT transaction_id, global_position
2839
+ FROM ${SQL.identifier(messagesTable.name)}
2840
+ WHERE partition = ${options?.partition ?? defaultTag} AND is_archived = FALSE AND transaction_id < pg_snapshot_xmin(pg_current_snapshot())
2841
+ ORDER BY transaction_id DESC, global_position DESC
2842
+ LIMIT 1`));
2843
+ return { currentCheckpoint: result !== null ? {
2844
+ transactionId: BigInt(result.transaction_id),
2845
+ globalPosition: BigInt(result.global_position)
2846
+ } : null };
2847
+ };
2848
+
2588
2849
  //#endregion
2589
2850
  //#region src/eventStore/schema/readProcessorCheckpoint.ts
2851
+ const resolveTransactionId = async (execute, rawCheckpoint) => {
2852
+ if (rawCheckpoint.includes(":")) return rawCheckpoint;
2853
+ const globalPosition = BigInt(rawCheckpoint);
2854
+ if (globalPosition === 0n) return PostgreSQLEventStoreCheckpoint.toProcessorCheckpoint(PostgreSQLEventStoreCheckpoint.default);
2855
+ const row = await single(execute.query(SQL`SELECT transaction_id
2856
+ FROM ${SQL.identifier(messagesTable.name)}
2857
+ WHERE global_position = ${globalPosition}
2858
+ LIMIT 1`));
2859
+ return PostgreSQLEventStoreCheckpoint.toProcessorCheckpoint({
2860
+ transactionId: BigInt(row.transaction_id),
2861
+ globalPosition
2862
+ });
2863
+ };
2590
2864
  const readProcessorCheckpoint = async (execute, options) => {
2591
2865
  const result = await singleOrNull(execute.query(SQL`SELECT last_processed_checkpoint
2592
2866
  FROM ${SQL.identifier(processorsTable.name)}
2593
2867
  WHERE partition = ${options?.partition ?? defaultTag} AND processor_id = ${options.processorId} AND version = ${options.version ?? 1}
2594
2868
  LIMIT 1`));
2595
- return { lastProcessedCheckpoint: result !== null ? result.last_processed_checkpoint : null };
2869
+ if (result === null) return { lastProcessedCheckpoint: null };
2870
+ return { lastProcessedCheckpoint: await resolveTransactionId(execute, result.last_processed_checkpoint) };
2596
2871
  };
2597
2872
 
2598
2873
  //#endregion
@@ -2601,7 +2876,7 @@ const readStream = async (execute, streamId, options) => {
2601
2876
  const fromCondition = options?.from ? `AND stream_position >= ${options.from}` : "";
2602
2877
  const to = Number(options?.to ?? (options?.maxCount ? (options.from ?? 0n) + options.maxCount : NaN));
2603
2878
  const toCondition = !isNaN(to) ? `AND stream_position <= ${to}` : "";
2604
- const events = await mapRows(execute.query(SQL`SELECT stream_id, stream_position, global_position, message_data, message_metadata, message_schema_version, message_type, message_id
2879
+ const events = await mapRows(execute.query(SQL`SELECT stream_id, stream_position, global_position, message_data, message_metadata, message_schema_version, message_type, message_id, transaction_id
2605
2880
  FROM ${SQL.identifier(messagesTable.name)}
2606
2881
  WHERE stream_id = ${streamId} AND partition = ${options?.partition ?? defaultTag} AND is_archived = FALSE ${SQL.plain(fromCondition)} ${SQL.plain(toCondition)}
2607
2882
  ORDER BY stream_position ASC`), (row) => {
@@ -2610,13 +2885,17 @@ const readStream = async (execute, streamId, options) => {
2610
2885
  data: row.message_data,
2611
2886
  metadata: row.message_metadata
2612
2887
  };
2888
+ const globalPosition = PostgreSQLEventStoreCheckpoint.toProcessorCheckpoint({
2889
+ transactionId: BigInt(row.transaction_id),
2890
+ globalPosition: BigInt(row.global_position)
2891
+ });
2613
2892
  const metadata = {
2614
2893
  ..."metadata" in rawEvent ? rawEvent.metadata ?? {} : {},
2615
2894
  messageId: row.message_id,
2616
2895
  streamName: streamId,
2617
2896
  streamPosition: BigInt(row.stream_position),
2618
- globalPosition: BigInt(row.global_position),
2619
- checkpoint: bigIntProcessorCheckpoint(BigInt(row.global_position))
2897
+ globalPosition,
2898
+ checkpoint: globalPosition
2620
2899
  };
2621
2900
  return upcastRecordedMessage({
2622
2901
  ...rawEvent,
@@ -2685,152 +2964,76 @@ const createEventStoreSchema = (connectionString, pool, hooks, options) => {
2685
2964
  };
2686
2965
 
2687
2966
  //#endregion
2688
- //#region src/eventStore/schema/truncateTables.ts
2689
- const truncateTables = async (execute, options) => {
2690
- await execute.command(SQL`TRUNCATE TABLE
2691
- ${SQL.identifier(streamsTable.name)},
2692
- ${SQL.identifier(messagesTable.name)},
2693
- ${SQL.identifier(processorsTable.name)},
2694
- ${SQL.identifier(projectionsTable.name)}
2695
- CASCADE${SQL.plain(options?.resetSequences ? "; ALTER SEQUENCE emt_global_message_position RESTART WITH 1" : "")};`);
2696
- };
2697
-
2698
- //#endregion
2699
- //#region src/eventStore/postgreSQLEventStore.ts
2700
- const defaultPostgreSQLOptions = {
2701
- projections: [],
2702
- schema: { autoMigration: "CreateOrUpdate" }
2703
- };
2704
- const PostgreSQLEventStoreDefaultStreamVersion = 0n;
2705
- const getPostgreSQLEventStore = (connectionString, options = defaultPostgreSQLOptions) => {
2706
- const poolOptions = {
2707
- connectionString,
2708
- ...options.connectionOptions ? options.connectionOptions : {}
2709
- };
2710
- const pool = "dumbo" in poolOptions ? poolOptions.dumbo : dumbo({
2711
- ...poolOptions,
2712
- serialization: options.serialization
2713
- });
2714
- let migrateSchema = void 0;
2715
- const autoGenerateSchema = options.schema?.autoMigration === void 0 || options.schema?.autoMigration !== "None";
2716
- const inlineProjections = (options.projections ?? []).filter(({ type }) => type === "inline").map(({ projection }) => projection);
2717
- const migrate = async (migrationOptions) => {
2718
- if (!migrateSchema) {
2719
- migrateSchema = createEventStoreSchema(connectionString, pool, {
2720
- onBeforeSchemaCreated: async (context) => {
2721
- if (options.hooks?.onBeforeSchemaCreated) await options.hooks.onBeforeSchemaCreated(context);
2722
- },
2723
- onAfterSchemaCreated: async (context) => {
2724
- for (const projection of inlineProjections) if (projection.init) await projection.init({
2725
- version: projection.version ?? 1,
2726
- status: "active",
2727
- registrationType: "inline",
2728
- context: {
2729
- ...context,
2730
- migrationOptions
2731
- }
2732
- });
2733
- if (options.hooks?.onAfterSchemaCreated) await options.hooks.onAfterSchemaCreated(context);
2967
+ //#region src/eventStore/consumers/messageBatchProcessing/index.ts
2968
+ const DefaultPostgreSQLEventStoreProcessorBatchSize = 100;
2969
+ const DefaultPostgreSQLEventStoreProcessorPullingFrequencyInMs = 50;
2970
+ const postgreSQLEventStoreMessageBatchPuller = ({ executor, batchSize, eachBatch, pullingFrequencyInMs, stopWhen, signal }) => {
2971
+ let isRunning = false;
2972
+ let start;
2973
+ const pullMessages = async (options) => {
2974
+ try {
2975
+ let after;
2976
+ try {
2977
+ after = options.startFrom === "BEGINNING" ? PostgreSQLEventStoreCheckpoint.default : options.startFrom === "END" ? (await readLastMessageCheckpoint(executor)).currentCheckpoint ?? PostgreSQLEventStoreCheckpoint.default : PostgreSQLEventStoreCheckpoint.parse(options.startFrom.lastCheckpoint);
2978
+ } catch (error) {
2979
+ console.log("Error occurred during reading last processor checkpoint:", error);
2980
+ options.started?.reject(error);
2981
+ throw error;
2982
+ }
2983
+ options.started?.resolve();
2984
+ const readMessagesOptions = {
2985
+ after,
2986
+ batchSize
2987
+ };
2988
+ let waitTime = 100;
2989
+ while (isRunning && !signal?.aborted) {
2990
+ const { messages, currentCheckpoint, areMessagesLeft } = await readMessagesBatch(executor, readMessagesOptions);
2991
+ if (messages.length > 0) {
2992
+ const result = await eachBatch(messages);
2993
+ if (result && result.type === "STOP") {
2994
+ isRunning = false;
2995
+ break;
2996
+ }
2734
2997
  }
2735
- }, migrationOptions);
2736
- return migrateSchema;
2998
+ readMessagesOptions.after = currentCheckpoint;
2999
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
3000
+ if (stopWhen?.noMessagesLeft === true && !areMessagesLeft) {
3001
+ console.log(`No messages left to process after reaching checkpoint ${JSONSerializer.serialize(readMessagesOptions.after)}. Stopping the puller.`);
3002
+ isRunning = false;
3003
+ break;
3004
+ }
3005
+ if (!areMessagesLeft) waitTime = Math.min(waitTime * 2, 1e3);
3006
+ else waitTime = pullingFrequencyInMs;
3007
+ }
3008
+ } catch (error) {
3009
+ console.log("Error occurred during message pulling:", error);
3010
+ throw error;
2737
3011
  }
2738
- const result = await migrateSchema;
2739
- if (migrationOptions?.dryRun) migrateSchema = void 0;
2740
- return {
2741
- applied: [],
2742
- skipped: result.applied.concat(result.skipped)
2743
- };
2744
- };
2745
- const ensureSchemaExists = () => {
2746
- if (!autoGenerateSchema) return Promise.resolve();
2747
- return migrate();
2748
3012
  };
2749
- const beforeCommitHook = inlineProjections.length > 0 ? async (events, { transaction }) => handleProjections({
2750
- projections: inlineProjections,
2751
- events,
2752
- ...await transactionToPostgreSQLProjectionHandlerContext(connectionString, pool, transaction)
2753
- }) : void 0;
2754
3013
  return {
2755
- schema: {
2756
- sql: () => SQL.describe(schemaSQL, getFormatter(fromDatabaseDriverType(pool.driverType).databaseType)),
2757
- print: () => console.log(SQL.describe(schemaSQL, getFormatter(fromDatabaseDriverType(pool.driverType).databaseType))),
2758
- migrate,
2759
- dangerous: { truncate: (truncateOptions) => pool.withTransaction(async (transaction) => {
2760
- await ensureSchemaExists();
2761
- await truncateTables(transaction.execute, truncateOptions);
2762
- if (truncateOptions?.truncateProjections) {
2763
- const projectionContext = await transactionToPostgreSQLProjectionHandlerContext(connectionString, pool, transaction);
2764
- for (const projection of options?.projections ?? []) if (projection.projection.truncate) await projection.projection.truncate(projectionContext);
2765
- }
2766
- }) }
2767
- },
2768
- async aggregateStream(streamName, options) {
2769
- const { evolve, initialState, read } = options;
2770
- const expectedStreamVersion = read?.expectedStreamVersion;
2771
- let state = initialState();
2772
- const result = await this.readStream(streamName, read);
2773
- const currentStreamVersion = result.currentStreamVersion;
2774
- assertExpectedVersionMatchesCurrent(currentStreamVersion, expectedStreamVersion, PostgreSQLEventStoreDefaultStreamVersion);
2775
- for (const event of result.events) {
2776
- if (!event) continue;
2777
- state = evolve(state, event);
2778
- }
2779
- return {
2780
- currentStreamVersion,
2781
- state,
2782
- streamExists: result.streamExists
2783
- };
2784
- },
2785
- readStream: async (streamName, readOptions) => {
2786
- await ensureSchemaExists();
2787
- return readStream(pool.execute, streamName, {
2788
- ...readOptions,
2789
- serialization: options.serialization ?? readOptions?.serialization
2790
- });
2791
- },
2792
- appendToStream: async (streamName, events, appendOptions) => {
2793
- await ensureSchemaExists();
2794
- const [firstPart, ...rest] = streamName.split("-");
2795
- const appendResult = await appendToStream(pool, streamName, firstPart && rest.length > 0 ? firstPart : unknownTag, downcastRecordedMessages(events, appendOptions?.schema?.versioning), {
2796
- ...appendOptions,
2797
- beforeCommitHook
2798
- });
2799
- if (!appendResult.success) throw new ExpectedVersionConflictError(-1n, appendOptions?.expectedStreamVersion ?? NO_CONCURRENCY_CHECK);
2800
- return {
2801
- nextExpectedStreamVersion: appendResult.nextStreamPosition,
2802
- lastEventGlobalPosition: appendResult.globalPositions[appendResult.globalPositions.length - 1],
2803
- createdNewStream: appendResult.nextStreamPosition >= BigInt(events.length)
2804
- };
3014
+ get isRunning() {
3015
+ return isRunning;
2805
3016
  },
2806
- streamExists: async (streamName, options) => {
2807
- await ensureSchemaExists();
2808
- return streamExists(pool.execute, streamName, options);
3017
+ start: (options) => {
3018
+ if (isRunning) return start;
3019
+ isRunning = true;
3020
+ start = (async () => {
3021
+ return pullMessages(options);
3022
+ })();
3023
+ return start;
2809
3024
  },
2810
- consumer: (options) => postgreSQLEventStoreConsumer({
2811
- ...options ?? {},
2812
- pool,
2813
- connectionString
2814
- }),
2815
- close: () => pool.close(),
2816
- async withSession(callback) {
2817
- return await pool.withConnection(async (connection) => {
2818
- const eventStore = getPostgreSQLEventStore(connectionString, {
2819
- ...options,
2820
- connectionOptions: { connection },
2821
- schema: {
2822
- ...options.schema ?? {},
2823
- autoMigration: "None"
2824
- }
2825
- });
2826
- return ensureSchemaExists().then(() => callback({
2827
- eventStore,
2828
- close: () => Promise.resolve()
2829
- }));
2830
- });
3025
+ stop: async () => {
3026
+ if (!isRunning) return;
3027
+ isRunning = false;
3028
+ await start;
2831
3029
  }
2832
3030
  };
2833
3031
  };
3032
+ const zipPostgreSQLEventStoreMessageBatchPullerStartFrom = (options) => {
3033
+ if (options.length === 0 || options.some((o) => o === void 0 || o === "BEGINNING")) return "BEGINNING";
3034
+ if (options.every((o) => o === "END")) return "END";
3035
+ return options.filter((o) => o !== void 0 && o !== "BEGINNING" && o !== "END").sort((a, b) => a > b ? 1 : -1)[0];
3036
+ };
2834
3037
 
2835
3038
  //#endregion
2836
3039
  //#region src/eventStore/consumers/postgreSQLProcessor.ts
@@ -3042,6 +3245,7 @@ const postgreSQLEventStoreConsumer = (options) => {
3042
3245
  let start;
3043
3246
  let messagePuller;
3044
3247
  const startedAwaiter = asyncAwaiter();
3248
+ const isOwnPool = !options.pool;
3045
3249
  const pool = options.pool ? options.pool : dumbo({
3046
3250
  connectionString: options.connectionString,
3047
3251
  serialization: options.serialization
@@ -3172,7 +3376,7 @@ const postgreSQLEventStoreConsumer = (options) => {
3172
3376
  throw error;
3173
3377
  }
3174
3378
  })));
3175
- console.log(`Starting message pulling with start position: ${JSONSerializer.serialize(startFrom)}. Waiting for messages...`);
3379
+ console.log(`Starting message pulling with start position: ${JSONSerializer$1.serialize(startFrom)}. Waiting for messages...`);
3176
3380
  await messagePuller.start({
3177
3381
  startFrom,
3178
3382
  started: startedAwaiter
@@ -3190,7 +3394,7 @@ const postgreSQLEventStoreConsumer = (options) => {
3190
3394
  stop,
3191
3395
  close: async () => {
3192
3396
  await stop();
3193
- await pool.close();
3397
+ if (isOwnPool) await pool.close();
3194
3398
  }
3195
3399
  };
3196
3400
  };
@@ -3230,5 +3434,5 @@ const rebuildPostgreSQLProjections = (options) => {
3230
3434
  };
3231
3435
 
3232
3436
  //#endregion
3233
- export { DefaultPostgreSQLEventStoreProcessorBatchSize, DefaultPostgreSQLEventStoreProcessorPullingFrequencyInMs, DefaultPostgreSQLProcessorLockPolicy, PostgreSQLEventStoreDefaultStreamVersion, PostgreSQLProjectionSpec, activateProjection, activateProjectionSQL, addDefaultPartitionSQL, addModuleForAllTenantsSQL, addModuleSQL, addPartitionSQL, addTablePartitions, addTenantForAllModulesSQL, addTenantSQL, appendToStream, appendToStreamSQL, assertSQLQueryResultMatches, callActivateProjection, callAppendToStream, callDeactivateProjection, callRegisterProjection, callReleaseProcessorLock, callStoreProcessorCheckpoint, callTryAcquireProcessorLock, callTryAcquireProjectionLock, createEventStoreSchema, currentPostgreSQLEventStoreSchemaVersion, deactivateProjection, deactivateProjectionSQL, defaultPostgreSQLOptions, documentDoesNotExist, documentExists, documentMatchingExists, documentsAreTheSame, documentsMatchingHaveCount, eventInStream, eventStoreSchemaMigrations, eventsInStream, expectPongoDocuments, expectSQL, getPostgreSQLEventStore, handleProjections, messagesTableSQL, newEventsInStream, pastEventStoreSchemaMigrations, pongoMultiStreamProjection, pongoProjection, pongoSingleStreamProjection, postgreSQLCheckpointer, postgreSQLEventStoreConsumer, postgreSQLEventStoreMessageBatchPuller, postgreSQLProcessorLock, postgreSQLProjection, postgreSQLProjectionLock, postgreSQLProjector, postgreSQLRawBatchSQLProjection, postgreSQLRawSQLProjection, postgreSQLReactor, postgreSQLWorkflowProcessor, processorsTableSQL, projectionsTableSQL, readLastMessageGlobalPosition, readMessagesBatch, readProcessorCheckpoint, readProjectionInfo, readStream, rebuildPostgreSQLProjections, registerProjection, registerProjectionSQL, releaseProcessorLockSQL, sanitizeNameSQL, schemaMigration, schemaSQL, storeProcessorCheckpoint, storeSubscriptionCheckpointSQL, streamExists, streamsTableSQL, toProcessorLockKey, toProjectionLockKey, transactionToPostgreSQLProjectionHandlerContext, tryAcquireProcessorLockSQL, tryAcquireProjectionLockSQL, zipPostgreSQLEventStoreMessageBatchPullerStartFrom };
3437
+ export { DefaultPostgreSQLEventStoreProcessorBatchSize, DefaultPostgreSQLEventStoreProcessorPullingFrequencyInMs, DefaultPostgreSQLProcessorLockPolicy, PostgreSQLEventStoreCheckpoint, PostgreSQLEventStoreDefaultStreamVersion, PostgreSQLProjectionSpec, activateProjection, activateProjectionSQL, addDefaultPartitionSQL, addModuleForAllTenantsSQL, addModuleSQL, addPartitionSQL, addTablePartitions, addTenantForAllModulesSQL, addTenantSQL, appendToStream, appendToStreamSQL, appentToStreamRaw, assertSQLQueryResultMatches, callActivateProjection, callAppendToStream, callDeactivateProjection, callRegisterProjection, callReleaseProcessorLock, callStoreProcessorCheckpoint, callTryAcquireProcessorLock, callTryAcquireProjectionLock, createEventStoreSchema, currentPostgreSQLEventStoreSchemaVersion, deactivateProjection, deactivateProjectionSQL, defaultPostgreSQLOptions, documentDoesNotExist, documentExists, documentMatchingExists, documentsAreTheSame, documentsMatchingHaveCount, eventInStream, eventStoreSchemaMigrations, eventsInStream, expectPongoDocuments, expectSQL, getPostgreSQLEventStore, handleProjections, messagesTableSQL, newEventsInStream, pastEventStoreSchemaMigrations, pongoMultiStreamProjection, pongoProjection, pongoSingleStreamProjection, postgreSQLCheckpointer, postgreSQLEventStoreConsumer, postgreSQLEventStoreMessageBatchPuller, postgreSQLProcessorLock, postgreSQLProjection, postgreSQLProjectionLock, postgreSQLProjector, postgreSQLRawBatchSQLProjection, postgreSQLRawSQLProjection, postgreSQLReactor, postgreSQLWorkflowProcessor, processorsTableSQL, projectionsTableSQL, readLastMessageCheckpoint, readMessagesBatch, readProcessorCheckpoint, readProjectionInfo, readStream, rebuildPostgreSQLProjections, registerProjection, registerProjectionSQL, releaseProcessorLockSQL, sanitizeNameSQL, schemaMigration, schemaSQL, storeProcessorCheckpoint, storeSubscriptionCheckpointSQL, streamExists, streamsTableSQL, toProcessorLockKey, toProjectionLockKey, transactionToPostgreSQLProjectionHandlerContext, tryAcquireProcessorLockSQL, tryAcquireProjectionLockSQL, zipPostgreSQLEventStoreMessageBatchPullerStartFrom };
3234
3438
  //# sourceMappingURL=index.js.map