@powersync/service-core 0.0.0-dev-20260313100403 → 0.0.0-dev-20260515144844

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 (235) hide show
  1. package/CHANGELOG.md +86 -7
  2. package/dist/api/RouteAPI.d.ts +17 -3
  3. package/dist/api/api-index.d.ts +1 -1
  4. package/dist/api/api-index.js +1 -1
  5. package/dist/api/api-index.js.map +1 -1
  6. package/dist/api/api-metrics.js.map +1 -1
  7. package/dist/api/diagnostics.d.ts +1 -1
  8. package/dist/api/diagnostics.js +32 -14
  9. package/dist/api/diagnostics.js.map +1 -1
  10. package/dist/auth/CachedKeyCollector.js +1 -1
  11. package/dist/auth/CachedKeyCollector.js.map +1 -1
  12. package/dist/auth/CompoundKeyCollector.js.map +1 -1
  13. package/dist/auth/KeyStore.js.map +1 -1
  14. package/dist/auth/RemoteJWKSCollector.js.map +1 -1
  15. package/dist/auth/StaticKeyCollector.d.ts +1 -1
  16. package/dist/auth/StaticKeyCollector.js.map +1 -1
  17. package/dist/auth/StaticSupabaseKeyCollector.d.ts +1 -1
  18. package/dist/auth/StaticSupabaseKeyCollector.js.map +1 -1
  19. package/dist/entry/commands/compact-action.js +26 -5
  20. package/dist/entry/commands/compact-action.js.map +1 -1
  21. package/dist/entry/commands/teardown-action.js +2 -2
  22. package/dist/entry/commands/teardown-action.js.map +1 -1
  23. package/dist/entry/entry-index.d.ts +1 -1
  24. package/dist/entry/entry-index.js +1 -1
  25. package/dist/entry/entry-index.js.map +1 -1
  26. package/dist/events/EventsEngine.js +1 -1
  27. package/dist/events/EventsEngine.js.map +1 -1
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +1 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/metrics/MetricsEngine.d.ts +1 -1
  32. package/dist/metrics/RollingBucketMax.d.ts +28 -0
  33. package/dist/metrics/RollingBucketMax.js +80 -0
  34. package/dist/metrics/RollingBucketMax.js.map +1 -0
  35. package/dist/metrics/metrics-index.d.ts +3 -2
  36. package/dist/metrics/metrics-index.js +3 -2
  37. package/dist/metrics/metrics-index.js.map +1 -1
  38. package/dist/metrics/open-telemetry/util.js +1 -1
  39. package/dist/metrics/open-telemetry/util.js.map +1 -1
  40. package/dist/metrics/register-metrics.js +2 -2
  41. package/dist/metrics/register-metrics.js.map +1 -1
  42. package/dist/modules/AbstractModule.d.ts +2 -2
  43. package/dist/modules/AbstractModule.js.map +1 -1
  44. package/dist/modules/modules-index.d.ts +1 -1
  45. package/dist/modules/modules-index.js +1 -1
  46. package/dist/modules/modules-index.js.map +1 -1
  47. package/dist/replication/AbstractReplicationJob.d.ts +2 -2
  48. package/dist/replication/AbstractReplicationJob.js +1 -1
  49. package/dist/replication/AbstractReplicationJob.js.map +1 -1
  50. package/dist/replication/AbstractReplicator.d.ts +7 -7
  51. package/dist/replication/AbstractReplicator.js +31 -28
  52. package/dist/replication/AbstractReplicator.js.map +1 -1
  53. package/dist/replication/ReplicationLagTracker.d.ts +50 -0
  54. package/dist/replication/ReplicationLagTracker.js +78 -0
  55. package/dist/replication/ReplicationLagTracker.js.map +1 -0
  56. package/dist/replication/replication-index.d.ts +3 -2
  57. package/dist/replication/replication-index.js +3 -2
  58. package/dist/replication/replication-index.js.map +1 -1
  59. package/dist/replication/replication-metrics.js.map +1 -1
  60. package/dist/routes/configure-fastify.d.ts +59 -32
  61. package/dist/routes/endpoints/admin.d.ts +108 -54
  62. package/dist/routes/endpoints/admin.js +7 -3
  63. package/dist/routes/endpoints/admin.js.map +1 -1
  64. package/dist/routes/endpoints/checkpointing.js +1 -1
  65. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  66. package/dist/routes/endpoints/socket-route.js +1 -1
  67. package/dist/routes/endpoints/socket-route.js.map +1 -1
  68. package/dist/routes/endpoints/sync-rules.js +10 -10
  69. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  70. package/dist/routes/endpoints/sync-stream.d.ts +10 -10
  71. package/dist/routes/endpoints/sync-stream.js +2 -2
  72. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  73. package/dist/routes/hooks.js +1 -1
  74. package/dist/routes/hooks.js.map +1 -1
  75. package/dist/routes/route-register.js.map +1 -1
  76. package/dist/runner/teardown.js +4 -4
  77. package/dist/runner/teardown.js.map +1 -1
  78. package/dist/storage/BucketStorage.d.ts +9 -9
  79. package/dist/storage/BucketStorage.js +9 -9
  80. package/dist/storage/BucketStorageBatch.d.ts +1 -1
  81. package/dist/storage/BucketStorageFactory.d.ts +27 -20
  82. package/dist/storage/BucketStorageFactory.js +19 -16
  83. package/dist/storage/BucketStorageFactory.js.map +1 -1
  84. package/dist/storage/ChecksumCache.js.map +1 -1
  85. package/dist/storage/PersistedSyncRulesContent.d.ts +3 -1
  86. package/dist/storage/PersistedSyncRulesContent.js +24 -5
  87. package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
  88. package/dist/storage/ReplicationEventPayload.d.ts +1 -1
  89. package/dist/storage/ReportStorage.d.ts +3 -3
  90. package/dist/storage/SourceTable.d.ts +4 -4
  91. package/dist/storage/SourceTable.js +3 -3
  92. package/dist/storage/SourceTable.js.map +1 -1
  93. package/dist/storage/StorageVersionConfig.d.ts +1 -1
  94. package/dist/storage/StorageVersionConfig.js +1 -1
  95. package/dist/storage/SyncRulesBucketStorage.d.ts +38 -6
  96. package/dist/storage/SyncRulesBucketStorage.js +14 -0
  97. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  98. package/dist/storage/WriteCheckpointAPI.d.ts +6 -6
  99. package/dist/storage/WriteCheckpointAPI.js +1 -1
  100. package/dist/storage/bson.d.ts +0 -1
  101. package/dist/storage/bson.js +0 -4
  102. package/dist/storage/bson.js.map +1 -1
  103. package/dist/storage/storage-index.d.ts +8 -8
  104. package/dist/storage/storage-index.js +8 -8
  105. package/dist/storage/storage-index.js.map +1 -1
  106. package/dist/storage/storage-metrics.js.map +1 -1
  107. package/dist/streams/streams-index.d.ts +2 -2
  108. package/dist/streams/streams-index.js +2 -2
  109. package/dist/streams/streams-index.js.map +1 -1
  110. package/dist/sync/BucketChecksumState.d.ts +2 -5
  111. package/dist/sync/BucketChecksumState.js +119 -75
  112. package/dist/sync/BucketChecksumState.js.map +1 -1
  113. package/dist/sync/RequestTracker.js +1 -1
  114. package/dist/sync/RequestTracker.js.map +1 -1
  115. package/dist/sync/sync-index.d.ts +2 -2
  116. package/dist/sync/sync-index.js +2 -2
  117. package/dist/sync/sync-index.js.map +1 -1
  118. package/dist/sync/sync.js.map +1 -1
  119. package/dist/sync/util.js.map +1 -1
  120. package/dist/system/ServiceContext.d.ts +1 -1
  121. package/dist/system/ServiceContext.js +1 -1
  122. package/dist/system/ServiceContext.js.map +1 -1
  123. package/dist/tracing/PerformanceTracer.d.ts +44 -0
  124. package/dist/tracing/PerformanceTracer.js +102 -0
  125. package/dist/tracing/PerformanceTracer.js.map +1 -0
  126. package/dist/tracing/TraceWriter.d.ts +22 -0
  127. package/dist/tracing/TraceWriter.js +63 -0
  128. package/dist/tracing/TraceWriter.js.map +1 -0
  129. package/dist/util/checkpointing.js +1 -1
  130. package/dist/util/config/collectors/impl/base64-config-collector.d.ts +1 -1
  131. package/dist/util/config/collectors/impl/base64-config-collector.js.map +1 -1
  132. package/dist/util/config/collectors/impl/filesystem-config-collector.d.ts +1 -1
  133. package/dist/util/config/collectors/impl/filesystem-config-collector.js +1 -1
  134. package/dist/util/config/collectors/impl/filesystem-config-collector.js.map +1 -1
  135. package/dist/util/config/compound-config-collector.d.ts +1 -1
  136. package/dist/util/config/compound-config-collector.js +2 -2
  137. package/dist/util/config/compound-config-collector.js.map +1 -1
  138. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -1
  139. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
  140. package/dist/util/config/sync-rules/sync-rules-provider.js.map +1 -1
  141. package/dist/util/config.js +1 -1
  142. package/dist/util/config.js.map +1 -1
  143. package/dist/util/env.js +1 -1
  144. package/dist/util/errors.d.ts +3 -0
  145. package/dist/util/errors.js +15 -0
  146. package/dist/util/errors.js.map +1 -0
  147. package/dist/util/protocol-types.d.ts +3 -3
  148. package/dist/util/protocol-types.js +1 -1
  149. package/dist/util/util-index.d.ts +1 -1
  150. package/dist/util/util-index.js +1 -1
  151. package/dist/util/util-index.js.map +1 -1
  152. package/dist/util/utils.d.ts +1 -1
  153. package/package.json +11 -11
  154. package/src/api/RouteAPI.ts +20 -3
  155. package/src/api/api-index.ts +1 -1
  156. package/src/api/api-metrics.ts +1 -1
  157. package/src/api/diagnostics.ts +42 -20
  158. package/src/auth/CachedKeyCollector.ts +2 -3
  159. package/src/auth/CompoundKeyCollector.ts +2 -3
  160. package/src/auth/KeyStore.ts +1 -1
  161. package/src/auth/RemoteJWKSCollector.ts +0 -1
  162. package/src/auth/StaticKeyCollector.ts +1 -1
  163. package/src/auth/StaticSupabaseKeyCollector.ts +1 -1
  164. package/src/entry/commands/compact-action.ts +29 -5
  165. package/src/entry/commands/teardown-action.ts +2 -2
  166. package/src/entry/entry-index.ts +1 -1
  167. package/src/events/EventsEngine.ts +1 -1
  168. package/src/index.ts +2 -0
  169. package/src/metrics/MetricsEngine.ts +1 -1
  170. package/src/metrics/RollingBucketMax.ts +109 -0
  171. package/src/metrics/metrics-index.ts +3 -2
  172. package/src/metrics/open-telemetry/util.ts +1 -1
  173. package/src/metrics/register-metrics.ts +3 -3
  174. package/src/modules/AbstractModule.ts +2 -2
  175. package/src/modules/modules-index.ts +1 -1
  176. package/src/replication/AbstractReplicationJob.ts +3 -3
  177. package/src/replication/AbstractReplicator.ts +32 -30
  178. package/src/replication/ReplicationLagTracker.ts +86 -0
  179. package/src/replication/replication-index.ts +3 -2
  180. package/src/replication/replication-metrics.ts +1 -1
  181. package/src/routes/endpoints/admin.ts +7 -3
  182. package/src/routes/endpoints/checkpointing.ts +1 -1
  183. package/src/routes/endpoints/socket-route.ts +1 -1
  184. package/src/routes/endpoints/sync-rules.ts +10 -12
  185. package/src/routes/endpoints/sync-stream.ts +2 -2
  186. package/src/routes/hooks.ts +2 -2
  187. package/src/routes/route-register.ts +2 -10
  188. package/src/runner/teardown.ts +4 -4
  189. package/src/storage/BucketStorage.ts +9 -9
  190. package/src/storage/BucketStorageBatch.ts +1 -1
  191. package/src/storage/BucketStorageFactory.ts +45 -34
  192. package/src/storage/ChecksumCache.ts +1 -1
  193. package/src/storage/PersistedSyncRulesContent.ts +30 -6
  194. package/src/storage/ReplicationEventPayload.ts +1 -1
  195. package/src/storage/ReportStorage.ts +3 -3
  196. package/src/storage/SourceTable.ts +4 -4
  197. package/src/storage/StorageVersionConfig.ts +1 -1
  198. package/src/storage/SyncRulesBucketStorage.ts +46 -7
  199. package/src/storage/WriteCheckpointAPI.ts +6 -6
  200. package/src/storage/bson.ts +0 -5
  201. package/src/storage/storage-index.ts +8 -8
  202. package/src/storage/storage-metrics.ts +2 -2
  203. package/src/streams/streams-index.ts +2 -2
  204. package/src/sync/BucketChecksumState.ts +141 -93
  205. package/src/sync/RequestTracker.ts +1 -1
  206. package/src/sync/sync-index.ts +2 -2
  207. package/src/sync/sync.ts +2 -8
  208. package/src/sync/util.ts +1 -1
  209. package/src/system/ServiceContext.ts +1 -1
  210. package/src/tracing/PerformanceTracer.ts +126 -0
  211. package/src/tracing/TraceWriter.ts +67 -0
  212. package/src/util/checkpointing.ts +1 -1
  213. package/src/util/config/collectors/impl/base64-config-collector.ts +1 -1
  214. package/src/util/config/collectors/impl/filesystem-config-collector.ts +2 -2
  215. package/src/util/config/compound-config-collector.ts +3 -3
  216. package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -1
  217. package/src/util/config/sync-rules/sync-rules-provider.ts +1 -1
  218. package/src/util/config.ts +1 -1
  219. package/src/util/env.ts +1 -1
  220. package/src/util/errors.ts +21 -0
  221. package/src/util/protocol-types.ts +1 -1
  222. package/src/util/util-index.ts +1 -1
  223. package/src/util/utils.ts +1 -1
  224. package/test/src/ReplicationLagTracker.test.ts +53 -0
  225. package/test/src/RollingBucketMax.test.ts +106 -0
  226. package/test/src/auth.test.ts +115 -7
  227. package/test/src/diagnostics.test.ts +151 -0
  228. package/test/src/module-loader.test.ts +1 -1
  229. package/test/src/routes/mocks.ts +1 -1
  230. package/test/src/routes/stream.test.ts +1 -2
  231. package/test/src/sync/BucketChecksumState.test.ts +223 -67
  232. package/test/src/util/protocol_types.test.ts +1 -1
  233. package/test/tsconfig.json +0 -1
  234. package/tsconfig.tsbuildinfo +1 -1
  235. package/vitest.config.ts +1 -1
