@causa/runtime-google 0.33.1 → 0.35.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.
Files changed (35) hide show
  1. package/README.md +14 -2
  2. package/dist/app-check/guard.js +4 -2
  3. package/dist/identity-platform/identity-platform.strategy.js +4 -2
  4. package/dist/pubsub/publisher.d.ts +7 -13
  5. package/dist/pubsub/publisher.js +27 -17
  6. package/dist/pubsub/publisher.module.js +1 -2
  7. package/dist/spanner/entity-manager.d.ts +22 -18
  8. package/dist/spanner/entity-manager.js +6 -6
  9. package/dist/spanner/index.d.ts +1 -1
  10. package/dist/transaction/firestore-pubsub/runner.js +4 -2
  11. package/dist/transaction/index.d.ts +3 -0
  12. package/dist/transaction/index.js +3 -0
  13. package/dist/transaction/spanner-outbox/event.d.ts +27 -0
  14. package/dist/transaction/spanner-outbox/event.js +63 -0
  15. package/dist/transaction/spanner-outbox/index.d.ts +5 -0
  16. package/dist/transaction/spanner-outbox/index.js +4 -0
  17. package/dist/transaction/spanner-outbox/module.d.ts +28 -0
  18. package/dist/transaction/spanner-outbox/module.js +91 -0
  19. package/dist/transaction/spanner-outbox/runner.d.ts +19 -0
  20. package/dist/transaction/spanner-outbox/runner.js +31 -0
  21. package/dist/transaction/spanner-outbox/sender.d.ts +95 -0
  22. package/dist/transaction/spanner-outbox/sender.js +150 -0
  23. package/dist/transaction/spanner-pubsub/index.d.ts +0 -2
  24. package/dist/transaction/spanner-pubsub/index.js +0 -2
  25. package/dist/transaction/spanner-pubsub/runner.d.ts +3 -3
  26. package/dist/transaction/spanner-pubsub/runner.js +12 -27
  27. package/dist/transaction/{spanner-pubsub/state-transaction.d.ts → spanner-state-transaction.d.ts} +4 -5
  28. package/dist/transaction/{spanner-pubsub/state-transaction.js → spanner-state-transaction.js} +2 -3
  29. package/dist/transaction/spanner-transaction.d.ts +16 -0
  30. package/dist/transaction/spanner-transaction.js +20 -0
  31. package/dist/transaction/spanner-utils.d.ts +9 -0
  32. package/dist/transaction/spanner-utils.js +31 -0
  33. package/package.json +11 -11
  34. package/dist/transaction/spanner-pubsub/transaction.d.ts +0 -17
  35. package/dist/transaction/spanner-pubsub/transaction.js +0 -21
