@causa/runtime-google 0.36.0 → 0.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { type Type as ParamType } from '@google-cloud/spanner/build/src/codec.js
3
3
  import type { TimestampBounds } from '@google-cloud/spanner/build/src/transaction.js';
4
4
  import { type Type } from '@nestjs/common';
5
5
  import { SpannerTableCache } from './table-cache.js';
6
- import type { RecursivePartialEntity } from './types.js';
6
+ import type { RecursivePartialEntity, SpannerReadOnlyTransactionOption, SpannerReadWriteTransactionOption } from './types.js';
7
7
  /**
8
8
  * Any Spanner transaction that can be used for reading.
9
9
  */
@@ -16,24 +16,6 @@ export type SpannerReadWriteTransaction = Transaction;
16
16
  * A key for a Spanner row.
17
17
  */
18
18
  export type SpannerKey = (string | null)[];
19
- /**
20
- * Base options for all write operations.
21
- */
22
- type WriteOperationOptions = {
23
- /**
24
- * The {@link SpannerReadWriteTransaction} to use.
25
- */
26
- transaction?: SpannerReadWriteTransaction;
27
- };
28
- /**
29
- * Base options for all read operations.
30
- */
31
- type ReadOperationOptions = {
32
- /**
33
- * The {@link SpannerReadOnlyTransaction} to use.
34
- */
35
- transaction?: SpannerReadOnlyTransaction;
36
- };
37
19
  /**
38
20
  * Options for {@link SpannerEntityManager.snapshot}.
39
21
  */
