@powersync/service-core 1.21.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CHANGELOG.md +48 -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/auth/RemoteJWKSCollector.js +3 -2
  7. package/dist/auth/RemoteJWKSCollector.js.map +1 -1
  8. package/dist/entry/commands/compact-action.js +1 -1
  9. package/dist/entry/commands/compact-action.js.map +1 -1
  10. package/dist/modules/AbstractModule.d.ts +6 -2
  11. package/dist/modules/AbstractModule.js.map +1 -1
  12. package/dist/replication/AbstractReplicator.d.ts +2 -2
  13. package/dist/replication/AbstractReplicator.js +24 -23
  14. package/dist/replication/AbstractReplicator.js.map +1 -1
  15. package/dist/replication/RelationCache.d.ts +9 -2
  16. package/dist/replication/RelationCache.js +21 -2
  17. package/dist/replication/RelationCache.js.map +1 -1
  18. package/dist/replication/ReplicationModule.d.ts +3 -3
  19. package/dist/replication/ReplicationModule.js.map +1 -1
  20. package/dist/routes/configure-fastify.d.ts +12 -12
  21. package/dist/routes/configure-fastify.js +3 -1
  22. package/dist/routes/configure-fastify.js.map +1 -1
  23. package/dist/routes/endpoints/admin.d.ts +24 -24
  24. package/dist/routes/endpoints/admin.js +49 -35
  25. package/dist/routes/endpoints/admin.js.map +1 -1
  26. package/dist/routes/endpoints/checkpointing.js +1 -1
  27. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  28. package/dist/routes/endpoints/socket-route.js +1 -1
  29. package/dist/routes/endpoints/socket-route.js.map +1 -1
  30. package/dist/routes/endpoints/sync-rules.js +22 -13
  31. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  32. package/dist/routes/endpoints/sync-stream.js +1 -1
  33. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  34. package/dist/routes/route-register.d.ts +2 -0
  35. package/dist/routes/route-register.js +65 -3
  36. package/dist/routes/route-register.js.map +1 -1
  37. package/dist/runner/teardown.js +8 -8
  38. package/dist/runner/teardown.js.map +1 -1
  39. package/dist/storage/BucketStorageBatch.d.ts +29 -6
  40. package/dist/storage/BucketStorageBatch.js.map +1 -1
  41. package/dist/storage/BucketStorageFactory.d.ts +26 -27
  42. package/dist/storage/BucketStorageFactory.js +5 -19
  43. package/dist/storage/BucketStorageFactory.js.map +1 -1
  44. package/dist/storage/ParsedSyncConfigSet.d.ts +23 -0
  45. package/dist/storage/ParsedSyncConfigSet.js +2 -0
  46. package/dist/storage/ParsedSyncConfigSet.js.map +1 -0
  47. package/dist/storage/PersistedReplicationStream.d.ts +32 -0
  48. package/dist/storage/PersistedReplicationStream.js +26 -0
  49. package/dist/storage/PersistedReplicationStream.js.map +1 -0
  50. package/dist/storage/PersistedSyncConfigContent.d.ts +60 -0
  51. package/dist/storage/PersistedSyncConfigContent.js +123 -0
  52. package/dist/storage/PersistedSyncConfigContent.js.map +1 -0
  53. package/dist/storage/PersistedSyncConfigStatus.d.ts +13 -0
  54. package/dist/storage/PersistedSyncConfigStatus.js +2 -0
  55. package/dist/storage/PersistedSyncConfigStatus.js.map +1 -0
  56. package/dist/storage/SourceEntity.d.ts +8 -1
  57. package/dist/storage/SourceTable.d.ts +37 -10
  58. package/dist/storage/SourceTable.js +50 -14
  59. package/dist/storage/SourceTable.js.map +1 -1
  60. package/dist/storage/SyncRulesBucketStorage.d.ts +80 -24
  61. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  62. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +117 -0
  63. package/dist/storage/implementation/BucketDefinitionMapping.js +277 -0
  64. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
  65. package/dist/storage/implementation/IncrementalReprocessingCompatibility.d.ts +6 -0
  66. package/dist/storage/implementation/IncrementalReprocessingCompatibility.js +31 -0
  67. package/dist/storage/implementation/IncrementalReprocessingCompatibility.js.map +1 -0
  68. package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.d.ts +17 -0
  69. package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js +62 -0
  70. package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js.map +1 -0
  71. package/dist/storage/implementation/ReplicationStreamStorageIds.d.ts +15 -0
  72. package/dist/storage/implementation/ReplicationStreamStorageIds.js +20 -0
  73. package/dist/storage/implementation/ReplicationStreamStorageIds.js.map +1 -0
  74. package/dist/storage/storage-index.d.ts +8 -1
  75. package/dist/storage/storage-index.js +8 -1
  76. package/dist/storage/storage-index.js.map +1 -1
  77. package/dist/sync/BucketChecksumState.d.ts +4 -4
  78. package/dist/sync/BucketChecksumState.js +1 -1
  79. package/dist/sync/BucketChecksumState.js.map +1 -1
  80. package/dist/sync/sync.d.ts +2 -2
  81. package/dist/sync/sync.js +5 -0
  82. package/dist/sync/sync.js.map +1 -1
  83. package/dist/tracing/PerformanceTracer.d.ts +17 -1
  84. package/dist/tracing/PerformanceTracer.js +3 -0
  85. package/dist/tracing/PerformanceTracer.js.map +1 -1
  86. package/dist/util/checkpointing.js +1 -1
  87. package/dist/util/checkpointing.js.map +1 -1
  88. package/dist/util/config/types.d.ts +1 -1
  89. package/dist/util/lsn.d.ts +4 -0
  90. package/dist/util/lsn.js +10 -0
  91. package/dist/util/lsn.js.map +1 -1
  92. package/dist/util/util-index.d.ts +1 -0
  93. package/dist/util/util-index.js +1 -0
  94. package/dist/util/util-index.js.map +1 -1
  95. package/dist/util/utils.d.ts +5 -0
  96. package/dist/util/utils.js +7 -0
  97. package/dist/util/utils.js.map +1 -1
  98. package/package.json +6 -7
  99. package/src/api/RouteAPI.ts +2 -2
  100. package/src/api/diagnostics.ts +45 -28
  101. package/src/auth/RemoteJWKSCollector.ts +3 -1
  102. package/src/entry/commands/compact-action.ts +1 -1
  103. package/src/modules/AbstractModule.ts +6 -2
  104. package/src/replication/AbstractReplicator.ts +27 -26
  105. package/src/replication/RelationCache.ts +23 -4
  106. package/src/replication/ReplicationModule.ts +2 -3
  107. package/src/routes/configure-fastify.ts +8 -1
  108. package/src/routes/endpoints/admin.ts +75 -49
  109. package/src/routes/endpoints/checkpointing.ts +1 -1
  110. package/src/routes/endpoints/socket-route.ts +1 -1
  111. package/src/routes/endpoints/sync-rules.ts +23 -13
  112. package/src/routes/endpoints/sync-stream.ts +1 -1
  113. package/src/routes/route-register.ts +73 -4
  114. package/src/runner/teardown.ts +8 -8
  115. package/src/storage/BucketStorageBatch.ts +32 -7
  116. package/src/storage/BucketStorageFactory.ts +36 -40
  117. package/src/storage/ParsedSyncConfigSet.ts +25 -0
  118. package/src/storage/PersistedReplicationStream.ts +55 -0
  119. package/src/storage/PersistedSyncConfigContent.ts +188 -0
  120. package/src/storage/PersistedSyncConfigStatus.ts +14 -0
  121. package/src/storage/SourceEntity.ts +9 -1
  122. package/src/storage/SourceTable.ts +72 -21
  123. package/src/storage/SyncRulesBucketStorage.ts +87 -26
  124. package/src/storage/implementation/BucketDefinitionMapping.ts +436 -0
  125. package/src/storage/implementation/IncrementalReprocessingCompatibility.ts +41 -0
  126. package/src/storage/implementation/IncrementalReprocessingSyncConfigLog.ts +109 -0
  127. package/src/storage/implementation/ReplicationStreamStorageIds.ts +21 -0
  128. package/src/storage/storage-index.ts +9 -1
  129. package/src/sync/BucketChecksumState.ts +5 -5
  130. package/src/sync/sync.ts +8 -3
  131. package/src/tracing/PerformanceTracer.ts +24 -1
  132. package/src/util/checkpointing.ts +1 -1
  133. package/src/util/config/types.ts +1 -1
  134. package/src/util/lsn.ts +9 -0
  135. package/src/util/util-index.ts +1 -0
  136. package/src/util/utils.ts +8 -0
  137. package/test/src/auth.test.ts +11 -0
  138. package/test/src/diagnostics.test.ts +64 -33
  139. package/test/src/module-loader.test.ts +33 -1
  140. package/test/src/routes/admin.test.ts +180 -18
  141. package/test/src/routes/error-handler.integration.test.ts +275 -0
  142. package/test/src/routes/mocks.ts +6 -2
  143. package/test/src/routes/stream.test.ts +15 -4
  144. package/test/src/storage/SourceTable.test.ts +89 -0
  145. package/test/src/sync/BucketChecksumState.test.ts +25 -17
  146. package/tsconfig.json +1 -4
  147. package/tsconfig.tsbuildinfo +1 -1
  148. package/dist/storage/PersistedSyncRulesContent.d.ts +0 -59
  149. package/dist/storage/PersistedSyncRulesContent.js +0 -100
  150. package/dist/storage/PersistedSyncRulesContent.js.map +0 -1
  151. package/src/storage/PersistedSyncRulesContent.ts +0 -174
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.21.0",
9
- "main": "dist/index.js",
7
+ "version": "1.23.0",
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.4",
35
+ "@powersync/lib-services-framework": "0.9.6",
37
36
  "@powersync/service-jsonbig": "0.17.13",