@@ -0,0 +1,91 @@
1
+ import { EVENT_PUBLISHER_INJECTION_NAME, Logger } from '@causa/runtime/nestjs';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { SpannerEntityManager } from '../../spanner/index.js';
4
+ import { SpannerOutboxEvent } from './event.js';
5
+ import { SpannerOutboxTransactionRunner } from './runner.js';
6
+ import { SpannerOutboxSender, } from './sender.js';
7
+ /**
8
+ * Combines options passed to the module with the configuration (from the environment).
9
+ *
10
+ * @param options Options passed to the module.
11
+ * @param config The {@link ConfigService} to use.
12
+ * @returns The parsed {@link SpannerOutboxSenderOptions}.
13
+ */
14
+ function parseSenderOptions(options, config) {
15
+ function validateIntOrUndefined(name) {
16
+ const strValue = config.get(name);
17
+ if (strValue === undefined) {
18
+ return undefined;
19
+ }
20
+ const value = parseInt(strValue);
21
+ if (isNaN(value)) {
22
+ throw new Error(`Environment variable ${name} must be a number.`);
23
+ }
24
+ return value;
25
+ }
26
+ const batchSize = validateIntOrUndefined('SPANNER_OUTBOX_BATCH_SIZE');
27
+ const pollingInterval = validateIntOrUndefined('SPANNER_OUTBOX_POLLING_INTERVAL');
28
+ const idColumn = config.get('SPANNER_OUTBOX_ID_COLUMN');
29
+ const leaseExpirationColumn = config.get('SPANNER_OUTBOX_LEASE_EXPIRATION_COLUMN');
30
+ const index = config.get('SPANNER_OUTBOX_INDEX');
31
+ const shardingColumn = config.get('SPANNER_OUTBOX_SHARDING_COLUMN');
32
+ const shardingCount = validateIntOrUndefined('SPANNER_OUTBOX_SHARDING_COUNT');
33
+ const leaseDuration = validateIntOrUndefined('SPANNER_OUTBOX_LEASE_DURATION');
34
+ const sharding = shardingColumn && shardingCount
35
+ ? { column: shardingColumn, count: shardingCount }
36
+ : undefined;
37
+ const envOptions = {
38
+ batchSize,
39
+ pollingInterval,
40
+ idColumn,
41
+ leaseExpirationColumn,
42
+ index,
43
+ sharding,
44
+ leaseDuration,
45
+ };
46
+ return { ...envOptions, ...options };
47
+ }
48
+ /**
49
+ * The module providing the {@link SpannerOutboxTransactionRunner}.
50
+ * This assumes the `SpannerModule` and an {@link EventPublisher} are available (as well as the `LoggerModule`).
51
+ */
52
+ export class SpannerOutboxTransactionModule {
53
+ /**
54
+ * Initializes the {@link SpannerOutboxTransactionModule} with the given options.
55
+ * The returned module is always global.
56
+ *
57
+ * @param options Options for the {@link SpannerOutboxTransactionModule}.
58
+ * @returns The module.
59
+ */
60
+ static forRoot(options = {}) {
61
+ const { outboxEventType, ...senderOptions } = {
62
+ outboxEventType: SpannerOutboxEvent,
63
+ ...options,
64
+ };
65
+ return {
66
+ module: SpannerOutboxTransactionModule,
67
+ global: true,
68
+ providers: [
69
+ {
70
+ provide: SpannerOutboxSender,
71
+ useFactory: (entityManager, publisher, logger, config) => {
72
+ const options = parseSenderOptions(senderOptions, config);
73
+ return new SpannerOutboxSender(entityManager, outboxEventType, publisher, logger, options);
74
+ },
75
+ inject: [
76
+ SpannerEntityManager,
77
+ EVENT_PUBLISHER_INJECTION_NAME,
78
+ Logger,
79
+ ConfigService,
80
+ ],
81
+ },
82
+ {
83
+ provide: SpannerOutboxTransactionRunner,
84
+ useFactory: (entityManager, sender, logger) => new SpannerOutboxTransactionRunner(entityManager, outboxEventType, sender, logger),
85
+ inject: [SpannerEntityManager, SpannerOutboxSender, Logger],
86
+ },
87
+ ],
88
+ exports: [SpannerOutboxTransactionRunner],
89
+ };
90
+ }
91
+ }
@@ -0,0 +1,19 @@
1
+ import { OutboxTransactionRunner, type OutboxEvent, type OutboxEventTransaction } from '@causa/runtime';
2
+ import { Logger } from '@causa/runtime/nestjs';
3
+ import type { Type } from '@nestjs/common';
4
+ import { SpannerEntityManager } from '../../spanner/index.js';
5
+ import { SpannerTransaction } from '../spanner-transaction.js';
6
+ import { SpannerOutboxSender } from './sender.js';
7
+ /**
8
+ * A {@link SpannerTransaction} that uses an {@link OutboxEventTransaction}.
9
+ */
10
+ export type SpannerOutboxTransaction = SpannerTransaction<OutboxEventTransaction>;
11
+ /**
12
+ * An {@link OutboxTransactionRunner} that uses a {@link SpannerTransaction} to run transactions.
13
+ * Events are stored in a Spanner table before being published.
14
+ */
15
+ export declare class SpannerOutboxTransactionRunner extends OutboxTransactionRunner<SpannerOutboxTransaction> {
16
+ readonly entityManager: SpannerEntityManager;
17
+ constructor(entityManager: SpannerEntityManager, outboxEventType: Type<OutboxEvent>, sender: SpannerOutboxSender, logger: Logger);
18
+ protected runStateTransaction<RT>(eventTransaction: OutboxEventTransaction, runFn: (transaction: SpannerOutboxTransaction) => Promise<RT>): Promise<RT>;
19
+ }
@@ -0,0 +1,31 @@
1
+ import { OutboxTransactionRunner, } from '@causa/runtime';
2
+ import { Logger } from '@causa/runtime/nestjs';
3
+ import { SpannerEntityManager } from '../../spanner/index.js';
4
+ import { SpannerStateTransaction } from '../spanner-state-transaction.js';
5
+ import { SpannerTransaction } from '../spanner-transaction.js';
6
+ import { throwRetryableInTransactionIfNeeded } from '../spanner-utils.js';
7
+ import { SpannerOutboxSender } from './sender.js';
8
+ /**
9
+ * An {@link OutboxTransactionRunner} that uses a {@link SpannerTransaction} to run transactions.
10
+ * Events are stored in a Spanner table before being published.
11
+ */
12
+ export class SpannerOutboxTransactionRunner extends OutboxTransactionRunner {
13
+ entityManager;
14
+ constructor(entityManager, outboxEventType, sender, logger) {
15
+ super(outboxEventType, sender, logger);
16
+ this.entityManager = entityManager;
17
+ }
18
+ async runStateTransaction(eventTransaction, runFn) {
19
+ return await this.entityManager.transaction(async (dbTransaction) => {
20
+ const stateTransaction = new SpannerStateTransaction(this.entityManager, dbTransaction);
21
+ const transaction = new SpannerTransaction(stateTransaction, eventTransaction);
22
+ try {
23
+ return await runFn(transaction);
24
+ }
25
+ catch (error) {
26
+ await throwRetryableInTransactionIfNeeded(error);
27
+ throw error;
28
+ }
29
+ });
30
+ }
31
+ }
@@ -0,0 +1,95 @@
1
+ import { OutboxEventSender, type EventPublisher, type OutboxEvent, type OutboxEventPublishResult, type OutboxEventSenderOptions } from '@causa/runtime';
2
+ import { Logger } from '@causa/runtime/nestjs';
3
+ import type { Type } from '@nestjs/common';
4
+ import { SpannerEntityManager } from '../../spanner/index.js';
5
+ /**
6
+ * Sharding options for the {@link SpannerOutboxSender}.
7
+ */
8
+ export type SpannerOutboxSenderShardingOptions = {
9
+ /**
10
+ * The name of the column used for sharding.
11
+ */
12
+ readonly column: string;
13
+ /**
14
+ * The number of shards.
15
+ */
16
+ readonly count: number;
17
+ };
18
+ /**
19
+ * Options for the {@link SpannerOutboxSender}.
20
+ */
21
+ export type SpannerOutboxSenderOptions = OutboxEventSenderOptions & {
22
+ /**
23
+ * Sharding options.
24
+ * If not set, queries to fetch events will not use sharding.
25
+ */
26
+ readonly sharding?: SpannerOutboxSenderShardingOptions;
27
+ /**
28
+ * The name of the column used to store the event ID.
29
+ * Defaults to `id`.
30
+ */
31
+ readonly idColumn?: string;
32
+ /**
33
+ * The name of the column used to store the lease expiration.
34
+ * Defaults to `leaseExpiration`.
35
+ */
36
+ readonly leaseExpirationColumn?: string;
37
+ /**
38
+ * The index used to fetch events.
39
+ */
40
+ readonly index?: string;
41
+ };
42
+ /**
43
+ * An {@link OutboxEventSender} that uses a Spanner table to store events.
44
+ */
45
+ export declare class SpannerOutboxSender extends OutboxEventSender {
46
+ readonly entityManager: SpannerEntityManager;
47
+ readonly outboxEventType: Type<OutboxEvent>;
48
+ /**
49
+ * Sharding options.
50
+ * If `null`, queries to fetch events will not use sharding.
51
+ */
52
+ readonly sharding: SpannerOutboxSenderShardingOptions | undefined;
53
+ /**
54
+ * The name of the column used for the {@link OutboxEvent.id} property.
55
+ */
56
+ readonly idColumn: string;
57
+ /**
58
+ * The name of the column used for the {@link OutboxEvent.leaseExpiration} property.
59
+ */
60
+ readonly leaseExpirationColumn: string;
61
+ /**
62
+ * The index used to fetch events.
63
+ */
64
+ readonly index: string | undefined;
65
+ /**
66
+ * The SQL query used to fetch events from the outbox.
67
+ */
68
+ readonly fetchEventsSql: string;
69
+ /**
70
+ * The SQL query used to update events in the outbox after they have been successfully published.
71
+ */
72
+ readonly successfulUpdateSql: string;
73
+ /**
74
+ * The SQL query used to update events in the outbox after they have failed to be published.
75
+ */
76
+ readonly failedUpdateSql: string;
77
+ /**
78
+ * Creates a new {@link SpannerOutboxSender}.
79
+ *
80
+ * @param entityManager The {@link SpannerEntityManager} to use to access the outbox.
81
+ * @param outboxEventType The type for the Spanner table used to store outbox events.
82
+ * @param publisher The {@link EventPublisher} to use to publish events.
83
+ * @param logger The {@link Logger} to use.
84
+ * @param options Options for the {@link SpannerOutboxSender}.
85
+ */
86
+ constructor(entityManager: SpannerEntityManager, outboxEventType: Type<OutboxEvent>, publisher: EventPublisher, logger: Logger, options?: SpannerOutboxSenderOptions);
87
+ /**
88
+ * Builds the SQL queries used to fetch and update events in the outbox based on the options.
89
+ *
90
+ * @returns The SQL queries used to fetch and update events in the outbox.
91
+ */
92
+ protected buildSql(): Pick<SpannerOutboxSender, 'fetchEventsSql' | 'successfulUpdateSql' | 'failedUpdateSql'>;
93
+ protected fetchEvents(): Promise<OutboxEvent[]>;
94
+ protected updateOutbox(result: OutboxEventPublishResult): Promise<void>;
95
+ }
@@ -0,0 +1,150 @@
1
+ import { OutboxEventSender, } from '@causa/runtime';
2
+ import { Logger } from '@causa/runtime/nestjs';
3
+ import { SpannerEntityManager } from '../../spanner/index.js';
4
+ /**
5
+ * The default name for the {@link OutboxEvent.id} column.
6
+ */
7
+ const DEFAULT_ID_COLUMN = 'id';
8
+ /**
9
+ * The default name for the {@link OutboxEvent.leaseExpiration} column.
10
+ */
11
+ const DEFAULT_LEASE_EXPIRATION_COLUMN = 'leaseExpiration';
12
+ /**
13
+ * An {@link OutboxEventSender} that uses a Spanner table to store events.
14
+ */
15
+ export class SpannerOutboxSender extends OutboxEventSender {
16
+ entityManager;
17
+ outboxEventType;
18
+ /**
19
+ * Sharding options.
20
+ * If `null`, queries to fetch events will not use sharding.
21
+ */
22
+ sharding;
23
+ /**
24
+ * The name of the column used for the {@link OutboxEvent.id} property.
25
+ */
26
+ idColumn;
27
+ /**
28
+ * The name of the column used for the {@link OutboxEvent.leaseExpiration} property.
29
+ */
30
+ leaseExpirationColumn;
31
+ /**
32
+ * The index used to fetch events.
33
+ */
34
+ index;
35
+ /**
36
+ * The SQL query used to fetch events from the outbox.
37
+ */
38
+ fetchEventsSql;
39
+ /**
40
+ * The SQL query used to update events in the outbox after they have been successfully published.
41
+ */
42
+ successfulUpdateSql;
43
+ /**
44
+ * The SQL query used to update events in the outbox after they have failed to be published.
45
+ */
46
+ failedUpdateSql;
47
+ /**
48
+ * Creates a new {@link SpannerOutboxSender}.
49
+ *
50
+ * @param entityManager The {@link SpannerEntityManager} to use to access the outbox.
51
+ * @param outboxEventType The type for the Spanner table used to store outbox events.
52
+ * @param publisher The {@link EventPublisher} to use to publish events.
53
+ * @param logger The {@link Logger} to use.
54
+ * @param options Options for the {@link SpannerOutboxSender}.
55
+ */
56
+ constructor(entityManager, outboxEventType, publisher, logger, options = {}) {
57
+ super(publisher, logger, options);
58
+ this.entityManager = entityManager;
59
+ this.outboxEventType = outboxEventType;
60
+ this.sharding = options.sharding;
61
+ this.idColumn = options.idColumn ?? DEFAULT_ID_COLUMN;
62
+ this.leaseExpirationColumn =
63
+ options.leaseExpirationColumn ?? DEFAULT_LEASE_EXPIRATION_COLUMN;
64
+ this.index = options.index;
65
+ ({
66
+ fetchEventsSql: this.fetchEventsSql,
67
+ successfulUpdateSql: this.successfulUpdateSql,
68
+ failedUpdateSql: this.failedUpdateSql,
69
+ } = this.buildSql());
70
+ }
71
+ /**
72
+ * Builds the SQL queries used to fetch and update events in the outbox based on the options.
73
+ *
74
+ * @returns The SQL queries used to fetch and update events in the outbox.
75
+ */
76
+ buildSql() {
77
+ const table = this.entityManager.sqlTableName(this.outboxEventType);
78
+ const tableWithIndex = this.entityManager.sqlTableName(this.outboxEventType, { index: this.index });
79
+ let filter = `${this.leaseExpirationColumn} IS NULL OR ${this.leaseExpirationColumn} < @currentTime`;
80
+ if (this.sharding) {
81
+ const { column, count } = this.sharding;
82
+ filter = `${column} BETWEEN 0 AND ${count - 1} AND (${filter})`;
83
+ }
84
+ const fetchEventsSql = `
85
+ UPDATE
86
+ ${table}
87
+ SET
88
+ \`${this.leaseExpirationColumn}\` = @leaseExpiration
89
+ WHERE
90
+ \`${this.idColumn}\` IN (
91
+ SELECT
92
+ \`${this.idColumn}\`
93
+ FROM
94
+ ${tableWithIndex}
95
+ WHERE
96
+ ${filter}
97
+ LIMIT
98
+ @batchSize
99
+ )
100
+ THEN RETURN
101
+ ${this.entityManager.sqlColumns(this.outboxEventType)}`;
102
+ const successfulUpdateSql = `
103
+ DELETE FROM
104
+ ${table}
105
+ WHERE
106
+ \`${this.idColumn}\` IN UNNEST(@ids)`;
107
+ const failedUpdateSql = `
108
+ UPDATE
109
+ ${table}
110
+ SET
111
+ \`${this.leaseExpirationColumn}\` = NULL
112
+ WHERE
113
+ \`${this.idColumn}\` IN UNNEST(@ids)`;
114
+ return { fetchEventsSql, successfulUpdateSql, failedUpdateSql };
115
+ }
116
+ async fetchEvents() {
117
+ return await this.entityManager.transaction(async (transaction) => {
118
+ const currentTime = new Date();
119
+ 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 });
126
+ });
127
+ }
128
+ async updateOutbox(result) {
129
+ const successfulSends = [];
130
+ const failedSends = [];
131
+ Object.entries(result).forEach(([id, success]) => (success ? successfulSends : failedSends).push(id));
132
+ const batchUpdates = [];
133
+ if (successfulSends.length > 0) {
134
+ batchUpdates.push({
135
+ sql: this.successfulUpdateSql,
136
+ params: { ids: successfulSends },
137
+ });
138
+ }
139
+ if (failedSends.length > 0) {
140
+ batchUpdates.push({
141
+ sql: this.failedUpdateSql,
142
+ params: { ids: failedSends },
143
+ });
144
+ }
145
+ if (batchUpdates.length === 0) {
146
+ return;
147
+ }
148
+ await this.entityManager.transaction((transaction) => transaction.batchUpdate(batchUpdates));
149
+ }
150
+ }
@@ -1,4 +1,2 @@
1
1
  export { SpannerPubSubTransactionModule } from './module.js';
