@causa/runtime-google 0.35.4 → 0.37.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.
@@ -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 SpanerReadWriteTransactionOption = {
21
+ /**
22
+ * The transaction to use.
23
+ */
24
+ readonly transaction?: SpannerReadWriteTransaction;
25
+ };
@@ -1,7 +1,7 @@
1
1
  import type { VersionedEntity } from '@causa/runtime';
2
2
  import { type MakeTestAppFactoryOptions } from '@causa/runtime/nestjs/testing';
3
3
  import { Database, Spanner } from '@google-cloud/spanner';
4
- import type { INestApplication, Type } from '@nestjs/common';
4
+ import type { INestApplication, NestApplicationOptions, Type } from '@nestjs/common';
5
5
  import { CollectionReference } from 'firebase-admin/firestore';
6
6
  import { Test } from 'supertest';
7
7
  import TestAgent from 'supertest/lib/agent.js';
@@ -177,6 +177,10 @@ export declare class GoogleAppFixture {
177
177
  * Options for the {@link makeTestAppFactory} function.
178
178
  */
179
179
  appFactoryOptions?: MakeTestAppFactoryOptions;
180
+ /**
181
+ * Options passed to the NestJS application factory.
182
+ */
183
+ nestApplicationOptions?: NestApplicationOptions;
180
184
  /**
181
185
  * Whether the `AppCheckGuard` should be disabled.
182
186
  * Defaults to `true`.
@@ -180,6 +180,7 @@ export class GoogleAppFixture {
180
180
  ? [appFactoryOptions.overrides]
181
181
  : (appFactoryOptions.overrides ?? []);
182
182
  const app = await createApp(appModule, {
183
+ nestApplicationOptions: options.nestApplicationOptions,
183
184
  appFactory: makeTestAppFactory({
184
185
  ...appFactoryOptions,
185
186
  overrides: [
@@ -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.35.4",
3
+ "version": "0.37.0",
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,33 +35,33 @@
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.8",
38
+ "@nestjs/common": "^11.0.9",
39
39
  "@nestjs/config": "^4.0.0",
40
- "@nestjs/core": "^11.0.8",
40
+ "@nestjs/core": "^11.0.9",
41
41
  "@nestjs/passport": "^11.0.5",
42
42
  "@nestjs/terminus": "^11.0.0",
43
43
  "class-transformer": "^0.5.1",
44
44
  "class-validator": "^0.14.1",
45
45
  "express": "^4.21.2",
46
- "firebase-admin": "^13.0.2",
46
+ "firebase-admin": "^13.1.0",
47
47
  "jsonwebtoken": "^9.0.2",
48
48
  "passport-http-bearer": "^1.0.1",
49
49
  "pino": "^9.6.0",
50
50
  "reflect-metadata": "^0.2.2"
51
51
  },
52
52
  "devDependencies": {
53
- "@nestjs/testing": "^11.0.8",
54
- "@swc/core": "^1.10.14",
53
+ "@nestjs/testing": "^11.0.9",
54
+ "@swc/core": "^1.10.16",
55
55
  "@swc/jest": "^0.2.37",
56
56
  "@tsconfig/node22": "^22.0.0",
57
57
  "@types/jest": "^29.5.14",
58
58
  "@types/jsonwebtoken": "^9.0.8",
59
- "@types/node": "^22.13.1",
59
+ "@types/node": "^22.13.4",
60
60
  "@types/passport-http-bearer": "^1.0.41",
61
61
  "@types/supertest": "^6.0.2",
62
62
  "@types/uuid": "^10.0.0",
63
63
  "dotenv": "^16.4.7",
64
- "eslint": "^9.19.0",
64
+ "eslint": "^9.20.1",
65
65
  "eslint-config-prettier": "^10.0.1",
66
66
  "eslint-plugin-prettier": "^5.2.3",
67
67
  "jest": "^29.7.0",
@@ -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.23.0",
74
+ "typescript-eslint": "^8.24.0",
75
75
  "uuid": "^11.0.5"
76
76
  }
77
77
  }