@powersync/service-core 1.22.0 → 1.23.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.
Files changed (113) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/api/RouteAPI.d.ts +2 -2
  3. package/dist/api/diagnostics.d.ts +12 -1
  4. package/dist/api/diagnostics.js +34 -22
  5. package/dist/api/diagnostics.js.map +1 -1
  6. package/dist/entry/commands/compact-action.js +1 -1
  7. package/dist/entry/commands/compact-action.js.map +1 -1
  8. package/dist/modules/AbstractModule.d.ts +6 -2
  9. package/dist/modules/AbstractModule.js.map +1 -1
  10. package/dist/replication/AbstractReplicator.d.ts +14 -3
  11. package/dist/replication/AbstractReplicator.js +122 -62
  12. package/dist/replication/AbstractReplicator.js.map +1 -1
  13. package/dist/replication/ReplicationModule.d.ts +3 -3
  14. package/dist/replication/ReplicationModule.js.map +1 -1
  15. package/dist/routes/configure-fastify.d.ts +12 -12
  16. package/dist/routes/endpoints/admin.d.ts +24 -24
  17. package/dist/routes/endpoints/admin.js +46 -35
  18. package/dist/routes/endpoints/admin.js.map +1 -1
  19. package/dist/routes/endpoints/checkpointing.js +1 -1
  20. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  21. package/dist/routes/endpoints/socket-route.js +1 -1
  22. package/dist/routes/endpoints/socket-route.js.map +1 -1
  23. package/dist/routes/endpoints/sync-rules.js +22 -13
  24. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  25. package/dist/routes/endpoints/sync-stream.js +1 -1
  26. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  27. package/dist/runner/teardown.js +8 -8
  28. package/dist/runner/teardown.js.map +1 -1
  29. package/dist/storage/BucketStorageBatch.d.ts +0 -6
  30. package/dist/storage/BucketStorageBatch.js.map +1 -1
  31. package/dist/storage/BucketStorageFactory.d.ts +22 -27
  32. package/dist/storage/BucketStorageFactory.js +4 -18
  33. package/dist/storage/BucketStorageFactory.js.map +1 -1
  34. package/dist/storage/ParsedSyncConfigSet.d.ts +23 -0
  35. package/dist/storage/ParsedSyncConfigSet.js +2 -0
  36. package/dist/storage/ParsedSyncConfigSet.js.map +1 -0
  37. package/dist/storage/PersistedReplicationStream.d.ts +32 -0
  38. package/dist/storage/PersistedReplicationStream.js +26 -0
  39. package/dist/storage/PersistedReplicationStream.js.map +1 -0
  40. package/dist/storage/PersistedSyncConfigContent.d.ts +63 -0
  41. package/dist/storage/PersistedSyncConfigContent.js +125 -0
  42. package/dist/storage/PersistedSyncConfigContent.js.map +1 -0
  43. package/dist/storage/PersistedSyncConfigStatus.d.ts +13 -0
  44. package/dist/storage/PersistedSyncConfigStatus.js +2 -0
  45. package/dist/storage/PersistedSyncConfigStatus.js.map +1 -0
  46. package/dist/storage/SourceTable.d.ts +9 -3
  47. package/dist/storage/SourceTable.js +13 -3
  48. package/dist/storage/SourceTable.js.map +1 -1
  49. package/dist/storage/SyncRulesBucketStorage.d.ts +59 -16
  50. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  51. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +117 -0
  52. package/dist/storage/implementation/BucketDefinitionMapping.js +277 -0
  53. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
  54. package/dist/storage/implementation/IncrementalReprocessingCompatibility.d.ts +6 -0
  55. package/dist/storage/implementation/IncrementalReprocessingCompatibility.js +31 -0
  56. package/dist/storage/implementation/IncrementalReprocessingCompatibility.js.map +1 -0
  57. package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.d.ts +17 -0
  58. package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js +62 -0
  59. package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js.map +1 -0
  60. package/dist/storage/implementation/ReplicationStreamStorageIds.d.ts +15 -0
  61. package/dist/storage/implementation/ReplicationStreamStorageIds.js +20 -0
  62. package/dist/storage/implementation/ReplicationStreamStorageIds.js.map +1 -0
  63. package/dist/storage/storage-index.d.ts +8 -1
  64. package/dist/storage/storage-index.js +8 -1
  65. package/dist/storage/storage-index.js.map +1 -1
  66. package/dist/sync/sync.js +5 -0
  67. package/dist/sync/sync.js.map +1 -1
  68. package/dist/util/checkpointing.js +1 -1
  69. package/dist/util/checkpointing.js.map +1 -1
  70. package/dist/util/config/types.d.ts +1 -1
  71. package/dist/util/lsn.d.ts +4 -0
  72. package/dist/util/lsn.js +10 -0
  73. package/dist/util/lsn.js.map +1 -1
  74. package/package.json +6 -7
  75. package/src/api/RouteAPI.ts +2 -2
  76. package/src/api/diagnostics.ts +43 -26
  77. package/src/entry/commands/compact-action.ts +1 -1
  78. package/src/modules/AbstractModule.ts +6 -2
  79. package/src/replication/AbstractReplicator.ts +145 -65
  80. package/src/replication/ReplicationModule.ts +2 -3
  81. package/src/routes/endpoints/admin.ts +72 -50
  82. package/src/routes/endpoints/checkpointing.ts +1 -1
  83. package/src/routes/endpoints/socket-route.ts +1 -1
  84. package/src/routes/endpoints/sync-rules.ts +23 -13
  85. package/src/routes/endpoints/sync-stream.ts +1 -1
  86. package/src/runner/teardown.ts +8 -8
  87. package/src/storage/BucketStorageBatch.ts +0 -7
  88. package/src/storage/BucketStorageFactory.ts +30 -39
  89. package/src/storage/ParsedSyncConfigSet.ts +25 -0
  90. package/src/storage/PersistedReplicationStream.ts +55 -0
  91. package/src/storage/PersistedSyncConfigContent.ts +192 -0
  92. package/src/storage/PersistedSyncConfigStatus.ts +14 -0
  93. package/src/storage/SourceTable.ts +20 -3
  94. package/src/storage/SyncRulesBucketStorage.ts +64 -16
  95. package/src/storage/implementation/BucketDefinitionMapping.ts +436 -0
  96. package/src/storage/implementation/IncrementalReprocessingCompatibility.ts +41 -0
  97. package/src/storage/implementation/IncrementalReprocessingSyncConfigLog.ts +109 -0
  98. package/src/storage/implementation/ReplicationStreamStorageIds.ts +21 -0
  99. package/src/storage/storage-index.ts +9 -1
  100. package/src/sync/sync.ts +5 -0
  101. package/src/util/checkpointing.ts +1 -1
  102. package/src/util/config/types.ts +1 -1
  103. package/src/util/lsn.ts +9 -0
  104. package/test/src/diagnostics.test.ts +62 -34
  105. package/test/src/module-loader.test.ts +33 -1
  106. package/test/src/routes/admin.test.ts +182 -18
  107. package/test/src/routes/mocks.ts +6 -2
  108. package/tsconfig.json +1 -4
  109. package/tsconfig.tsbuildinfo +1 -1
  110. package/dist/storage/PersistedSyncRulesContent.d.ts +0 -59
  111. package/dist/storage/PersistedSyncRulesContent.js +0 -100
  112. package/dist/storage/PersistedSyncRulesContent.js.map +0 -1
  113. package/src/storage/PersistedSyncRulesContent.ts +0 -174
