@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,7 +1,13 @@
1
1
  import * as sqlite from 'node:sqlite';
2
2
 
3
3
  import { ErrorCode, errors, router, schema } from '@powersync/lib-services-framework';
4
- import { nodeSqlite, SourceSchema, SqlSyncRules, StaticSchema } from '@powersync/service-sync-rules';
4
+ import {
5
+ HydratedSyncConfig,
6
+ nodeSqlite,
7
+ SourceSchema,
8
+ SqlSyncRules,
9
+ StaticSchema
10
+ } from '@powersync/service-sync-rules';
5
11
  import { internal_routes } from '@powersync/service-types';
6
12
 
7
13
  import { DEFAULT_HYDRATION_STATE } from '@powersync/service-sync-rules';
@@ -65,20 +71,35 @@ export const diagnostics = routeDefinition({
65
71
  const {
66
72
  storageEngine: { activeBucketStorage }
67
73
  } = service_context;
68
- const active = await activeBucketStorage.getActiveSyncRulesContent();
69
- const next = await activeBucketStorage.getNextSyncRulesContent();
74
+ const active = await activeBucketStorage.getActiveSyncConfig();
75
+ const deploying = await activeBucketStorage.getDeployingSyncConfig();
70
76
 
71
- const active_status = await api.getSyncRulesStatus(activeBucketStorage, apiHandler, active, {
72
- include_content,
73
- check_connection: status.connected,
74
- live_status: true
75
- });
77
+ const active_status = await api.getSyncRulesStatus(
78
+ apiHandler,
79
+ active?.content ?? null,
80
+ {
81
+ include_content,
82
+ check_connection: status.connected,
83
+ live_status: true,
84
+ active: true
85
+ },
86
+ active?.storage
87
+ );
76
88
 
77
- const next_status = await api.getSyncRulesStatus(activeBucketStorage, apiHandler, next, {
78
- include_content,
79
- check_connection: status.connected,
80
- live_status: true
81
- });
89
+ const deploying_status =
90
+ deploying == null
91
+ ? undefined
92
+ : await api.getSyncRulesStatus(
93
+ apiHandler,
94
+ deploying.content,
95
+ {
96
+ include_content,
97
+ check_connection: status.connected,
98
+ live_status: true,
99
+ active: false
100
+ },
101
+ deploying.storage
102
+ );
82
103
 
83
104
  return internal_routes.DiagnosticsResponse.encode({
84
105
  connections: [
@@ -89,7 +110,7 @@ export const diagnostics = routeDefinition({
89
110
  }
90
111
  ],
91
112
  active_sync_rules: active_status,
92
- deploying_sync_rules: next_status
113
+ deploying_sync_rules: deploying_status
93
114
  });
94
115
  }
95
116
  });