2
2
  export { SpannerPubSubTransactionRunner } from './runner.js';
3
- export { SpannerStateTransaction } from './state-transaction.js';
4
- export { SpannerPubSubTransaction } from './transaction.js';
@@ -1,4 +1,2 @@
1
1
  export { SpannerPubSubTransactionModule } from './module.js';
2
2
  export { SpannerPubSubTransactionRunner } from './runner.js';
3
- export { SpannerStateTransaction } from './state-transaction.js';
4
- export { SpannerPubSubTransaction } from './transaction.js';
@@ -2,12 +2,12 @@ import { TransactionRunner } from '@causa/runtime';
2
2
  import { Logger } from '@causa/runtime/nestjs';
3
3
  import { PubSubPublisher } from '../../pubsub/index.js';
4
4
  import { SpannerEntityManager } from '../../spanner/index.js';
5
- import { SpannerPubSubTransaction } from './transaction.js';
5
+ import { SpannerTransaction } from '../spanner-transaction.js';
6
6
  /**
7
7
  * A {@link TransactionRunner} that uses Spanner for state and Pub/Sub for events.
8
8
  * A Spanner transaction is used as the main transaction. If it succeeds, events are published to Pub/Sub outside of it.
9
9
  */
10
- export declare class SpannerPubSubTransactionRunner extends TransactionRunner<SpannerPubSubTransaction> {
10
+ export declare class SpannerPubSubTransactionRunner extends TransactionRunner<SpannerTransaction> {
11
11
  readonly entityManager: SpannerEntityManager;
12
12
  readonly publisher: PubSubPublisher;
13
13
  private readonly logger;
@@ -19,5 +19,5 @@ export declare class SpannerPubSubTransactionRunner extends TransactionRunner<Sp
19
19
  * @param logger The {@link Logger} to use.
20
20
  */
21
21
  constructor(entityManager: SpannerEntityManager, publisher: PubSubPublisher, logger: Logger);
22
- run<T>(runFn: (transaction: SpannerPubSubTransaction) => Promise<T>): Promise<[T]>;
22
+ run<T>(runFn: (transaction: SpannerTransaction) => Promise<T>): Promise<[T]>;
23
23
  }
@@ -7,23 +7,20 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
- import { BufferEventTransaction, TransactionOldTimestampError, TransactionRunner, } from '@causa/runtime';
10
+ var SpannerPubSubTransactionRunner_1;
11
+ import { BufferEventTransaction, TransactionRunner } from '@causa/runtime';
11
12
  import { Logger } from '@causa/runtime/nestjs';
12
13
  import { Injectable } from '@nestjs/common';
13
- import { setTimeout } from 'timers/promises';
14
14
  import { PubSubPublisher } from '../../pubsub/index.js';
15
- import { SpannerEntityManager, TemporarySpannerError, } from '../../spanner/index.js';
16
- import { SpannerStateTransaction } from './state-transaction.js';
17
- import { SpannerPubSubTransaction } from './transaction.js';
18
- /**
19
- * The delay, in milliseconds, over which a timestamp issue is deemed irrecoverable.
20
- */
21
- const ACCEPTABLE_PAST_DATE_DELAY = 25000;
15
+ import { SpannerEntityManager } from '../../spanner/index.js';
16
+ import { SpannerStateTransaction } from '../spanner-state-transaction.js';
17
+ import { SpannerTransaction } from '../spanner-transaction.js';
18
+ import { throwRetryableInTransactionIfNeeded } from '../spanner-utils.js';
22
19
  /**
23
20
  * A {@link TransactionRunner} that uses Spanner for state and Pub/Sub for events.
24
21
  * A Spanner transaction is used as the main transaction. If it succeeds, events are published to Pub/Sub outside of it.
25
22
  */
26
- let SpannerPubSubTransactionRunner = class SpannerPubSubTransactionRunner extends TransactionRunner {
23
+ let SpannerPubSubTransactionRunner = SpannerPubSubTransactionRunner_1 = class SpannerPubSubTransactionRunner extends TransactionRunner {
27
24
  entityManager;
28
25
  publisher;
29
26
  logger;
@@ -39,6 +36,7 @@ let SpannerPubSubTransactionRunner = class SpannerPubSubTransactionRunner extend
39
36
  this.entityManager = entityManager;
40
37
  this.publisher = publisher;
41
38
  this.logger = logger;
39
+ this.logger.setContext(SpannerPubSubTransactionRunner_1.name);
42
40
  }
43
41
  async run(runFn) {
44
42
  this.logger.info('Creating a Spanner Pub/Sub transaction.');
@@ -46,28 +44,15 @@ let SpannerPubSubTransactionRunner = class SpannerPubSubTransactionRunner extend
46
44
  const stateTransaction = new SpannerStateTransaction(this.entityManager, dbTransaction);
47
45
  // This must be inside the Spanner transaction because staged messages should be cleared when the transaction is retried.
48
46
  const eventTransaction = new BufferEventTransaction(this.publisher);
49
- const transaction = new SpannerPubSubTransaction(stateTransaction, eventTransaction);
47
+ const transaction = new SpannerTransaction(stateTransaction, eventTransaction);
50
48
  try {
51
49
  const result = await runFn(transaction);
52
50
  this.logger.info('Committing the Spanner transaction.');
53
51
  return { result, eventTransaction };
54
52
  }
55
53
  catch (error) {
56
- // `TransactionOldTimestampError`s indicate that the transaction is using a timestamp older than what is
57
- // observed in the state (Spanner).
58
- // Throwing a `SpannerTransactionOldTimestampError` will cause the transaction to be retried with a newer
59
- // timestamp.
60
- if (!(error instanceof TransactionOldTimestampError)) {
61
- throw error;
62
- }
63
- const delay = error.delay ?? Infinity;
64
- if (delay >= ACCEPTABLE_PAST_DATE_DELAY) {
65
- throw error;
66
- }
67
- if (delay > 0) {
68
- await setTimeout(delay);
69
- }
70
- throw TemporarySpannerError.retryableInTransaction(error.message);
54
+ await throwRetryableInTransactionIfNeeded(error);
55
+ throw error;
71
56
  }
72
57
  });
73
58
  this.logger.info('Publishing Pub/Sub events.');
@@ -75,7 +60,7 @@ let SpannerPubSubTransactionRunner = class SpannerPubSubTransactionRunner extend
75
60
  return [result];
76
61
  }