@@ -32,19 +32,19 @@ export interface AbstractReplicatorOptions {
32
32
  /**
33
33
  * A replicator manages the mechanics for replicating data from a data source to a storage bucket.
34
34
  * This includes copying across the original data set and then keeping it in sync with the data source using Replication Jobs.
35
- * It also handles any changes to the sync rules.
35
+ * It also handles any changes to the sync config.
36
36
  */
37
37
  export abstract class AbstractReplicator<T extends AbstractReplicationJob = AbstractReplicationJob> {
38
38
  protected logger: winston.Logger;
39
39
  private lockAlerted: boolean = false;
40
40
  /**
41
- * Map of replication jobs by sync rule id. Usually there is only one running job, but there could be two when
42
- * transitioning to a new set of sync rules.
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.
43
43
  */
44
44
  private replicationJobs = new Map<number, T>();
45
45
 
46
46
  /**
47
- * Map of sync rule ids to promises that are clearing the sync rule configuration.
47
+ * Map of replciation stream ids to promises that are clearing the replication stream.
48
48
  *
49
49
  * We primarily do this to keep track of what we're currently clearing, but don't currently
50
50
  * use the Promise value.
@@ -68,8 +68,8 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
68
68
  abstract createJob(options: CreateJobOptions): T;
69
69
 
70
70
  /**
71
- * Clean up any configuration or state for the specified sync rule on the datasource.
72
- * Should be a no-op if the configuration has already been cleared
71
+ * Clean up any configuration or state for the specified replication stream on the datasource.
72
+ * Should be a no-op if the replication stream has already been cleared
73
73
  */
74
74
  abstract cleanUp(syncRuleStorage: storage.SyncRulesBucketStorage): Promise<void>;
75
75
 
@@ -100,22 +100,24 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
100
100
  public async start(): Promise<void> {
101
101
  this.abortController = new AbortController();
102
102
  this.runLoop().catch((e) => {
103
- this.logger.error('Data source fatal replication error', e);
103
+ this.logger.error('Fatal replication error', e);
104
104
  container.reporter.captureException(e);
105
105
  setTimeout(() => {
106
106
  process.exit(1);
107
107
  }, 1000);
108
108
  });
109
109
  this.metrics.getObservableGauge(ReplicationMetric.REPLICATION_LAG_SECONDS).setValueProvider(async () => {
110
- const lag = await this.getReplicationLagMillis().catch((e) => {
110
+ try {
111
+ const lag = this.getReplicationLagMillis();
112
+ if (lag == null) {
113
+ return undefined;
114
+ }
115
+ // ms to seconds
116
+ return Math.round(lag / 1000);
117
+ } catch (e) {
111
118
  this.logger.error('Failed to get replication lag', e);
112
119
  return undefined;
113
- });
114
- if (lag == null) {
115
- return undefined;
116
120
  }
117
- // ms to seconds
118
- return Math.round(lag / 1000);
119
121
  });
120
122
  }
121
123
 
@@ -133,9 +135,9 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
133
135
 
134
136
  let configuredLock: storage.ReplicationLock | undefined = undefined;
135
137
  if (syncRules != null) {
136
- this.logger.info('Loaded sync rules');
138
+ this.logger.info('Loaded sync config');
137
139
  try {
138
- // Configure new sync rules, if they have changed.
140
+ // Configure new sync config, if they have changed.
139
141
  // In that case, also immediately take out a lock, so that another process doesn't start replication on it.
140
142
 
141
143
  const { lock } = await this.storage.configureSyncRules(
@@ -147,11 +149,11 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
147
149
  } catch (e) {
148
150
  // Log and re-raise to exit.
149
151
  // Should only reach this due to validation errors if exit_on_error is true.
150
- this.logger.error(`Failed to update sync rules from configuration`, e);
152
+ this.logger.error(`Failed to update sync config`, e);
151
153
  throw e;
152
154
  }
153
155
  } else {
154
- this.logger.info('No sync rules configured - configure via API');
156
+ this.logger.info('No sync streams or rules configured - configure via API');
155
157
  }
156
158
  while (!this.stopped) {
157
159
  await container.probes.touch();
@@ -204,7 +206,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
204
206
  // Remove from the list. Next refresh call will restart the job.
205
207
  existingJobs.delete(syncRules.id);
206
208
  } else {
207
- // New sync rules were found (or resume after restart)
209
+ // New sync config was found (or resume after restart)
208
210
  try {
209
211
  let lock: storage.ReplicationLock;
210
212
  if (configuredLock?.sync_rules_id == syncRules.id) {
@@ -227,15 +229,15 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
227
229
  } catch (e) {
228
230
  if (e?.errorData?.code === ErrorCode.PSYNC_S1003) {
229
231
  if (!this.lockAlerted) {
230
- this.logger.info(`[${e.errorData.code}] ${e.errorData.description}`);
232
+ syncRules.logger.info(`[${e.errorData.code}] ${e.errorData.description}`);
231
233
  this.lockAlerted = true;
232
234
  }
233
235
  } else {
234
- // Could be a sync rules parse error,
236
+ // Could be a sync config parse error,
235
237
  // for example from stricter validation that was added.
236
238
  // This will be retried every couple of seconds.
237
- // When new (valid) sync rules are deployed and processed, this one be disabled.
238
- this.logger.error('Failed to start replication for new sync rules', e);
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);
239
241
  }
240
242
  }
241
243
  }
@@ -244,7 +246,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
244
246
  this.replicationJobs = newJobs;
245
247
  this.activeReplicationJob = activeJob;
246
248
 
247
- // Stop any orphaned jobs that no longer have sync rules.
249
+ // Stop any orphaned jobs that no longer have a replication stream.
248
250
  // Termination happens below
249
251
  for (let job of existingJobs.values()) {
250
252
  // Old - stop and clean up
@@ -252,11 +254,11 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
252
254
  await job.stop();
253
255
  } catch (e) {
254
256
  // This will be retried
255
- this.logger.warn('Failed to stop old replication job}', e);
257
+ job.storage.logger.warn('Failed to stop old replication job', e);
256
258
  }
257
259
  }
258
260
 
259
- // Sync rules stopped previously, including by a different process.
261
+ // Replication stream stopped previously, including by a different process.
260
262
  const stopped = await this.storage.getStoppedSyncRules();
261
263
  for (let syncRules of stopped) {
262
264
  if (this.clearingJobs.has(syncRules.id)) {
@@ -266,11 +268,11 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
266
268
 
267
269
  // We clear storage asynchronously.
268
270
  // It is important to be able to continue running the refresh loop, otherwise we cannot
269
- // retry locked sync rules, for example.
271
+ // retry locked replication stream, for example.
270
272
  const syncRuleStorage = this.storage.getInstance(syncRules, { skipLifecycleHooks: true });
271
273
  const promise = this.terminateSyncRules(syncRuleStorage)
272
274
  .catch((e) => {
273
- this.logger.warn(`Failed clean up replication config for sync rule: ${syncRules.id}`, e);
275
+ syncRuleStorage.logger.warn(`Failed clean up replication config`, e);
274
276
  })
275
277
  .finally(() => {
276
278
  this.clearingJobs.delete(syncRules.id);
@@ -284,12 +286,12 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
284
286
  }
285
287
 
286
288
  protected async terminateSyncRules(syncRuleStorage: storage.SyncRulesBucketStorage) {
287
- this.logger.info(`Terminating sync rules: ${syncRuleStorage.group_id}...`);
289
+ syncRuleStorage.logger.info(`Terminating replication stream...`);
288
290
  // This deletes postgres replication slots - should complete quickly.
289
291
  // It is safe to do before or after clearing the data in the storage.
290
292
  await this.cleanUp(syncRuleStorage);
291
293
  await syncRuleStorage.terminate({ signal: this.abortController?.signal, clearStorage: true });
292
- this.logger.info(`Successfully terminated sync rules: ${syncRuleStorage.group_id}`);
294
+ syncRuleStorage.logger.info(`Successfully terminated replication stream`);
293
295
  }
294
296
 
295
297
  abstract testConnection(): Promise<ConnectionTestResult>;
@@ -312,7 +314,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
312
314
  *
313
315
  * "processing" replication streams are not taken into account for this metric.
314
316
  */
315
- async getReplicationLagMillis(): Promise<number | undefined> {
317
+ getReplicationLagMillis(): number | undefined {
316
318
  return this.activeReplicationJob?.getReplicationLagMillis();
317
319
  }
318
320
  }
@@ -0,0 +1,86 @@
1
+ import { RollingBucketMax } from '../metrics/RollingBucketMax.js';
2
+
3
+ /**
4
+ * Tracks replication lag across the current in-flight transaction and a rolling
5
+ * max of recently observed lag values.
6
+ */
7
+ export class ReplicationLagTracker {
8
+ private readonly rollingReplicationLag = new RollingBucketMax();
9
+ private _oldestUncommittedChange: Date | null = null;
10
+ private _isStartingReplication = true;
11
+
12
+ /**
13
+ * The oldest source timestamp still part of the current in-flight work.
14
+ */
15
+ get oldestUncommittedChange(): Date | null {
16
+ return this._oldestUncommittedChange;
17
+ }
18
+
19
+ /**
20
+ * True until replication has seen its first completed commit or equivalent keepalive.
21
+ */
22
+ get isStartingReplication(): boolean {
23
+ return this._isStartingReplication;
24
+ }
25
+
26
+ /**
27
+ * Registers the first source timestamp for the current in-flight work,
28
+ * for example the start of a transaction
29
+ */
30
+ trackUncommittedChange(timestamp: Date | null | undefined): void {
31
+ if (this._oldestUncommittedChange == null && timestamp != null) {
32
+ this._oldestUncommittedChange = timestamp;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Clears the current in-flight timestamp without changing startup state.
38
+ */
39
+ clearUncommittedChange(): void {
40
+ this._oldestUncommittedChange = null;
41
+ }
42
+
43
+ /**
44
+ * Marks replication as started even if no committed transaction lag was recorded.
45
+ */
46
+ markStarted(): void {
47
+ this._isStartingReplication = false;
48
+ }
49
+
50
+ /**
51
+ * Mark the current pending changes as "committed".
52
+ *
53
+ * Records the current in-flight lag into the rolling window and clears it.
54
+ * The current lag is calculated as the differnence between current time and the oldest change,
55
+ * as marked by trackUncommittedChange.
56
+ */
57
+ markCommitted(timestampMs = Date.now()): void {
58
+ if (this._oldestUncommittedChange != null) {
59
+ this.rollingReplicationLag.report(timestampMs - this._oldestUncommittedChange.getTime(), timestampMs);
60
+ }
61
+ this.clearUncommittedChange();
62
+ this.markStarted();
63
+ }
64
+
65
+ /**
66
+ * Returns the lag for the current in-flight work.
67
+ *
68
+ * 0 if idle (no pending changes to replicate).
69
+ *
70
+ * undefined when replication is still starting up.
71
+ */
72
+ getCurrentLagMillis(timestampMs = Date.now()): number | undefined {
73
+ if (this._oldestUncommittedChange == null) {
74
+ return this._isStartingReplication ? undefined : 0;
75
+ }
76
+ return timestampMs - this._oldestUncommittedChange.getTime();
77
+ }
78
+
79
+ /**
80
+ * Returns the rolling lag metric value, including the current in-flight lag when present.
81
+ */
82
+ getLagMillis(timestampMs = Date.now()): number | undefined {
83
+ this.rollingReplicationLag.report(this.getCurrentLagMillis(timestampMs), timestampMs);
84
+ return this.rollingReplicationLag.getRollingMax(timestampMs);
85
+ }
86
+ }
@@ -1,7 +1,8 @@
1
1
  export * from './AbstractReplicationJob.js';
2
2
  export * from './AbstractReplicator.js';
3
3
  export * from './ErrorRateLimiter.js';
4
+ export * from './RelationCache.js';
5
+ export * from './replication-metrics.js';
4
6
  export * from './ReplicationEngine.js';
7
+ export * from './ReplicationLagTracker.js';
5
8
  export * from './ReplicationModule.js';
6
- export * from './replication-metrics.js';
7
- export * from './RelationCache.js';
@@ -1,5 +1,5 @@
1
- import { MetricsEngine } from '../metrics/metrics-index.js';
2
1
  import { ReplicationMetric } from '@powersync/service-types';
2
+ import { MetricsEngine } from '../metrics/metrics-index.js';
3
3
 
4
4
  /**
5
5
  * Create and register the core replication metrics.
@@ -119,7 +119,7 @@ export const reprocess = routeDefinition({
119
119
  const apiHandler = service_context.routerEngine.getAPI();
120
120
  const next = await activeBucketStorage.getNextSyncRules(apiHandler.getParseSyncRulesOptions());
121
121
  if (next != null) {
122
- throw new Error(`Busy processing sync rules - cannot reprocess`);
122
+ throw new Error(`Busy processing sync config - cannot reprocess`);
123
123
  }
124
124
 
125
125
  const active = await activeBucketStorage.getActiveSyncRules(apiHandler.getParseSyncRulesOptions());
@@ -127,13 +127,17 @@ export const reprocess = routeDefinition({
127
127
  throw new errors.ServiceError({
128
128
  status: 422,
129
129
  code: ErrorCode.PSYNC_S4104,
130
- description: 'No active sync rules'
130
+ description: 'No active sync config'
131
131
  });
132
132
  }
133
133
 
134
+ // There are some differences between this and using asUpdateOptions():
135
+ // 1. This always re-parses the source YAML. If there are changes to the sync stream compiler, that can affect the sync plan.
136
+ // 2. If the source does not set the storage version, this will update it do the current version.
137
+ // We can consider tweaking this behavior in the future.
134
138
  const new_rules = await activeBucketStorage.updateSyncRules(
135
139
  storage.updateSyncRulesFromYaml(active.sync_rules.config.content, {
136
- // These sync rules already passed validation. But if the rules are not valid anymore due
140
+ // This sync config already passed validation. But if the config is not valid anymore due
137
141
  // to a service change, we do want to report the error here.
138
142
  validate: true
139
143
  })
@@ -33,7 +33,7 @@ export const writeCheckpoint = routeDefinition({
33
33
  const bucketStorage = await service_context.storageEngine.activeBucketStorage.getActiveStorage();
34
34
  const cp = await bucketStorage?.getCheckpoint();
35
35
  if (cp == null) {
36
- throw new Error('No sync rules available');
36
+ throw new Error('No sync config available');
37
37
  }
38
38
  if (cp.lsn && cp.lsn >= head) {
39
39
  logger.info(`Got write checkpoint: ${head} : ${cp.checkpoint}`);
@@ -77,7 +77,7 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
77
77
  new errors.ServiceError({
78
78
  status: 500,
79
79
  code: ErrorCode.PSYNC_S2302,
80
- description: 'No sync rules available'
80
+ description: 'No sync config available'
81
81
  })
82
82
  );
83
83
  responder.onComplete();
@@ -4,9 +4,9 @@ import type { FastifyPluginAsync } from 'fastify';
4
4
  import * as t from 'ts-codec';
5
5
 
6
6
  import { RouteAPI } from '../../api/RouteAPI.js';
7
+ import { updateSyncRulesFromConfig, updateSyncRulesFromYaml } from '../../storage/BucketStorageFactory.js';
7
8
  import { authApi } from '../auth.js';
8
9
  import { routeDefinition } from '../router.js';
9
- import { updateSyncRulesFromConfig, updateSyncRulesFromYaml } from '../../storage/BucketStorageFactory.js';
10
10
 
11
11
  const DeploySyncRulesRequest = t.object({
12
12
  content: t.string
@@ -43,12 +43,12 @@ export const deploySyncRules = routeDefinition({
43
43
  const { storageEngine } = service_context;
44
44
 
45
45
  if (service_context.configuration.sync_rules.present) {
46
- // If sync rules are configured via the config, disable deploy via the API.
46
+ // If sync config is configured via the service config, disable deploy via the API.
47
47
  throw new errors.ServiceError({
48
48
  status: 422,
49
49
  code: ErrorCode.PSYNC_S4105,
50
- description: 'Sync rules API disabled',
51
- details: 'Use the management API to deploy sync rules'
50
+ description: 'Sync config API disabled',
51
+ details: 'Update sync config in the service configuration'
52
52
  });
53
53
  }
54
54
  const content = payload.params.content;
@@ -56,7 +56,7 @@ export const deploySyncRules = routeDefinition({
56
56
 
57
57
  try {
58
58
  const apiHandler = service_context.routerEngine.getAPI();
59
- syncConfig = SqlSyncRules.fromYaml(payload.params.content, {
59
+ syncConfig = SqlSyncRules.fromYaml(content, {
60
60
  ...apiHandler.getParseSyncRulesOptions(),
61
61
  // We don't do any schema-level validation at this point
62
62
  schema: undefined
@@ -65,14 +65,12 @@ export const deploySyncRules = routeDefinition({
65
65
  throw new errors.ServiceError({
66
66
  status: 422,
67
67
  code: ErrorCode.PSYNC_R0001,
68
- description: 'Sync rules parsing failed',
68
+ description: 'Sync config parsing failed',
69
69
  details: e.message
70
70
  });
71
71
  }
72
72
 
73
- const sync_rules = await storageEngine.activeBucketStorage.updateSyncRules(
74
- updateSyncRulesFromConfig(syncConfig.config)
75
- );
73
+ const sync_rules = await storageEngine.activeBucketStorage.updateSyncRules(updateSyncRulesFromConfig(syncConfig));
76
74
 
77
75
  return {
78
76
  slot_name: sync_rules.slot_name
@@ -117,7 +115,7 @@ export const currentSyncRules = routeDefinition({
117
115
  throw new errors.ServiceError({
118
116
  status: 422,
119
117
  code: ErrorCode.PSYNC_S4104,
120
- description: 'No active sync rules'
118
+ description: 'No active sync config'
121
119
  });
122
120
  }
123
121
 
@@ -164,13 +162,13 @@ export const reprocessSyncRules = routeDefinition({
164
162
  throw new errors.ServiceError({
165
163
  status: 422,
166
164
  code: ErrorCode.PSYNC_S4104,
167
- description: 'No active sync rules'
165
+ description: 'No active sync config'
168
166
  });
169
167
  }
170
168
 
171
169
  const new_rules = await activeBucketStorage.updateSyncRules(
172
170
  updateSyncRulesFromYaml(sync_rules.sync_rules.config.content, {
173
- // These sync rules already passed validation. But if the rules are not valid anymore due
171
+ // This sync config already passed validation. But if the rules are not valid anymore due
174
172
  // to a service change, we do want to report the error here.
175
173
  validate: true
176
174
  })
@@ -68,7 +68,7 @@ export const syncStreamed = routeDefinition({
68
68
  throw new errors.ServiceError({
69
69
  status: 500,
70
70
  code: ErrorCode.PSYNC_S2302,
71
- description: 'No sync rules available'
71
+ description: 'No sync config available'
72
72
  });
73
73
  }
74
74
 
@@ -121,7 +121,7 @@ export const syncStreamed = routeDefinition({
121
121
  });
122
122
 
123
123
  stream.on('end', () => {
124
- // Auth failure or switch to new sync rules
124
+ // Auth failure or switch to new sync config
125
125
  closeReason ??= 'service closing stream';
126
126
  });
127
127
 
@@ -1,6 +1,6 @@
1
- import type fastify from 'fastify';
2
- import a from 'async';
3
1
  import { logger } from '@powersync/lib-services-framework';
2
+ import a from 'async';
3
+ import type fastify from 'fastify';
4
4
 
5
5
  export type CreateRequestQueueParams = {
6
6
  max_queue_depth: number;
@@ -1,17 +1,9 @@
1
1
  import type fastify from 'fastify';
2
2
  import * as uuid from 'uuid';
3
3
 
4
- import {
5
- ErrorCode,
6
- errors,
7
- HTTPMethod,
8
- logger,
9
- RouteNotFound,
10
- router,
11
- ServiceError
12
- } from '@powersync/lib-services-framework';
13
- import { Context, ContextProvider, RequestEndpoint, RequestEndpointHandlerPayload } from './router.js';
4
+ import { errors, HTTPMethod, logger, RouteNotFound, router, ServiceError } from '@powersync/lib-services-framework';
14
5
  import { FastifyReply } from 'fastify';
6
+ import { Context, ContextProvider, RequestEndpoint, RequestEndpointHandlerPayload } from './router.js';
15
7
 
16
8
  export type FastifyEndpoint<I, O, C> = RequestEndpoint<I, O, C> & {
17
9
  parse?: boolean;
@@ -35,13 +35,13 @@ export async function teardown(runnerConfig: utils.RunnerConfig) {
35
35
  }
36
36
 
37
37
  async function terminateSyncRules(storageFactory: storage.BucketStorageFactory, moduleManager: modules.ModuleManager) {
38
- logger.info(`Terminating sync rules...`);
38
+ logger.info(`Terminating replication stream...`);
39
39
  const start = Date.now();
40
40
  const locks: storage.ReplicationLock[] = [];
41
41
  while (Date.now() - start < 120_000) {
42
42
  let retry = false;
43
43
  const replicatingSyncRules = await storageFactory.getReplicatingSyncRules();
44
- // Lock all the replicating sync rules
44
+ // Lock all the replicating replication streams
45
45
  for (const replicatingSyncRule of replicatingSyncRules) {
46
46
  const lock = await replicatingSyncRule.lock();
47
47
  locks.push(lock);
@@ -50,10 +50,10 @@ async function terminateSyncRules(storageFactory: storage.BucketStorageFactory,
50
50
  const stoppedSyncRules = await storageFactory.getStoppedSyncRules();
51
51
  const combinedSyncRules = [...replicatingSyncRules, ...stoppedSyncRules];
52
52
  try {
53
- // Clean up any module specific configuration for the sync rules
53
+ // Clean up any module specific configuration for the replication stream
54
54
  await moduleManager.tearDown({ syncRules: combinedSyncRules });
55
55
 
56
- // Mark the sync rules as terminated
56
+ // Mark the replication stream as terminated
57
57
  for (let syncRules of combinedSyncRules) {
58
58
  const syncRulesStorage = storageFactory.getInstance(syncRules);
59
59
  // The storage will be dropped at the end of the teardown, so we don't need to clear it here
@@ -2,36 +2,36 @@ import { ToastableSqliteRow } from '@powersync/service-sync-rules';
2
2
 
3
3
  export enum SyncRuleState {
4
4
  /**
5
- * New sync rules - needs to be processed (initial replication).
5
+ * New replication stream - needs to be processed (initial replication).
6
6
  *
7
- * While multiple sets of sync rules _can_ be in PROCESSING,
7
+ * While multiple replication streams _can_ be in PROCESSING,
8
8
  * it's generally pointless, so we only keep one in that state.
9
9
  */
10
10
  PROCESSING = 'PROCESSING',
11
11
 
12
12
  /**
13
- * Sync rule processing is done, and can be used for sync.
13
+ * Intial processing is done, and can be used for sync.
14
14
  *
15
- * Only one set of sync rules should be in ACTIVE or ERRORED state.
15
+ * Only one replication stream should be in ACTIVE or ERRORED state.
16
16
  */
17
17
  ACTIVE = 'ACTIVE',
18
18
  /**
19
- * This state is used when the sync rules has been replaced,
19
+ * This state is used when the replication stream has been replaced,
20
20
  * and replication is or should be stopped.
21
21
  */
22
22
  STOP = 'STOP',
23
23
  /**
24
- * After sync rules have been stopped, the data needs to be
24
+ * After replication stream has been stopped, the data needs to be
25
25
  * deleted. Once deleted, the state is TERMINATED.
26
26
  */
27
27
  TERMINATED = 'TERMINATED',
28
28
 
29
29
  /**
30
- * Sync rules has run into a permanent replication error. It
31
- * is still the "active" sync rules for syncing to users,
30
+ * Replication stream has run into a permanent replication error. It
31
+ * is still the "active" replication stram for syncing to users,
32
32
  * but should not replicate anymore.
33
33
  *
34
- * It will transition to STOP when a new sync rules is activated.
34
+ * It will transition to STOP when a new replication stream is activated.
35
35
  */
36
36
  ERRORED = 'ERRORED'
37
37
  }
@@ -1,10 +1,10 @@
1
1
  import { ObserverClient } from '@powersync/lib-services-framework';
2
2
  import { EvaluatedParameters, EvaluatedRow, SqliteRow, ToastableSqliteRow } from '@powersync/service-sync-rules';
3
3
  import { BSON } from 'bson';
4
+ import { InternalOpId } from '../util/utils.js';
4
5
  import { ReplicationEventPayload } from './ReplicationEventPayload.js';
5
6
  import { SourceTable, TableSnapshotStatus } from './SourceTable.js';
6
7
  import { BatchedCustomWriteCheckpointOptions } from './storage-index.js';
7
- import { InternalOpId } from '../util/utils.js';
8
8
 
9
9
  export const DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS: ResolvedBucketBatchCommitOptions = {
10
10
  createEmptyCheckpoints: true,