38
- "@powersync/service-rsocket-router": "0.2.20",
39
- "@powersync/service-sync-rules": "0.36.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,5 +1,5 @@
1
- import { logger } from '@powersync/lib-services-framework';
2
- import { DEFAULT_TAG, SourceTableInterface, SyncConfigWithErrors } from '@powersync/service-sync-rules';
1
+ import { logger, ServiceAssertionError } from '@powersync/lib-services-framework';
2
+ import { DEFAULT_TAG, SourceTableRef, SyncConfigWithErrors } from '@powersync/service-sync-rules';
3
3
  import { ReplicationError, SyncRulesStatus, TableInfo } from '@powersync/service-types';
4
4
 
5
5
  import * as storage from '../storage/storage-index.js';
@@ -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.sync_rules;
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);
@@ -116,7 +131,7 @@ export async function getSyncRulesStatus(
116
131
  errors: [{ level: 'fatal', message: 'connection failed', ts: now }]
117
132
  };
118
133
  } else {
119
- const source: SourceTableInterface = {
134
+ const source: SourceTableRef = {
120
135
  connectionTag: tag,
121
136
  schema: pattern.schema,
122
137
  name: pattern.tablePattern
@@ -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,
@@ -5,6 +5,7 @@ import fetch from 'node-fetch';
5
5
  import {
6
6
  AuthorizationError,
7
7
  ErrorCode,
8
+ hostnameFromSocketAddress,
8
9
  LookupOptions,
9
10
  makeHostnameLookupFunction,
10
11
  ServiceAssertionError,
@@ -140,7 +141,8 @@ export class RemoteJWKSCollector implements KeyCollector {
140
141
  */
141
142
  resolveAgent(): http.Agent | https.Agent {
142
143
  const lookupOptions = this.options?.lookupOptions ?? { reject_ip_ranges: [] };
143
- const lookup = makeHostnameLookupFunction(this.url.hostname, lookupOptions);
144
+ const hostname = hostnameFromSocketAddress(this.url.hostname);
145
+ const lookup = makeHostnameLookupFunction(hostname, lookupOptions);
144
146
 
145
147
  const options: http.AgentOptions = {
146
148
  lookup
@@ -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 {
@@ -38,10 +38,10 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
38
38
  protected logger: winston.Logger;
39
39
  private lockAlerted: boolean = false;
40
40
  /**
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.
41
+ * Map of replication jobs by replication stream job id. Usually there is only one running job, but there could be two
42
+ * when transitioning to a new replication stream.
43
43
  */
44
- private replicationJobs = new Map<number, T>();
44
+ private replicationJobs = new Map<string, T>();
45
45
 
46
46
  /**
47
47
  * Map of replciation stream ids to promises that are clearing the replication stream.
@@ -188,48 +188,49 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
188
188
 
189
189
  let configuredLock = options?.configured_lock;
190
190
 
191
- const existingJobs = new Map<number, T>(this.replicationJobs.entries());
192
- const replicatingSyncRules = await this.storage.getReplicatingSyncRules();
193
- const newJobs = new Map<number, T>();
191
+ const existingJobs = new Map<string, T>(this.replicationJobs.entries());
192
+ const replicatingStreams = await this.storage.getReplicatingReplicationStreams();
193
+ const newJobs = new Map<string, T>();
194
194
  let activeJob: T | undefined = undefined;
195
- for (let syncRules of replicatingSyncRules) {
196
- const existingJob = existingJobs.get(syncRules.id);
197
- if (syncRules.active && activeJob == null) {
195
+ for (let replicationStream of replicatingStreams) {
196
+ const jobId = replicationStream.replicationJobId;
197
+ const existingJob = existingJobs.get(jobId);
198
+ if (replicationStream.state == storage.SyncRuleState.ACTIVE && activeJob == null) {
198
199
  activeJob = existingJob;
199
200
  }
200
201
  if (existingJob && !existingJob.isStopped) {
201
202
  // No change
202
- existingJobs.delete(syncRules.id);
203
- newJobs.set(syncRules.id, existingJob);
203
+ existingJobs.delete(jobId);
204
+ newJobs.set(jobId, existingJob);
204
205
  } else if (existingJob && existingJob.isStopped) {
205
206
  // Stopped (e.g. fatal error).
206
207
  // Remove from the list. Next refresh call will restart the job.
207
- existingJobs.delete(syncRules.id);
208
+ existingJobs.delete(jobId);
208
209
  } else {
209
210
  // New sync config was found (or resume after restart)
210
211
  try {
211
212
  let lock: storage.ReplicationLock;
212
- if (configuredLock?.sync_rules_id == syncRules.id) {
213
+ if (configuredLock?.sync_rules_id == replicationStream.replicationStreamId) {
213
214
  lock = configuredLock;
214
215
  } else {
215
- lock = await syncRules.lock();
216
+ lock = await replicationStream.lock();
216
217
  }
217
- const storage = this.storage.getInstance(syncRules);
218
+ const syncRuleStorage = this.storage.getInstance(replicationStream);
218
219
  const newJob = this.createJob({
219
220
  lock: lock,
220
- storage: storage
221
+ storage: syncRuleStorage
221
222
  });
222
223
 
223
- newJobs.set(syncRules.id, newJob);
224
+ newJobs.set(jobId, newJob);
224
225
  newJob.start();
225
- if (syncRules.active) {
226
+ if (replicationStream.state == storage.SyncRuleState.ACTIVE) {
226
227
  activeJob = newJob;
227
228
  }
228
229
  this.lockAlerted = false;
229
230
  } catch (e) {
230
231
  if (e?.errorData?.code === ErrorCode.PSYNC_S1003) {
231
232
  if (!this.lockAlerted) {
232
- syncRules.logger.info(`[${e.errorData.code}] ${e.errorData.description}`);
233
+ replicationStream.logger.info(`[${e.errorData.code}] ${e.errorData.description}`);
233
234
  this.lockAlerted = true;
234
235
  }
235
236
  } else {
@@ -237,7 +238,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
237
238
  // for example from stricter validation that was added.
238
239
  // This will be retried every couple of seconds.
239
240
  // 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
+ replicationStream.logger.error('Failed to start replication for new sync config', e);
241
242
  }
242
243
  }
243
244
  }
@@ -259,9 +260,9 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
259
260
  }
260
261
 
261
262
  // 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)) {
263
+ const stopped = await this.storage.getStoppedReplicationStreams();
264
+ for (let replicationStream of stopped) {
265
+ if (this.clearingJobs.has(replicationStream.replicationStreamId)) {
265
266
  // Already in progress
266
267
  continue;
267
268
  }
@@ -269,15 +270,15 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
269
270
  // We clear storage asynchronously.
270
271
  // It is important to be able to continue running the refresh loop, otherwise we cannot
271
272
  // retry locked replication stream, for example.
272
- const syncRuleStorage = this.storage.getInstance(syncRules, { skipLifecycleHooks: true });
273
+ const syncRuleStorage = this.storage.getInstance(replicationStream, { skipLifecycleHooks: true });
273
274
  const promise = this.terminateSyncRules(syncRuleStorage)
274
275
  .catch((e) => {
275
276
  syncRuleStorage.logger.warn(`Failed clean up replication config`, e);
276
277
  })
277
278
  .finally(() => {
278
- this.clearingJobs.delete(syncRules.id);
279
+ this.clearingJobs.delete(replicationStream.replicationStreamId);
279
280
  });
280
- this.clearingJobs.set(syncRules.id, promise);
281
+ this.clearingJobs.set(replicationStream.replicationStreamId, promise);
281
282
  }
282
283
  }
283
284
 
@@ -1,24 +1,43 @@
1
1
  import { SourceTable } from '../storage/SourceTable.js';
2
2
 
3
3
  export class RelationCache<T> {
4
- private cache = new Map<string | number, SourceTable>();
4
+ private cache = new Map<string | number, SourceTable[]>();
5
5
  private idFunction: (item: T | SourceTable) => string | number;
6
6
 
7
7
  constructor(idFunction: (item: T | SourceTable) => string | number) {
8
8
  this.idFunction = idFunction;
9
9
  }
10
10
 
11
+ /**
12
+ * Update a single table in-place - use when the snapshot status of the table has changed.
13
+ */
11
14
  update(table: SourceTable) {
12
15
  const id = this.idFunction(table);
13
- this.cache.set(id, table);
16
+ const existing = this.cache.get(id) ?? [];
17
+ const replacementIndex = existing.findIndex((candidate) => candidate.id == table.id);
18
+ if (replacementIndex == -1) {
19
+ this.cache.set(id, [...existing, table]);
20
+ } else {
21
+ const next = [...existing];
22
+ next[replacementIndex] = table;
23
+ this.cache.set(id, next);
24
+ }
14
25
  }
15
26
 
16
- get(source: T | SourceTable): SourceTable | undefined {
27
+ /**
28
+ * Set the full set of tables for a specific reference.
29
+ */
30
+ updateAll(ref: T, tables: SourceTable[]) {
31
+ const id = this.idFunction(ref);
32
+ this.cache.set(id, tables);
33
+ }
34
+
35
+ getAll(source: T | SourceTable): SourceTable[] | undefined {
17
36
  const id = this.idFunction(source);
18
37
  return this.cache.get(id);
19
38
  }
20
39
 
21
- delete(source: T | SourceTable): boolean {
40
+ delete(source: T): boolean {
22
41
  const id = this.idFunction(source);
23
42
  return this.cache.delete(id);
24
43
  }
@@ -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
  {
@@ -1,6 +1,10 @@
1
1
  import type fastify from 'fastify';
2
2
 
3
- import { registerFastifyNotFoundHandler, registerFastifyRoutes } from './route-register.js';
3
+ import {
4
+ registerFastifyErrorHandler,
5
+ registerFastifyNotFoundHandler,
6
+ registerFastifyRoutes
7
+ } from './route-register.js';
4
8
 
5
9
  import * as system from '../system/system-index.js';
6
10
 
@@ -66,6 +70,9 @@ export function configureFastifyServer(server: fastify.FastifyInstance, options:
66
70
  };
67
71
  };
68
72
 
73
+ // Set on the outer server so both child scopes inherit.
74
+ registerFastifyErrorHandler(server);
75
+
69
76
  /**
70
77
  * Fastify creates an encapsulated context for each `.register` call.
71
78
  * Creating a separate context here to separate the concurrency limits for Admin APIs