@@ -67,7 +49,7 @@ export type SqlStatement = {
67
49
  /**
68
50
  * Options for {@link SpannerEntityManager.query}.
69
51
  */
70
- export type QueryOptions<T> = ReadOperationOptions & {
52
+ export type QueryOptions<T> = SpannerReadOnlyTransactionOption & {
71
53
  /**
72
54
  * The type of entity to return in the list of results.
73
55
  */
@@ -76,7 +58,7 @@ export type QueryOptions<T> = ReadOperationOptions & {
76
58
  /**
77
59
  * Options when reading entities.
78
60
  */
79
- type FindOptions = ReadOperationOptions & {
61
+ type FindOptions = SpannerReadOnlyTransactionOption & {
80
62
  /**
81
63
  * The index to use to look up the entity.
82
64
  */
@@ -231,7 +213,7 @@ export declare class SpannerEntityManager {
231
213
  * @param entityType The type of entity for which the table should be cleared.
232
214
  * @param options The options to use when running the operation.
233
215
  */
234
- clear(entityType: Type, options?: WriteOperationOptions): Promise<void>;
216
+ clear(entityType: Type, options?: SpannerReadWriteTransactionOption): Promise<void>;
235
217
  /**
236
218
  * Runs the given SQL statement in the database.
237
219
  * By default, the statement is run in a {@link SpannerReadOnlyTransaction}. To perform a write operation, pass a
@@ -276,7 +258,7 @@ export declare class SpannerEntityManager {
276
258
  * @param entity The entity or array of entities to insert.
277
259
  * @param options Options for the operation.
278
260
  */
279
- insert(entity: object | object[], options?: WriteOperationOptions): Promise<void>;
261
+ insert(entity: object | object[], options?: SpannerReadWriteTransactionOption): Promise<void>;
280
262
  /**
281
263
  * Replaces the given entities in the database.
282
264
  * If the entity already exists, all columns are overwritten, even if they are not present in the entity (in the case
@@ -288,7 +270,7 @@ export declare class SpannerEntityManager {
288
270
  * @param entity The entity to write.
289
271
  * @param options Options for the operation.
290
272
  */
291
- replace(entity: object | object[], options?: WriteOperationOptions): Promise<void>;
273
+ replace(entity: object | object[], options?: SpannerReadWriteTransactionOption): Promise<void>;
292
274
  /**
293
275
  * Updates the given entity in the database.
294
276
  *
@@ -306,7 +288,7 @@ export declare class SpannerEntityManager {
306
288
  * @param options Options for the operation.
307
289
  * @returns The updated entity.
308
290
  */
309
- update<T>(entityType: Type<T>, update: RecursivePartialEntity<T>, options?: WriteOperationOptions & Pick<FindOptions, 'includeSoftDeletes'> & {
291
+ update<T>(entityType: Type<T>, update: RecursivePartialEntity<T>, options?: SpannerReadWriteTransactionOption & Pick<FindOptions, 'includeSoftDeletes'> & {
310
292
  /**
311
293
  * A function that will be called with the entity before it is updated.
312
294
  * This function can throw an error to prevent the update.
@@ -327,7 +309,7 @@ export declare class SpannerEntityManager {
327
309
  * @param options Options for the operation.
328
310
  * @returns The deleted entity.
329
311
  */
330
- delete<T>(entityType: Type<T>, key: SpannerKey | SpannerKey[number], options?: WriteOperationOptions & Pick<FindOptions, 'includeSoftDeletes'> & {
312
+ delete<T>(entityType: Type<T>, key: SpannerKey | SpannerKey[number], options?: SpannerReadWriteTransactionOption & Pick<FindOptions, 'includeSoftDeletes'> & {
331
313
  /**
332
314
  * A function that will be called with the entity before it is deleted.
333
315
  * This function can throw an error to prevent the deletion.
@@ -1,6 +1,25 @@
1
+ import type { SpannerReadOnlyTransaction, SpannerReadWriteTransaction } from './entity-manager.js';
1
2
  /**
2
3
  * A partial Spanner entity instance, where nested objects can also be partial.
3
4
  */
4
5
  export type RecursivePartialEntity<T> = T extends Date ? T : T extends object ? Partial<T> | {
5
6
  [P in keyof T]?: RecursivePartialEntity<T[P]>;
6
7
  } : T;
8
+ /**
9
+ * Option for a function that accepts a Spanner read-only transaction.
10
+ */
11
+ export type SpannerReadOnlyTransactionOption = {
12
+ /**
13
+ * The transaction to use.
14
+ */
15
+ readonly transaction?: SpannerReadOnlyTransaction;
16
+ };
17
+ /**
18
+ * Option for a function that accepts a Spanner read-write transaction.
19
+ */
20
+ export type SpannerReadWriteTransactionOption = {
21
+ /**
22
+ * The transaction to use.
23
+ */
24
+ readonly transaction?: SpannerReadWriteTransaction;
25
+ };
@@ -1,5 +1,5 @@
1
1
  export { SpannerOutboxEvent } from './event.js';
2
2
  export { SpannerOutboxTransactionModule } from './module.js';
3
3
  export { SpannerOutboxTransactionRunner } from './runner.js';
4
- export type { SpannerOutboxTransaction } from './runner.js';
4
+ export type { SpannerOutboxTransaction, SpannerOutboxTransactionOption, } from './runner.js';
5
5
  export { SpannerOutboxSender } from './sender.js';
@@ -8,6 +8,15 @@ import { SpannerOutboxSender } from './sender.js';
8
8
  * A {@link SpannerTransaction} that uses an {@link OutboxEventTransaction}.
9
9
  */
10
10
  export type SpannerOutboxTransaction = SpannerTransaction<OutboxEventTransaction>;
11
+ /**
12
+ * Option for a function that accepts a {@link SpannerOutboxTransaction}.
13
+ */
14
+ export type SpannerOutboxTransactionOption = {
15
+ /**
16
+ * The transaction to use.
17
+ */
18
+ readonly transaction?: SpannerOutboxTransaction;
19
+ };
11
20
  /**
12
21
  * An {@link OutboxTransactionRunner} that uses a {@link SpannerTransaction} to run transactions.
13
22
  * Events are stored in a Spanner table before being published.
@@ -63,9 +63,15 @@ export declare class SpannerOutboxSender extends OutboxEventSender {
63
63
  */
64
64
  readonly index: string | undefined;
65
65
  /**
66
- * The SQL query used to fetch events from the outbox.
66
+ * The SQL query used to fetch (non-leased) events from the outbox.
67
+ * This should be run in a read-only transaction to avoid table-wide locking.
67
68
  */
68
69
  readonly fetchEventsSql: string;
70
+ /**
71
+ * The SQL query used to acquire a lease on events in the outbox.
72
+ * This is based on event IDs, that are checked again to have a null lease expiration.
73
+ */
74
+ readonly acquireLeaseSql: string;
69
75
  /**
70
76
  * The SQL query used to update events in the outbox after they have been successfully published.
71
77
  */
@@ -89,7 +95,7 @@ export declare class SpannerOutboxSender extends OutboxEventSender {
89
95
  *
90
96
  * @returns The SQL queries used to fetch and update events in the outbox.
91
97
  */
92
- protected buildSql(): Pick<SpannerOutboxSender, 'fetchEventsSql' | 'successfulUpdateSql' | 'failedUpdateSql'>;
98
+ protected buildSql(): Pick<SpannerOutboxSender, 'fetchEventsSql' | 'acquireLeaseSql' | 'successfulUpdateSql' | 'failedUpdateSql'>;
93
99
  protected fetchEvents(): Promise<OutboxEvent[]>;
94
100
  protected updateOutbox(result: OutboxEventPublishResult): Promise<void>;
95
101
  }
@@ -33,9 +33,15 @@ export class SpannerOutboxSender extends OutboxEventSender {
33
33
  */
34
34
  index;
35
35
  /**
36
- * The SQL query used to fetch events from the outbox.
36
+ * The SQL query used to fetch (non-leased) events from the outbox.
37
+ * This should be run in a read-only transaction to avoid table-wide locking.
37
38
  */
38
39
  fetchEventsSql;
40
+ /**
41
+ * The SQL query used to acquire a lease on events in the outbox.
42
+ * This is based on event IDs, that are checked again to have a null lease expiration.
43
+ */
44
+ acquireLeaseSql;
39
45
  /**
40
46
  * The SQL query used to update events in the outbox after they have been successfully published.
41
47
  */
@@ -64,6 +70,7 @@ export class SpannerOutboxSender extends OutboxEventSender {
64
70
  this.index = options.index;
65
71
  ({
66
72
  fetchEventsSql: this.fetchEventsSql,
73
+ acquireLeaseSql: this.acquireLeaseSql,
67
74
  successfulUpdateSql: this.successfulUpdateSql,
68
75
  failedUpdateSql: this.failedUpdateSql,
69
76
  } = this.buildSql());
@@ -76,27 +83,32 @@ export class SpannerOutboxSender extends OutboxEventSender {
76
83
  buildSql() {
77
84
  const table = this.entityManager.sqlTableName(this.outboxEventType);
78
85
  const tableWithIndex = this.entityManager.sqlTableName(this.outboxEventType, { index: this.index });
79
- let filter = `${this.leaseExpirationColumn} IS NULL OR ${this.leaseExpirationColumn} < @currentTime`;
86
+ const noLeaseFilter = `\`${this.leaseExpirationColumn}\` IS NULL OR \`${this.leaseExpirationColumn}\` < @currentTime`;
87
+ let fetchFilter = noLeaseFilter;
80
88
  if (this.sharding) {
81
89
  const { column, count } = this.sharding;
82
- filter = `${column} BETWEEN 0 AND ${count - 1} AND (${filter})`;
90
+ fetchFilter = `\`${column}\` BETWEEN 0 AND ${count - 1} AND (${fetchFilter})`;
83
91
  }
84
92
  const fetchEventsSql = `
93
+ SELECT
94
+ \`${this.idColumn}\` AS id,
95
+ FROM
96
+ ${tableWithIndex}
97
+ WHERE
98
+ ${fetchFilter}
99
+ LIMIT
100
+ @batchSize
101
+ `;
102
+ // This will not be run in the same transaction as `fetchEventsSql`. The `noLeaseFilter` is re-applied to avoid
103
+ // acquiring a lease on outbox events that have since been leased by another process.
104
+ const acquireLeaseSql = `
85
105
  UPDATE
86
106
  ${table}
87
107
  SET
88
108
  \`${this.leaseExpirationColumn}\` = @leaseExpiration
89
109
  WHERE
90
- \`${this.idColumn}\` IN (
91
- SELECT
92
- \`${this.idColumn}\`
93
- FROM
94
- ${tableWithIndex}
95
- WHERE
96
- ${filter}
97
- LIMIT
98
- @batchSize
99
- )
110
+ \`${this.idColumn}\` IN UNNEST(@ids)
111
+ AND (${noLeaseFilter})
100
112
  THEN RETURN
101
113
  ${this.entityManager.sqlColumns(this.outboxEventType)}`;
102
114
  const successfulUpdateSql = `
@@ -111,18 +123,29 @@ export class SpannerOutboxSender extends OutboxEventSender {
111
123
  \`${this.leaseExpirationColumn}\` = NULL
112
124
  WHERE
113
125
  \`${this.idColumn}\` IN UNNEST(@ids)`;
114
- return { fetchEventsSql, successfulUpdateSql, failedUpdateSql };
126
+ return {
127
+ fetchEventsSql,
128
+ acquireLeaseSql,
129
+ successfulUpdateSql,
130
+ failedUpdateSql,
131
+ };
115
132
  }
116
133
  async fetchEvents() {
134
+ // Event IDs are first acquired in an (implicit) read-only transaction, to avoid a lock on the entire table.
135
+ const currentTime = new Date();
136
+ const eventIds = await this.entityManager.query({
137
+ sql: this.fetchEventsSql,
138
+ params: { currentTime, batchSize: this.batchSize },
139
+ });
140
+ if (eventIds.length === 0) {
141
+ return [];
142
+ }
143
+ const ids = eventIds.map(({ id }) => id);
117
144
  return await this.entityManager.transaction(async (transaction) => {
118
145
  const currentTime = new Date();
119
146
  const leaseExpiration = new Date(currentTime.getTime() + this.leaseDuration);
120
- const params = {
121
- leaseExpiration,
122
- currentTime,
123
- batchSize: this.batchSize,
124
- };
125
- return await this.entityManager.query({ transaction, entityType: this.outboxEventType }, { sql: this.fetchEventsSql, params });
147
+ const params = { ids, leaseExpiration, currentTime };
148
+ return await this.entityManager.query({ transaction, entityType: this.outboxEventType }, { sql: this.acquireLeaseSql, params });
126
149
  });
127
150
  }
128
151
  async updateOutbox(result) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@causa/runtime-google",
3
- "version": "0.36.0",
3
+ "version": "0.37.1",
4
4
  "description": "An extension to the Causa runtime SDK (`@causa/runtime`), providing Google-specific features.",
5
5
  "repository": "github:causa-io/runtime-typescript-google",
6
6
  "license": "ISC",
@@ -35,9 +35,9 @@
35
35
  "@google-cloud/spanner": "^7.18.1",
36
36
  "@google-cloud/tasks": "^5.5.2",
37
37
  "@grpc/grpc-js": "^1.12.6",
38
- "@nestjs/common": "^11.0.9",
38
+ "@nestjs/common": "^11.0.10",
39
39
  "@nestjs/config": "^4.0.0",
40
- "@nestjs/core": "^11.0.9",
40
+ "@nestjs/core": "^11.0.10",
41
41
  "@nestjs/passport": "^11.0.5",
42
42
  "@nestjs/terminus": "^11.0.0",
43
43
  "class-transformer": "^0.5.1",
@@ -50,8 +50,8 @@
50
50
  "reflect-metadata": "^0.2.2"
51
51
  },
52
52
  "devDependencies": {
53
- "@nestjs/testing": "^11.0.9",
54
- "@swc/core": "^1.10.16",
53
+ "@nestjs/testing": "^11.0.10",
54
+ "@swc/core": "^1.10.17",
55
55
  "@swc/jest": "^0.2.37",
56
56
  "@tsconfig/node22": "^22.0.0",
57
57
  "@types/jest": "^29.5.14",
@@ -71,7 +71,7 @@
71
71
  "ts-jest": "^29.2.5",
72
72
  "ts-node": "^10.9.2",
73
73
  "typescript": "^5.7.3",
74
- "typescript-eslint": "^8.24.0",
74
+ "typescript-eslint": "^8.24.1",
75
75
  "uuid": "^11.0.5"
76
76
  }
77
77
  }