77
62
  };
78
- SpannerPubSubTransactionRunner = __decorate([
63
+ SpannerPubSubTransactionRunner = SpannerPubSubTransactionRunner_1 = __decorate([
79
64
  Injectable(),
80
65
  __metadata("design:paramtypes", [SpannerEntityManager,
81
66
  PubSubPublisher,
@@ -1,20 +1,19 @@
1
1
  import type { FindReplaceStateTransaction } from '@causa/runtime';
2
- import { Transaction as SpannerTransaction } from '@google-cloud/spanner';
3
2
  import type { Type } from '@nestjs/common';
4
- import { SpannerEntityManager } from '../../spanner/index.js';
3
+ import { SpannerEntityManager, type SpannerReadWriteTransaction } from '../spanner/index.js';
5
4
  /**
6
5
  * A {@link FindReplaceStateTransaction} that uses Spanner for state storage.
7
6
  */
8
7
  export declare class SpannerStateTransaction implements FindReplaceStateTransaction {
9
8
  readonly entityManager: SpannerEntityManager;
10
- readonly transaction: SpannerTransaction;
9
+ readonly transaction: SpannerReadWriteTransaction;
11
10
  /**
12
11
  * Creates a new {@link SpannerStateTransaction}.
13
12
  *
14
13
  * @param entityManager The {@link SpannerEntityManager} to use to access entities in the state.
15
- * @param transaction The {@link SpannerTransaction} to use for the transaction.
14
+ * @param transaction The {@link SpannerReadWriteTransaction} to use for the transaction.
16
15
  */
17
- constructor(entityManager: SpannerEntityManager, transaction: SpannerTransaction);
16
+ constructor(entityManager: SpannerEntityManager, transaction: SpannerReadWriteTransaction);
18
17
  replace<T extends object>(entity: T): Promise<void>;
19
18
  deleteWithSameKeyAs<T extends object>(type: Type<T>, key: Partial<T>): Promise<void>;
20
19
  findOneWithSameKeyAs<T extends object>(type: Type<T>, entity: Partial<T>): Promise<T | undefined>;
@@ -1,5 +1,4 @@
1
- import { Transaction as SpannerTransaction } from '@google-cloud/spanner';
2
- import { SpannerEntityManager } from '../../spanner/index.js';
1
+ import { SpannerEntityManager, } from '../spanner/index.js';
3
2
  /**
4
3
  * A {@link FindReplaceStateTransaction} that uses Spanner for state storage.
5
4
  */
@@ -10,7 +9,7 @@ export class SpannerStateTransaction {
10
9
  * Creates a new {@link SpannerStateTransaction}.
11
10
  *
12
11
  * @param entityManager The {@link SpannerEntityManager} to use to access entities in the state.
13
- * @param transaction The {@link SpannerTransaction} to use for the transaction.
12
+ * @param transaction The {@link SpannerReadWriteTransaction} to use for the transaction.
14
13
  */
15
14
  constructor(entityManager, transaction) {
16
15
  this.entityManager = entityManager;
@@ -0,0 +1,16 @@
1
+ import { type EventTransaction, Transaction } from '@causa/runtime';
2
+ import { SpannerEntityManager, type SpannerReadWriteTransaction } from '../spanner/index.js';
3
+ import { SpannerStateTransaction } from './spanner-state-transaction.js';
4
+ /**
5
+ * A {@link Transaction} that uses Spanner for state storage, and any available {@link EventTransaction} implementation.
6
+ */
7
+ export declare class SpannerTransaction<ET extends EventTransaction = EventTransaction> extends Transaction<SpannerStateTransaction, ET> {
8
+ /**
9
+ * The underlying {@link SpannerTransaction} used by the state transaction.
10
+ */
11
+ get spannerTransaction(): SpannerReadWriteTransaction;
12
+ /**
13
+ * The underlying {@link SpannerEntityManager} used by the state transaction.
14
+ */
15
+ get entityManager(): SpannerEntityManager;
16
+ }
@@ -0,0 +1,20 @@
1
+ import { Transaction } from '@causa/runtime';
2
+ import { SpannerEntityManager, } from '../spanner/index.js';
3
+ import { SpannerStateTransaction } from './spanner-state-transaction.js';
4
+ /**
5
+ * A {@link Transaction} that uses Spanner for state storage, and any available {@link EventTransaction} implementation.
6
+ */
7
+ export class SpannerTransaction extends Transaction {
8
+ /**
9
+ * The underlying {@link SpannerTransaction} used by the state transaction.
10
+ */
11
+ get spannerTransaction() {
12
+ return this.stateTransaction.transaction;
13
+ }
14
+ /**
15
+ * The underlying {@link SpannerEntityManager} used by the state transaction.
16
+ */
17
+ get entityManager() {
18
+ return this.stateTransaction.entityManager;
19
+ }
20
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Checks if the given error is a `TransactionOldTimestampError` and throws a `TemporarySpannerError` such that the
3
+ * transaction can be retried by the runner.
4
+ * If the {@link TransactionOldTimestampError.delay} is too large, the error is deemed irrecoverable and nothing is
5
+ * thrown (it is up to the caller to handle the error).
6
+ *
7
+ * @param error The error to test.
8
+ */
9
+ export declare function throwRetryableInTransactionIfNeeded(error: unknown): Promise<void>;
@@ -0,0 +1,31 @@
1
+ import { TransactionOldTimestampError } from '@causa/runtime';
2
+ import { setTimeout } from 'timers/promises';
3
+ import { TemporarySpannerError } from '../spanner/index.js';
4
+ /**
5
+ * The delay, in milliseconds, over which a timestamp issue is deemed irrecoverable.
6
+ */
7
+ const ACCEPTABLE_PAST_DATE_DELAY = 25000;
8
+ /**
9
+ * Checks if the given error is a `TransactionOldTimestampError` and throws a `TemporarySpannerError` such that the
10
+ * transaction can be retried by the runner.
11
+ * If the {@link TransactionOldTimestampError.delay} is too large, the error is deemed irrecoverable and nothing is
12
+ * thrown (it is up to the caller to handle the error).
13
+ *
14
+ * @param error The error to test.
15
+ */
16
+ export async function throwRetryableInTransactionIfNeeded(error) {
17
+ // `TransactionOldTimestampError`s indicate that the transaction is using a timestamp older than what is
18
+ // observed in the state (Spanner).
19
+ if (!(error instanceof TransactionOldTimestampError)) {
20
+ return;
21
+ }
22
+ const delay = error.delay ?? Infinity;
23
+ if (delay >= ACCEPTABLE_PAST_DATE_DELAY) {
24
+ return;
25
+ }
26
+ if (delay > 0) {
27
+ await setTimeout(delay);
28
+ }
29
+ // Throwing a `TemporarySpannerError` will cause the transaction to be retried with a newer timestamp.
30
+ throw TemporarySpannerError.retryableInTransaction(error.message);
31
+ }