@@ -1 +1 @@
1
- {"version":3,"file":"lsn.js","sourceRoot":"","sources":["../../src/util/lsn.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,MAAM,CAAC,CAA4B,EAAE,CAA4B;IAC/E,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,CAAC,IAAI,IAAI,CAAC;IAChC,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,CAAC,CAAC;IACxB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC"}
1
+ {"version":3,"file":"lsn.js","sourceRoot":"","sources":["../../src/util/lsn.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,MAAM,CAAC,CAA4B,EAAE,CAA4B;IAC/E,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,CAAC,IAAI,IAAI,CAAC;IAChC,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,CAAC,CAAC;IACxB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,MAAM,CAAC,CAA4B,EAAE,CAA4B;IAC/E,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,CAAC,IAAI,IAAI,CAAC;IAChC,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,CAAC,CAAC;IACxB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC"}
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@powersync/service-core",
3
3
  "repository": "https://github.com/powersync-ja/powersync-service",
4
- "types": "dist/index.d.ts",
5
4
  "publishConfig": {
6
5
  "access": "public"
7
6
  },
8
- "version": "1.22.0",
9
- "main": "dist/index.js",
7
+ "version": "1.23.1",
10
8
  "license": "FSL-1.1-ALv2",
11
9
  "type": "module",
10
+ "exports": "./dist/index.js",
12
11
  "dependencies": {
13
12
  "@js-sdsl/ordered-set": "^4.4.2",
14
13
  "@opentelemetry/api": "^1.9.0",
@@ -33,11 +32,11 @@
33
32
  "uuid": "^14.0.0",
34
33
  "winston": "^3.13.0",
35
34
  "yaml": "^2.8.3",
36
- "@powersync/lib-services-framework": "0.9.5",
35
+ "@powersync/lib-services-framework": "0.9.6",
37
36
  "@powersync/service-jsonbig": "0.17.13",
38
- "@powersync/service-rsocket-router": "0.2.21",
39
- "@powersync/service-sync-rules": "0.37.0",
40
- "@powersync/service-types": "0.15.2"
37
+ "@powersync/service-rsocket-router": "0.2.22",
38
+ "@powersync/service-sync-rules": "0.38.0",
39
+ "@powersync/service-types": "0.16.0"
41
40
  },
42
41
  "devDependencies": {
43
42
  "@types/async": "^3.2.24",
@@ -1,6 +1,6 @@
1
1
  import { SyncConfig, TablePattern } from '@powersync/service-sync-rules';
2
2
  import * as types from '@powersync/service-types';
3
- import { ParseSyncRulesOptions, SyncRulesBucketStorage } from '../storage/storage-index.js';
3
+ import { ParseSyncConfigOptions, SyncRulesBucketStorage } from '../storage/storage-index.js';
4
4
 
5
5
  export interface PatternResult {
6
6
  schema: string;
@@ -94,7 +94,7 @@ export interface RouteAPI {
94
94
  /**
95
95
  * Get the default schema (or database) when only a table name is specified in sync config.
96
96
  */
97
- getParseSyncRulesOptions(): ParseSyncRulesOptions;
97
+ getParseSyncRulesOptions(): ParseSyncConfigOptions;
98
98
  }
99
99
 
100
100
  export type ReplicationHeadCallback<T> = (head: string) => Promise<T>;
@@ -1,4 +1,4 @@
1
- import { logger } from '@powersync/lib-services-framework';
1
+ import { logger, ServiceAssertionError } from '@powersync/lib-services-framework';
2
2
  import { DEFAULT_TAG, SourceTableRef, SyncConfigWithErrors } from '@powersync/service-sync-rules';
3
3
  import { ReplicationError, SyncRulesStatus, TableInfo } from '@powersync/service-types';
4
4
 
@@ -19,6 +19,11 @@ export interface DiagnosticsOptions {
19
19
  */
20
20
  live_status: boolean;
21
21
 
22
+ /**
23
+ * Whether this is the active sync config or not.
24
+ */
25
+ active: boolean;
26
+
22
27
  /**
23
28
  * Check against the source postgres connection.
24
29
  */
@@ -28,12 +33,18 @@ export interface DiagnosticsOptions {
28
33
  export const DEFAULT_DATASOURCE_ID = 'default';
29
34
 
30
35
  export async function getSyncRulesStatus(
31
- bucketStorage: storage.BucketStorageFactory,
32
36
  apiHandler: RouteAPI,
33
- sync_rules: storage.PersistedSyncRulesContent | null,
34
- options: DiagnosticsOptions
37
+ syncConfig: storage.PersistedSyncConfigContent | null,
38
+ options: DiagnosticsOptions,
39
+ /**
40
+ * Storage instance for the replication stream of this config.
41
+ *
42
+ * Required to populate live status (snapshot/checkpoint info). The content object
43
+ * itself is no longer a replication stream, so the caller must resolve this.
44
+ */
45
+ systemStorage?: storage.SyncRulesBucketStorage
35
46
  ): Promise<SyncRulesStatus | undefined> {
36
- if (sync_rules == null) {
47
+ if (syncConfig == null) {
37
48
  return undefined;
38
49
  }
39
50
 
@@ -43,13 +54,18 @@ export async function getSyncRulesStatus(
43
54
  const now = new Date().toISOString();
44
55
 
45
56
  let parsed: SyncConfigWithErrors;
46
- let persisted: storage.PersistedSyncRules;
57
+ let persisted: storage.ParsedSyncConfigSet;
47
58
  try {
48
- persisted = sync_rules.parsed(apiHandler.getParseSyncRulesOptions());
49
- parsed = persisted.syncConfigWithErrors;
59
+ persisted = syncConfig.parsed(apiHandler.getParseSyncRulesOptions());
60
+ // A content object represents a single sync config, so its parsed result has exactly one entry.
61
+ const [singleConfig] = persisted.syncConfigs;
62
+ if (singleConfig == null) {
63
+ throw new ServiceAssertionError('Expected one sync config');
64
+ }
65
+ parsed = singleConfig;
50
66
  } catch (e) {
51
67
  return {
52
- content: include_content ? sync_rules.sync_rules_content : undefined,
68
+ content: include_content ? syncConfig.sync_rules_content : undefined,
53
69
  connections: [],
54
70
  errors: [{ level: 'fatal', message: e.message, ts: now }]
55
71
  };
@@ -60,8 +76,7 @@ export async function getSyncRulesStatus(
60
76
  // This method can run under some situations if no connection is configured yet.
61
77
  // It will return a default tag in such a case. This default tag is not module specific.
62
78
  const tag = sourceConfig.tag ?? DEFAULT_TAG;
63
- const systemStorage = live_status ? bucketStorage.getInstance(sync_rules) : undefined;
64
- const status = await systemStorage?.getStatus();
79
+ const status = live_status ? await systemStorage?.getStatus() : undefined;
65
80
  let replication_lag_bytes: number | undefined = undefined;
66
81
  let slot_wal_budget: SlotWalBudgetInfo | undefined = undefined;
67
82
 
@@ -90,10 +105,10 @@ export async function getSyncRulesStatus(
90
105
  logger.warn(`Unable to get replication lag`, e);
91
106
  }
92
107
 
93
- if (apiHandler.getSlotWalBudget && sync_rules.slot_name) {
108
+ if (apiHandler.getSlotWalBudget && syncConfig.replicationStreamName) {
94
109
  try {
95
110
  slot_wal_budget = await apiHandler.getSlotWalBudget({
96
- slotName: sync_rules.slot_name
111
+ slotName: syncConfig.replicationStreamName
97
112
  });
98
113
  } catch (e) {
99
114
  logger.warn(`Unable to get WAL budget`, e);
@@ -136,11 +151,13 @@ export async function getSyncRulesStatus(
136
151
  }
137
152
 
138
153
  const errors = tables_flat.flatMap((info) => info.errors);
139
- if (sync_rules.last_fatal_error) {
154
+ const statusSource = await syncConfig.getSyncConfigStatus();
155
+
156
+ if (statusSource?.last_fatal_error) {
140
157
  errors.push({
141
158
  level: 'fatal',
142
- message: sync_rules.last_fatal_error,
143
- ts: sync_rules.last_fatal_error_ts?.toISOString()
159
+ message: statusSource.last_fatal_error,
160
+ ts: statusSource.last_fatal_error_ts?.toISOString()
144
161
  });
145
162
  }
146
163
  errors.push(...syncRuleErrors.map((error) => syncConfigYamlErrorToReplicationError(error, now)));
@@ -168,10 +185,10 @@ export async function getSyncRulesStatus(
168
185
  }
169
186
  }
170
187
 
171
- if (live_status && status?.active) {
188
+ if (live_status && options.active) {
172
189
  // Check replication lag for active replication stream.
173
190
  // Right now we exclude mysql, since it we don't have consistent keepalives for it.
174
- if (sync_rules.last_checkpoint_ts == null && sync_rules.last_keepalive_ts == null) {
191
+ if (statusSource?.last_checkpoint_ts == null && statusSource?.last_keepalive_ts == null) {
175
192
  errors.push({
176
193
  level: 'warning',
177
194
  message: 'No checkpoint found, cannot calculate replication lag',
@@ -179,8 +196,8 @@ export async function getSyncRulesStatus(
179
196
  });
180
197
  } else {
181
198
  const lastTime = Math.max(
182
- sync_rules.last_checkpoint_ts?.getTime() ?? 0,
183
- sync_rules.last_keepalive_ts?.getTime() ?? 0
199
+ statusSource.last_checkpoint_ts?.getTime() ?? 0,
200
+ statusSource.last_keepalive_ts?.getTime() ?? 0
184
201
  );
185
202
  const lagSeconds = Math.round((Date.now() - lastTime) / 1000);
186
203
  // On idle instances, keepalive messages are only persisted every 60 seconds.
@@ -204,17 +221,17 @@ export async function getSyncRulesStatus(
204
221
  }
205
222
 
206
223
  return {
207
- content: include_content ? sync_rules.sync_rules_content : undefined,
224
+ content: include_content ? syncConfig.sync_rules_content : undefined,
208
225
  connections: [
209
226
  {
210
227
  id: sourceConfig.id ?? DEFAULT_DATASOURCE_ID,
211
228
  tag: tag,
212
- slot_name: sync_rules.slot_name,
213
- initial_replication_done: status?.snapshot_done ?? false,
229
+ slot_name: syncConfig.replicationStreamName,
230
+ initial_replication_done: status?.snapshotDone ?? false,
214
231
  // TODO: Rename?
215
- last_lsn: status?.checkpoint_lsn ?? undefined,
216
- last_checkpoint_ts: sync_rules.last_checkpoint_ts?.toISOString(),
217
- last_keepalive_ts: sync_rules.last_keepalive_ts?.toISOString(),
232
+ last_lsn: status?.resumeLsn ?? undefined,
233
+ last_checkpoint_ts: statusSource?.last_checkpoint_ts?.toISOString(),
234
+ last_keepalive_ts: statusSource?.last_keepalive_ts?.toISOString(),
218
235
  replication_lag_bytes: replication_lag_bytes,
219
236
  wal_status: slot_wal_budget?.wal_status,
220
237
  safe_wal_size: slot_wal_budget?.safe_wal_size,
@@ -83,7 +83,7 @@ export function registerCompactAction(program: Command) {
83
83
  await serviceContext.lifeCycleEngine.start();
84
84
  const bucketStorage = serviceContext.storageEngine.activeBucketStorage;
85
85
 
86
- const active = await bucketStorage.getActiveStorage();
86
+ const active = (await bucketStorage.getActiveSyncConfig())?.storage;
87
87
  if (active == null) {
88
88
  logger.info('No active instance to compact');
89
89
  return;
@@ -1,13 +1,17 @@
1
1
  import { logger } from '@powersync/lib-services-framework';
2
2
  import winston from 'winston';
3
- import { PersistedSyncRulesContent } from '../storage/storage-index.js';
3
+ import { PersistedReplicationStream } from '../storage/storage-index.js';
4
4
  import { ServiceContextContainer } from '../system/ServiceContext.js';
5
5
 
6
6
  export interface TearDownOptions {
7
7
  /**
8
8
  * If required, tear down any configuration/state for the specific replication stream
9
9
  */
10
- syncRules?: PersistedSyncRulesContent[];
10
+ replicationStreams?: PersistedReplicationStream[];
11
+ /**
12
+ * @deprecated Use replicationStreams.
13
+ */
14
+ syncRules?: PersistedReplicationStream[];
11
15
  }
12
16
 
13
17
  export interface AbstractModuleOptions {
@@ -1,6 +1,7 @@
1
1
  import { container, ErrorCode, logger } from '@powersync/lib-services-framework';
2
2
  import { ReplicationMetric } from '@powersync/service-types';
3
3
  import { hrtime } from 'node:process';
4
+ import { setTimeout as sleep } from 'node:timers/promises';
4
5
  import winston from 'winston';
5
6
  import { MetricsEngine } from '../metrics/MetricsEngine.js';
6
7
  import * as storage from '../storage/storage-index.js';
@@ -13,6 +14,12 @@ import { ConnectionTestResult } from './ReplicationModule.js';
13
14
  // 1 minute
14
15
  const PING_INTERVAL = 1_000_000_000n * 60n;
15
16
 
17
+ // In the initial startup period, we use a short refresh interval. This helps to take over replication quickly in the case
18
+ // of rolling deploys. After the initial period, we switch to a longer refresh interval to reduce load.
19
+ const FAST_REFRESH_INTERVAL_MS = 500;
20
+ const FAST_REFRESH_TIMEOUT_MS = 120_000;
21
+ const REFRESH_INTERVAL_MS = 5_000;
22
+
16
23
  export interface CreateJobOptions {
17
24
  lock: storage.ReplicationLock;
18
25
  storage: storage.SyncRulesBucketStorage;
@@ -36,12 +43,12 @@ export interface AbstractReplicatorOptions {
36
43
  */
37
44
  export abstract class AbstractReplicator<T extends AbstractReplicationJob = AbstractReplicationJob> {
38
45
  protected logger: winston.Logger;
39
- private lockAlerted: boolean = false;
46
+ private lastReplicationStreamInfoLogs = new Map<string, string>();
40
47
  /**
41
- * Map of replication jobs by replication stream id. Usually there is only one running job, but there could be two when
42
- * transitioning to a new replication stream.
48
+ * Map of replication jobs by replication stream job id. Usually there is only one running job, but there could be two
49
+ * when transitioning to a new replication stream.
43
50
  */
44
- private replicationJobs = new Map<number, T>();
51
+ private replicationJobs = new Map<string, T>();
45
52
 
46
53
  /**
47
54
  * Map of replciation stream ids to promises that are clearing the replication stream.
@@ -131,17 +138,20 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
131
138
  }
132
139
 
133
140
  private async runLoop() {
134
- const syncRules = await this.syncRuleProvider.get();
141
+ const loadedSyncConfig = await this.syncRuleProvider.get();
135
142
 
136
143
  let configuredLock: storage.ReplicationLock | undefined = undefined;
137
- if (syncRules != null) {
144
+ if (loadedSyncConfig != null) {
138
145
  this.logger.info('Loaded sync config');
139
146
  try {
140
147
  // Configure new sync config, if they have changed.
141
148
  // In that case, also immediately take out a lock, so that another process doesn't start replication on it.
149
+ // This upfront lock is mostly superseded by the replicationStreamLoadedSyncConfigMatch() check. However, that doesn't cover old
150
+ // versions before that check was added, so we keep the lock for now - for where te service version and sync config is updated at
151
+ // the same time.
142
152
 
143
153
  const { lock } = await this.storage.configureSyncRules(
144
- storage.updateSyncRulesFromYaml(syncRules, { lock: true, validate: this.syncRuleProvider.exitOnError })
154
+ storage.updateSyncRulesFromYaml(loadedSyncConfig, { lock: true, validate: this.syncRuleProvider.exitOnError })
145
155
  );
146
156
  if (lock) {
147
157
  configuredLock = lock;
@@ -155,10 +165,15 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
155
165
  } else {
156
166
  this.logger.info('No sync streams or rules configured - configure via API');
157
167
  }
168
+ let useFastRefresh = true;
169
+ const fastRefreshDeadline = Date.now() + FAST_REFRESH_TIMEOUT_MS;
158
170
  while (!this.stopped) {
159
171
  await container.probes.touch();
160
172
  try {
161
- await this.refresh({ configured_lock: configuredLock });
173
+ const refreshResult = await this.refresh({ configuredLock, loadedSyncConfig });
174
+ if (refreshResult.replicationJobStarted || Date.now() >= fastRefreshDeadline) {
175
+ useFastRefresh = false;
176
+ }
162
177
  // The lock is only valid on the first refresh.
163
178
  configuredLock = undefined;
164
179
 
@@ -177,77 +192,59 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
177
192
  } catch (e) {
178
193
  this.logger.error('Failed to refresh replication jobs', e);
179
194
  }
180
- await new Promise((resolve) => setTimeout(resolve, 5000));
195
+ if (Date.now() >= fastRefreshDeadline) {
196
+ useFastRefresh = false;
197
+ }
198
+ await sleep(useFastRefresh ? FAST_REFRESH_INTERVAL_MS : REFRESH_INTERVAL_MS);
181
199
  }
182
200
  }
183
201
 
184
- private async refresh(options?: { configured_lock?: storage.ReplicationLock }) {
202
+ private async refresh(options?: {
203
+ configuredLock?: storage.ReplicationLock;
204
+ loadedSyncConfig?: string;
205
+ }): Promise<{ replicationJobStarted: boolean }> {
185
206
  if (this.stopped) {
186
- return;
207
+ return { replicationJobStarted: false };
187
208
  }
188
209
 
189
- let configuredLock = options?.configured_lock;
210
+ let configuredLock = options?.configuredLock;
211
+ let replicationJobStarted = false;
190
212
 
191
- const existingJobs = new Map<number, T>(this.replicationJobs.entries());
192
- const replicatingSyncRules = await this.storage.getReplicatingSyncRules();
193
- const newJobs = new Map<number, T>();
213
+ const existingJobs = new Map<string, T>(this.replicationJobs.entries());
214
+ const replicatingStreams = await this.storage.getReplicatingReplicationStreams();
215
+ const newJobs = new Map<string, T>();
216
+ const streamsToStart: storage.PersistedReplicationStream[] = [];
194
217
  let activeJob: T | undefined = undefined;
195
- for (let syncRules of replicatingSyncRules) {
196
- const existingJob = existingJobs.get(syncRules.id);
197
- if (syncRules.active && activeJob == null) {
218
+ for (let replicationStream of replicatingStreams) {
219
+ const jobId = replicationStream.replicationJobId;
220
+ const existingJob = existingJobs.get(jobId);
221
+ const syncConfigMismatchMessage = 'Ignoring replication stream for sync config not loaded by this process';
222
+ if (!this.shouldHandleReplicationStream(replicationStream, options?.loadedSyncConfig)) {
223
+ this.logReplicationStreamInfoOnce(replicationStream, 'sync-config-mismatch', () => {
224
+ replicationStream.logger.info(syncConfigMismatchMessage);
225
+ });
226
+ continue;
227
+ }
228
+ this.clearReplicationStreamInfoLog(replicationStream, 'sync-config-mismatch');
229
+ if (replicationStream.state == storage.SyncRuleState.ACTIVE && activeJob == null) {
198
230
  activeJob = existingJob;
199
231
  }
200
232
  if (existingJob && !existingJob.isStopped) {
201
233
  // No change
202
- existingJobs.delete(syncRules.id);
203
- newJobs.set(syncRules.id, existingJob);
234
+ existingJobs.delete(jobId);
235
+ newJobs.set(jobId, existingJob);
204
236
  } else if (existingJob && existingJob.isStopped) {
205
237
  // Stopped (e.g. fatal error).
206
238
  // Remove from the list. Next refresh call will restart the job.
207
- existingJobs.delete(syncRules.id);
239
+ existingJobs.delete(jobId);
208
240
  } else {
209
241
  // New sync config was found (or resume after restart)
210
- try {
211
- let lock: storage.ReplicationLock;
212
- if (configuredLock?.sync_rules_id == syncRules.id) {
213
- lock = configuredLock;
214
- } else {
215
- lock = await syncRules.lock();
216
- }
217
- const storage = this.storage.getInstance(syncRules);
218
- const newJob = this.createJob({
219
- lock: lock,
220
- storage: storage
221
- });
222
-
223
- newJobs.set(syncRules.id, newJob);
224
- newJob.start();
225
- if (syncRules.active) {
226
- activeJob = newJob;
227
- }
228
- this.lockAlerted = false;
229
- } catch (e) {
230
- if (e?.errorData?.code === ErrorCode.PSYNC_S1003) {
231
- if (!this.lockAlerted) {
232
- syncRules.logger.info(`[${e.errorData.code}] ${e.errorData.description}`);
233
- this.lockAlerted = true;
234
- }
235
- } else {
236
- // Could be a sync config parse error,
237
- // for example from stricter validation that was added.
238
- // This will be retried every couple of seconds.
239
- // When new (valid) sync config is deployed and processed, this one be disabled.
240
- syncRules.logger.error('Failed to start replication for new sync config', e);
241
- }
242
- }
242
+ streamsToStart.push(replicationStream);
243
243
  }
244
244
  }
245
245
 
246
- this.replicationJobs = newJobs;
247
- this.activeReplicationJob = activeJob;
248
-
249
- // Stop any orphaned jobs that no longer have a replication stream.
250
- // Termination happens below
246
+ // Stop any orphaned jobs that no longer have a replication stream before starting replacements.
247
+ // Termination happens below.
251
248
  for (let job of existingJobs.values()) {
252
249
  // Old - stop and clean up
253
250
  try {
@@ -258,10 +255,50 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
258
255
  }
259
256
  }
260
257
 
258
+ for (let replicationStream of streamsToStart) {
259
+ const jobId = replicationStream.replicationJobId;
260
+ try {
261
+ let lock: storage.ReplicationLock;
262
+ if (configuredLock?.sync_rules_id == replicationStream.replicationStreamId) {
263
+ lock = configuredLock;
264
+ } else {
265
+ lock = await replicationStream.lock();
266
+ }
267
+ const syncRuleStorage = this.storage.getInstance(replicationStream);
268
+ const newJob = this.createJob({
269
+ lock: lock,
270
+ storage: syncRuleStorage
271
+ });
272
+
273
+ newJobs.set(jobId, newJob);
274
+ newJob.start();
275
+ replicationJobStarted = true;
276
+ if (replicationStream.state == storage.SyncRuleState.ACTIVE) {
277
+ activeJob = newJob;
278
+ }
279
+ this.lastReplicationStreamInfoLogs.delete(replicationStream.replicationStreamName);
280
+ } catch (e) {
281
+ if (e?.errorData?.code === ErrorCode.PSYNC_S1003) {
282
+ this.logReplicationStreamInfoOnce(replicationStream, 'replication-stream-locked', () => {
283
+ replicationStream.logger.info(`[${e.errorData.code}] ${e.errorData.description}`);
284
+ });
285
+ } else {
286
+ // Could be a sync config parse error,
287
+ // for example from stricter validation that was added.
288
+ // This will be retried every couple of seconds.
289
+ // When new (valid) sync config is deployed and processed, this one be disabled.
290
+ replicationStream.logger.error('Failed to start replication for new sync config', e);
291
+ }
292
+ }
293
+ }
294
+
295
+ this.replicationJobs = newJobs;
296
+ this.activeReplicationJob = activeJob;
297
+
261
298
  // Replication stream stopped previously, including by a different process.
262
- const stopped = await this.storage.getStoppedSyncRules();
263
- for (let syncRules of stopped) {
264
- if (this.clearingJobs.has(syncRules.id)) {
299
+ const stopped = await this.storage.getStoppedReplicationStreams();
300
+ for (let replicationStream of stopped) {
301
+ if (this.clearingJobs.has(replicationStream.replicationStreamId)) {
265
302
  // Already in progress
266
303
  continue;
267
304
  }
@@ -269,16 +306,59 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
269
306
  // We clear storage asynchronously.
270
307
  // It is important to be able to continue running the refresh loop, otherwise we cannot
271
308
  // retry locked replication stream, for example.
272
- const syncRuleStorage = this.storage.getInstance(syncRules, { skipLifecycleHooks: true });
309
+ const syncRuleStorage = this.storage.getInstance(replicationStream, { skipLifecycleHooks: true });
273
310
  const promise = this.terminateSyncRules(syncRuleStorage)
274
311
  .catch((e) => {
275
312
  syncRuleStorage.logger.warn(`Failed clean up replication config`, e);
276
313
  })
277
314
  .finally(() => {
278
- this.clearingJobs.delete(syncRules.id);
315
+ this.clearingJobs.delete(replicationStream.replicationStreamId);
279
316
  });
280
- this.clearingJobs.set(syncRules.id, promise);
317
+ this.clearingJobs.set(replicationStream.replicationStreamId, promise);
318
+ }
319
+
320
+ return { replicationJobStarted };
321
+ }
322
+
323
+ private logReplicationStreamInfoOnce(
324
+ replicationStream: storage.PersistedReplicationStream,
325
+ category: string,
326
+ log: () => void
327
+ ) {
328
+ if (this.lastReplicationStreamInfoLogs.get(replicationStream.replicationStreamName) == category) {
329
+ return;
281
330
  }
331
+
332
+ log();
333
+ this.lastReplicationStreamInfoLogs.set(replicationStream.replicationStreamName, category);
334
+ }
335
+
336
+ private clearReplicationStreamInfoLog(replicationStream: storage.PersistedReplicationStream, category: string) {
337
+ if (this.lastReplicationStreamInfoLogs.get(replicationStream.replicationStreamName) == category) {
338
+ this.lastReplicationStreamInfoLogs.delete(replicationStream.replicationStreamName);
339
+ }
340
+ }
341
+
342
+ /**
343
+ * When we load sync config from the filesystem/config source, we ignore any config loaded by other processes.
344
+ * The idea is that if a different process loads updated config, that process should process it, not this one.
345
+ *
346
+ * This specifically helps for cases with rolling deploys.
347
+ *
348
+ * This does not apply when sync config is loaded from the database/API, instead of in the config.
349
+ */
350
+ private shouldHandleReplicationStream(
351
+ replicationStream: storage.PersistedReplicationStream,
352
+ loadedSyncRules: string | undefined
353
+ ) {
354
+ if (loadedSyncRules == null) {
355
+ return true;
356
+ }
357
+
358
+ const processingConfig = replicationStream.syncConfigContent.find(
359
+ (syncConfig) => syncConfig.syncConfigState == storage.SyncRuleState.PROCESSING
360
+ );
361
+ return processingConfig == null || processingConfig.sync_rules_content == loadedSyncRules;
282
362
  }
283
363
 
284
364
  protected createJobId(syncRuleId: number) {
@@ -1,4 +1,3 @@
1
- import { DataSourceConfig } from '@powersync/service-types/dist/config/PowerSyncConfig.js';
2
1
  import * as t from 'ts-codec';
3
2
 
4
3
  import { schema } from '@powersync/lib-services-framework';
@@ -18,7 +17,7 @@ export interface ConnectionTestResult {
18
17
  /**
19
18
  * Provides a common interface for testing the connection to a DataSource.
20
19
  */
21
- export interface ConnectionTester<TConfig extends DataSourceConfig> {
20
+ export interface ConnectionTester<TConfig extends types.configFile.DataSourceConfig> {
22
21
  /**
23
22
  * Confirm if a connection can be established to the datasource for the provided datasource configuration
24
23
  * @param config
@@ -35,7 +34,7 @@ export interface ReplicationModuleOptions extends modules.AbstractModuleOptions
35
34
  * A replication module describes all the functionality that PowerSync requires to
36
35
  * replicate data from a DataSource. Whenever a new data source is added to powersync this class should be extended.
37
36
  */
38
- export abstract class ReplicationModule<TConfig extends DataSourceConfig>
37
+ export abstract class ReplicationModule<TConfig extends types.configFile.DataSourceConfig>
39
38
  extends modules.AbstractModule
40
39
  implements ConnectionTester<TConfig>
41
40
  {