@@ -119,12 +140,16 @@ export const reprocess = routeDefinition({
119
140
  storageEngine: { activeBucketStorage }
120
141
  } = service_context;
121
142
  const apiHandler = service_context.routerEngine.getAPI();
122
- const next = await activeBucketStorage.getNextSyncRules(apiHandler.getParseSyncRulesOptions());
143
+ const next = await activeBucketStorage.getDeployingSyncConfig();
123
144
  if (next != null) {
124
- throw new Error(`Busy processing sync config - cannot reprocess`);
145
+ throw new errors.ServiceError({
146
+ status: 409,
147
+ code: ErrorCode.PSYNC_S4106,
148
+ description: 'Busy processing sync config - cannot reprocess'
149
+ });
125
150
  }
126
151
 
127
- const active = await activeBucketStorage.getActiveSyncRules(apiHandler.getParseSyncRulesOptions());
152
+ const active = await activeBucketStorage.getActiveSyncConfig();
128
153
  if (active == null) {
129
154
  throw new errors.ServiceError({
130
155
  status: 422,
@@ -132,13 +157,12 @@ export const reprocess = routeDefinition({
132
157
  description: 'No active sync config'
133
158
  });
134
159
  }
135
-
136
160
  // There are some differences between this and using asUpdateOptions():
137
161
  // 1. This always re-parses the source YAML. If there are changes to the sync stream compiler, that can affect the sync plan.
138
162
  // 2. If the source does not set the storage version, this will update it do the current version.
139
163
  // We can consider tweaking this behavior in the future.
140
164
  const new_rules = await activeBucketStorage.updateSyncRules(
141
- storage.updateSyncRulesFromYaml(active.syncConfigWithErrors.config.content, {
165
+ storage.updateSyncRulesFromYaml(active.content.sync_rules_content, {
142
166
  // This sync config already passed validation. But if the config is not valid anymore due
143
167
  // to a service change, we do want to report the error here.
144
168
  validate: true
@@ -153,44 +177,47 @@ export const reprocess = routeDefinition({
153
177
  // Previously the connection was asserted with `!`
154
178
  tag: baseConfig.tag,
155
179
  id: baseConfig.id,
156
- slot_name: new_rules.slot_name
180
+ slot_name: new_rules.replicationStreamName
157
181
  }
158
182
  ]
159
183
  });
160
184
  }
161
185
  });
162
186
 
163
- class FakeSyncRulesContentForValidation extends storage.PersistedSyncRulesContent {
187
+ class FakeSyncRulesContentForValidation extends storage.PersistedSyncConfigContent {
164
188
  constructor(
165
189
  private readonly apiHandler: api.RouteAPI,
166
190
  private readonly schema: SourceSchema,
167
- data: storage.PersistedSyncRulesContentData
191
+ data: storage.PersistedSyncConfigContentData
168
192
  ) {
169
193
  super(data);
170
194
  }
171
195
 
172
- current_lock: storage.ReplicationLock | null = null;
173
-
174
- async lock(): Promise<storage.ReplicationLock> {
175
- throw new Error('Lock not implemented');
176
- }
196
+ parsed(options: storage.ParseSyncConfigOptions): storage.ParsedSyncConfigSet {
197
+ const syncConfig = SqlSyncRules.fromYaml(this.sync_rules_content, {
198
+ ...this.apiHandler.getParseSyncRulesOptions(),
199
+ schema: this.schema
200
+ });
177
201
 
178
- parsed(options: storage.ParseSyncRulesOptions): storage.PersistedSyncRules {
202
+ let hydrated: HydratedSyncConfig | undefined;
179
203
  return {
180
- ...this,
181
- syncConfigWithErrors: SqlSyncRules.fromYaml(this.sync_rules_content, {
182
- ...this.apiHandler.getParseSyncRulesOptions(),
183
- schema: this.schema
184
- }),
204
+ replicationStreamId: this.replicationStreamId,
205
+ replicationStreamName: this.replicationStreamName,
206
+ syncConfigs: [syncConfig],
185
207
  hydrationState: DEFAULT_HYDRATION_STATE,
186
- hydratedSyncConfig() {
187
- return this.syncConfigWithErrors.config.hydrate({
208
+ get hydratedSyncConfig(): HydratedSyncConfig {
209
+ hydrated ??= syncConfig.config.hydrate({
188
210
  hydrationState: DEFAULT_HYDRATION_STATE,
189
211
  sqlite: nodeSqlite(sqlite)
190
212
  });
213
+ return hydrated;
191
214
  }
192
215
  };
193
216
  }
217
+
218
+ async getSyncConfigStatus(): Promise<storage.PersistedSyncConfigStatus | null> {
219
+ return null;
220
+ }
194
221
  }
195
222
 
196
223
  export const validate = routeDefinition({
@@ -210,13 +237,12 @@ export const validate = routeDefinition({
210
237
 
211
238
  const sync_rules = new FakeSyncRulesContentForValidation(apiHandler, schema, {
212
239
  // Dummy values
213
- id: 0,
214
- slot_name: '',
215
- active: false,
216
- last_checkpoint_lsn: '',
240
+ replicationStreamId: 0,
241
+ replicationStreamName: '',
217
242
  storageVersion: storage.LEGACY_STORAGE_VERSION,
218
243
  sync_rules_content: content,
219
- compiled_plan: null
244
+ compiled_plan: null,
245
+ syncConfigState: storage.SyncRuleState.PROCESSING
220
246
  });
221
247
 
222
248
  const connectionStatus = await apiHandler.getConnectionStatus();
@@ -227,16 +253,12 @@ export const validate = routeDefinition({
227
253
  });
228
254
  }
229
255
 
230
- const status = (await api.getSyncRulesStatus(
231
- service_context.storageEngine.activeBucketStorage,
232
- apiHandler,
233
- sync_rules,
234
- {
235
- include_content: false,
236
- check_connection: connectionStatus.connected,
237
- live_status: false
238
- }
239
- ))!;
256
+ const status = (await api.getSyncRulesStatus(apiHandler, sync_rules, {
257
+ include_content: false,
258
+ check_connection: connectionStatus.connected,
259
+ live_status: false,
260
+ active: false
261
+ }))!;
240
262
 
241
263
  if (connectionStatus == null) {
242
264
  status.errors.push({ level: 'fatal', message: 'No connection configured', ts: new Date().toISOString() });
@@ -30,7 +30,7 @@ export const writeCheckpoint = routeDefinition({
30
30
 
31
31
  logger.info(`Waiting for LSN checkpoint: ${head}`);
32
32
  while (Date.now() - start < timeout) {
33
- const bucketStorage = await service_context.storageEngine.activeBucketStorage.getActiveStorage();
33
+ const bucketStorage = (await service_context.storageEngine.activeBucketStorage.getActiveSyncConfig())?.storage;
34
34
  const cp = await bucketStorage?.getCheckpoint();
35
35
  if (cp == null) {
36
36
  throw new Error('No sync config available');
@@ -71,7 +71,7 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
71
71
  storageEngine: { activeBucketStorage }
72
72
  } = service_context;
73
73
 
74
- const bucketStorage = await activeBucketStorage.getActiveStorage();
74
+ const bucketStorage = (await activeBucketStorage.getActiveSyncConfig())?.storage;
75
75
  if (bucketStorage == null) {
76
76
  responder.onError(
77
77
  new errors.ServiceError({
@@ -73,7 +73,7 @@ export const deploySyncRules = routeDefinition({
73
73
  const sync_rules = await storageEngine.activeBucketStorage.updateSyncRules(updateSyncRulesFromConfig(syncConfig));
74
74
 
75
75
  return {
76
- slot_name: sync_rules.slot_name
76
+ slot_name: sync_rules.replicationStreamName
77
77
  };
78
78
  }
79
79
  });
@@ -110,8 +110,8 @@ export const currentSyncRules = routeDefinition({
110
110
  storageEngine: { activeBucketStorage }
111
111
  } = service_context;
112
112
 
113
- const sync_rules = await activeBucketStorage.getActiveSyncRulesContent();
114
- if (!sync_rules) {
113
+ const active = await activeBucketStorage.getActiveSyncConfig();
114
+ if (!active) {
115
115
  throw new errors.ServiceError({
116
116
  status: 422,
117
117
  code: ErrorCode.PSYNC_S4104,
@@ -119,15 +119,16 @@ export const currentSyncRules = routeDefinition({
119
119
  });
120
120
  }
121
121
 
122
+ const sync_rules = active.content;
122
123
  const apiHandler = service_context.routerEngine.getAPI();
123
124
  const info = await debugSyncRules(apiHandler, sync_rules.sync_rules_content);
124
- const next = await activeBucketStorage.getNextSyncRulesContent();
125
+ const next = await activeBucketStorage.getDeployingSyncConfig();
125
126
 
126
- const next_info = next ? await debugSyncRules(apiHandler, next.sync_rules_content) : null;
127
+ const next_info = next ? await debugSyncRules(apiHandler, next.content.sync_rules_content) : null;
127
128
 
128
129
  const response = {
129
130
  current: {
130
- slot_name: sync_rules.slot_name,
131
+ slot_name: sync_rules.replicationStreamName,
131
132
  content: sync_rules.sync_rules_content,
132
133
  ...info
133
134
  },
@@ -135,8 +136,8 @@ export const currentSyncRules = routeDefinition({
135
136
  next == null
136
137
  ? null
137
138
  : {
138
- slot_name: next.slot_name,
139
- content: next.sync_rules_content,
139
+ slot_name: next.content.replicationStreamName,
140
+ content: next.content.sync_rules_content,
140
141
  ...next_info
141
142
  }
142
143
  };
@@ -156,9 +157,17 @@ export const reprocessSyncRules = routeDefinition({
156
157
  const {
157
158
  storageEngine: { activeBucketStorage }
158
159
  } = payload.context.service_context;
159
- const apiHandler = payload.context.service_context.routerEngine.getAPI();
160
- const sync_rules = await activeBucketStorage.getActiveSyncRules(apiHandler.getParseSyncRulesOptions());
161
- if (sync_rules == null) {
160
+ const next = await activeBucketStorage.getDeployingSyncConfig();
161
+ if (next != null) {
162
+ throw new errors.ServiceError({
163
+ status: 409,
164
+ code: ErrorCode.PSYNC_S4106,
165
+ description: 'Busy processing sync config - cannot reprocess'
166
+ });
167
+ }
168
+
169
+ const active = await activeBucketStorage.getActiveSyncConfig();
170
+ if (active == null) {
162
171
  throw new errors.ServiceError({
163
172
  status: 422,
164
173
  code: ErrorCode.PSYNC_S4104,
@@ -166,15 +175,16 @@ export const reprocessSyncRules = routeDefinition({
166
175
  });
167
176
  }
168
177
 
178
+ const sync_rules = active.content;
169
179
  const new_rules = await activeBucketStorage.updateSyncRules(
170
- updateSyncRulesFromYaml(sync_rules.syncConfigWithErrors.config.content, {
180
+ updateSyncRulesFromYaml(sync_rules.sync_rules_content, {
171
181
  // This sync config already passed validation. But if the rules are not valid anymore due
172
182
  // to a service change, we do want to report the error here.
173
183
  validate: true
174
184
  })
175
185
  );
176
186
  return {
177
- slot_name: new_rules.slot_name
187
+ slot_name: new_rules.replicationStreamName
178
188
  };
179
189
  }
180
190
  });
@@ -62,7 +62,7 @@ export const syncStreamed = routeDefinition({
62
62
  });
63
63
  }
64
64
 
65
- const bucketStorage = await storageEngine.activeBucketStorage.getActiveStorage();
65
+ const bucketStorage = (await storageEngine.activeBucketStorage.getActiveSyncConfig())?.storage;
66
66
 
67
67
  if (bucketStorage == null) {
68
68
  throw new errors.ServiceError({
@@ -40,22 +40,22 @@ async function terminateSyncRules(storageFactory: storage.BucketStorageFactory,
40
40
  const locks: storage.ReplicationLock[] = [];
41
41
  while (Date.now() - start < 120_000) {
42
42
  let retry = false;
43
- const replicatingSyncRules = await storageFactory.getReplicatingSyncRules();
43
+ const replicatingStreams = await storageFactory.getReplicatingReplicationStreams();
44
44
  // Lock all the replicating replication streams
45
- for (const replicatingSyncRule of replicatingSyncRules) {
46
- const lock = await replicatingSyncRule.lock();
45
+ for (const replicatingStream of replicatingStreams) {
46
+ const lock = await replicatingStream.lock();
47
47
  locks.push(lock);
48
48
  }
49
49
 
50
- const stoppedSyncRules = await storageFactory.getStoppedSyncRules();
51
- const combinedSyncRules = [...replicatingSyncRules, ...stoppedSyncRules];
50
+ const stoppedStreams = await storageFactory.getStoppedReplicationStreams();
51
+ const combinedStreams = [...replicatingStreams, ...stoppedStreams];
52
52
  try {
53
53
  // Clean up any module specific configuration for the replication stream
54
- await moduleManager.tearDown({ syncRules: combinedSyncRules });
54
+ await moduleManager.tearDown({ replicationStreams: combinedStreams, syncRules: combinedStreams });
55
55
 
56
56
  // Mark the replication stream as terminated
57
- for (let syncRules of combinedSyncRules) {
58
- const syncRulesStorage = storageFactory.getInstance(syncRules);
57
+ for (let replicationStream of combinedStreams) {
58
+ const syncRulesStorage = storageFactory.getInstance(replicationStream);
59
59
  // The storage will be dropped at the end of the teardown, so we don't need to clear it here
60
60
  await syncRulesStorage.terminate({ clearStorage: false });
61
61
  }
@@ -83,11 +83,6 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
83
83
  */
84
84
  setResumeLsn(lsn: string): Promise<void>;
85
85
 
86
- /**
87
- * Get the last checkpoint LSN, from either commit or keepalive.
88
- */
89
- lastCheckpointLsn: string | null;
90
-
91
86
  /**
92
87
  * LSN to resume from.
93
88
  *
@@ -216,8 +211,6 @@ export interface CheckpointResult {
216
211
  /**
217
212
  * True if a checkpoint was actually created by this operation. This can be false even if checkpointBlocked is false,
218
213
  * if the checkpoint was empty.
219
- *
220
- * This is primarily used for testing.
221
214
  */
222
215
  checkpointCreated: boolean;
223
216
  }
@@ -2,13 +2,15 @@ import { BaseObserver, logger } from '@powersync/lib-services-framework';
2
2
  import {
3
3
  PrecompiledSyncConfig,
4
4
  SerializedCompatibilityContext,
5
+ SerializedSyncPlanV1,
5
6
  serializeSyncPlan,
6
7
  SqlSyncRules,
7
8
  SyncConfigWithErrors
8
9
  } from '@powersync/service-sync-rules';
9
10
  import { ReplicationError } from '@powersync/service-types';
10
11
  import { syncConfigYamlErrorToReplicationError } from '../util/errors.js';
11
- import { ParseSyncRulesOptions, PersistedSyncRules, PersistedSyncRulesContent } from './PersistedSyncRulesContent.js';
12
+ import { PersistedReplicationStream } from './PersistedReplicationStream.js';
13
+ import { PersistedSyncConfigContent } from './PersistedSyncConfigContent.js';
12
14
  import { ReplicationEventPayload } from './ReplicationEventPayload.js';
13
15
  import { ReplicationLock } from './ReplicationLock.js';
14
16
  import { ReportStorage } from './ReportStorage.js';
@@ -31,14 +33,14 @@ export abstract class BucketStorageFactory
31
33
  */
32
34
  async configureSyncRules(
33
35
  options: UpdateSyncRulesOptions
34
- ): Promise<{ updated: boolean; persisted_sync_rules?: PersistedSyncRulesContent; lock?: ReplicationLock }> {
35
- const next = await this.getNextSyncRulesContent();
36
- const active = await this.getActiveSyncRulesContent();
36
+ ): Promise<{ updated: boolean; persisted_sync_rules?: PersistedReplicationStream; lock?: ReplicationLock }> {
37
+ const deploying = await this.getDeployingSyncConfig();
38
+ const active = await this.getActiveSyncConfig();
37
39
 
38
- if (next?.sync_rules_content == options.config.yaml) {
40
+ if (deploying?.content.sync_rules_content == options.config.yaml) {
39
41
  logger.info('Sync config unchanged');
40
42
  return { updated: false };
41
- } else if (next == null && active?.sync_rules_content == options.config.yaml) {
43
+ } else if (deploying == null && active?.content.sync_rules_content == options.config.yaml) {
42
44
  logger.info('Sync config unchanged');
43
45
  return { updated: false };
44
46
  } else {
@@ -51,12 +53,15 @@ export abstract class BucketStorageFactory
51
53
  /**
52
54
  * Get a storage instance to query sync data for specific sync config.
53
55
  */
54
- abstract getInstance(syncRules: PersistedSyncRulesContent, options?: GetIntanceOptions): SyncRulesBucketStorage;
56
+ abstract getInstance(
57
+ replicationStream: PersistedReplicationStream,
58
+ options?: GetIntanceOptions
59
+ ): SyncRulesBucketStorage;
55
60
 
56
61
  /**
57
62
  * Deploy new sync config.
58
63
  */
59
- abstract updateSyncRules(options: UpdateSyncRulesOptions): Promise<PersistedSyncRulesContent>;
64
+ abstract updateSyncRules(options: UpdateSyncRulesOptions): Promise<PersistedReplicationStream>;
60
65
 
61
66
  /**
62
67
  * Indicate that a slot was removed, and we should re-sync by creating
@@ -68,48 +73,27 @@ export abstract class BucketStorageFactory
68
73
  *
69
74
  * Replication should be restarted after this.
70
75
  */
71
- abstract restartReplication(sync_rules_group_id: number): Promise<void>;
76
+ abstract restartReplication(replicationStreamId: number): Promise<void>;
72
77
 
73
78
  /**
74
- * Get the sync config used for querying.
79
+ * Get the sync config and storage used for querying.
75
80
  */
76
- async getActiveSyncRules(options: ParseSyncRulesOptions): Promise<PersistedSyncRules | null> {
77
- const content = await this.getActiveSyncRulesContent();
78
- return content?.parsed(options) ?? null;
79
- }
81
+ abstract getActiveSyncConfig(): Promise<ResolvedSyncConfig | null>;
80
82
 
81
83
  /**
82
- * Get the sync config used for querying.
84
+ * Get the sync config and storage that is still deploying.
83
85
  */
84
- abstract getActiveSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
86
+ abstract getDeployingSyncConfig(): Promise<ResolvedSyncConfig | null>;
85
87
 
86
88
  /**
87
- * Get the sync config that will be active next once done with initial replicatino.
89
+ * Get all replication streams currently replicating.
88
90
  */
89
- async getNextSyncRules(options: ParseSyncRulesOptions): Promise<PersistedSyncRules | null> {
90
- const content = await this.getNextSyncRulesContent();
91
- return content?.parsed(options) ?? null;
92
- }
91
+ abstract getReplicatingReplicationStreams(): Promise<PersistedReplicationStream[]>;
93
92
 
94
93
  /**
95
- * Get the sync config that will be active next once done with initial replicatino.
94
+ * Get all replication streams stopped but not terminated yet.
96
95
  */
97
- abstract getNextSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
98
-
99
- /**
100
- * Get all sync config currently replicating. Typically this is the "active" and "next" sync config.
101
- */
102
- abstract getReplicatingSyncRules(): Promise<PersistedSyncRulesContent[]>;
103
-
104
- /**
105
- * Get all sync config stopped but not terminated yet.
106
- */
107
- abstract getStoppedSyncRules(): Promise<PersistedSyncRulesContent[]>;
108
-
109
- /**
110
- * Get the active storage instance.
111
- */
112
- abstract getActiveStorage(): Promise<SyncRulesBucketStorage | null>;
96
+ abstract getStoppedReplicationStreams(): Promise<PersistedReplicationStream[]>;
113
97
 
114
98
  /**
115
99
  * Get storage size of active replication stream.
@@ -134,6 +118,12 @@ export interface BucketStorageFactoryListener {
134
118
  replicationEvent: (event: ReplicationEventPayload) => void;
135
119
  }
136
120
 
121
+ export interface ResolvedSyncConfig {
122
+ content: PersistedSyncConfigContent;
123
+ replicationStream: PersistedReplicationStream;
124
+ storage: SyncRulesBucketStorage;
125
+ }
126
+
137
127
  export interface StorageMetrics {
138
128
  /**
139
129
  * Size of operations (bucket_data)
@@ -181,7 +171,7 @@ export interface SerializedSyncPlan {
181
171
  /**
182
172
  * The serialized plan, from {@link serializeSyncPlan}.
183
173
  */
184
- plan: unknown;
174
+ plan: SerializedSyncPlanV1;
185
175
  compatibility: SerializedCompatibilityContext;
186
176
  /**
187
177
  * Event descriptors are not currently represented in the sync plan because they don't use the sync streams compiler
@@ -278,4 +268,5 @@ export interface TestStorageConfig {
278
268
  factory: TestStorageFactory;
279
269
  tableIdStrings: boolean;
280
270
  storageVersion?: number;
271
+ compressedBucketStorage?: boolean;
281
272
  }
@@ -0,0 +1,25 @@
1
+ import { HydratedSyncConfig, HydrationState, SyncConfigWithErrors } from '@powersync/service-sync-rules';
2
+
3
+ /**
4
+ * A parsed sync config set is the identity boundary for parsed sync config state.
5
+ *
6
+ * All values on a single instance - the parsed configs, hydration state and hydrated
7
+ * sync config - are derived from the same parse, and may be associated with each other
8
+ * by object identity. Values from two different instances must never be mixed, even if
9
+ * they were parsed from the same persisted content.
10
+ */
11
+ export interface ParsedSyncConfigSet {
12
+ readonly replicationStreamId: number;
13
+ readonly replicationStreamName: string;
14
+ readonly syncConfigs: SyncConfigWithErrors[];
15
+ /**
16
+ * For testing only.
17
+ */
18
+ readonly hydrationState: HydrationState;
19
+
20
+ /**
21
+ * The hydrated sync config for this parse. This is a stable property: repeated reads
22
+ * return the same instance.
23
+ */
24
+ readonly hydratedSyncConfig: HydratedSyncConfig;
25
+ }
@@ -0,0 +1,55 @@
1
+ import { logger as defaultLogger, ErrorCode, ServiceError } from '@powersync/lib-services-framework';
2
+ import { Logger } from 'winston';
3
+ import { SyncRuleState } from './BucketStorage.js';
4
+ import type { ParsedSyncConfigSet } from './ParsedSyncConfigSet.js';
5
+ import type { ParseSyncConfigOptions, PersistedSyncConfigContent } from './PersistedSyncConfigContent.js';
6
+ import { ReplicationLock } from './ReplicationLock.js';
7
+ import { STORAGE_VERSION_CONFIG, StorageVersionConfig } from './StorageVersionConfig.js';
8
+
9
+ export abstract class PersistedReplicationStream implements PersistedReplicationStreamData {
10
+ readonly replicationStreamId: number;
11
+ readonly replicationJobId: string;
12
+ readonly replicationStreamName: string;
13
+ readonly state: SyncRuleState;
14
+ readonly storageVersion: number;
15
+ readonly logger: Logger;
16
+
17
+ abstract readonly syncConfigContent: readonly PersistedSyncConfigContent[];
18
+ abstract readonly current_lock: ReplicationLock | null;
19
+
20
+ constructor(data: PersistedReplicationStreamData) {
21
+ this.replicationStreamId = data.replicationStreamId;
22
+ this.replicationJobId = data.replicationJobId ?? String(data.replicationStreamId);
23
+ this.replicationStreamName = data.replicationStreamName;
24
+ this.state = data.state;
25
+ this.storageVersion = data.storageVersion;
26
+ this.logger = defaultLogger.child({ prefix: `[${this.replicationStreamName}] ` });
27
+ }
28
+
29
+ getStorageConfig(): StorageVersionConfig {
30
+ const storageConfig = STORAGE_VERSION_CONFIG[this.storageVersion];
31
+ if (storageConfig == null) {
32
+ throw new ServiceError(
33
+ ErrorCode.PSYNC_S1005,
34
+ `Unsupported storage version ${this.storageVersion} for replication stream ${this.replicationStreamId}`
35
+ );
36
+ }
37
+ return storageConfig;
38
+ }
39
+
40
+ abstract lock(): Promise<ReplicationLock>;
41
+
42
+ abstract parsed(options: ParseSyncConfigOptions): ParsedSyncConfigSet;
43
+ }
44
+ export interface PersistedReplicationStreamData {
45
+ readonly replicationStreamId: number;
46
+ readonly replicationStreamName: string;
47
+ readonly state: SyncRuleState;
48
+ readonly storageVersion: number;
49
+ /**
50
+ * Uniquely identifies a job, as a combination of replication stream and processing sync configs.
51
+ *
52
+ * Used in-memory only.
53
+ */
54
+ readonly replicationJobId?: string;
55
+ }