@powersync/service-core 1.20.5 → 1.22.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 (135) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/api/RouteAPI.d.ts +3 -3
  3. package/dist/api/diagnostics.d.ts +1 -1
  4. package/dist/api/diagnostics.js +19 -3
  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/teardown-action.js +1 -1
  9. package/dist/entry/commands/teardown-action.js.map +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/modules/AbstractModule.d.ts +1 -1
  14. package/dist/replication/AbstractReplicationJob.js +1 -1
  15. package/dist/replication/AbstractReplicationJob.js.map +1 -1
  16. package/dist/replication/AbstractReplicator.d.ts +6 -6
  17. package/dist/replication/AbstractReplicator.js +21 -21
  18. package/dist/replication/AbstractReplicator.js.map +1 -1
  19. package/dist/replication/RelationCache.d.ts +9 -2
  20. package/dist/replication/RelationCache.js +21 -2
  21. package/dist/replication/RelationCache.js.map +1 -1
  22. package/dist/routes/configure-fastify.js +3 -1
  23. package/dist/routes/configure-fastify.js.map +1 -1
  24. package/dist/routes/endpoints/admin.js +16 -8
  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 +8 -8
  31. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  32. package/dist/routes/endpoints/sync-stream.js +2 -2
  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 +4 -4
  38. package/dist/runner/teardown.js.map +1 -1
  39. package/dist/storage/BucketStorage.d.ts +9 -9
  40. package/dist/storage/BucketStorage.js +9 -9
  41. package/dist/storage/BucketStorageBatch.d.ts +29 -0
  42. package/dist/storage/BucketStorageBatch.js.map +1 -1
  43. package/dist/storage/BucketStorageFactory.d.ts +27 -18
  44. package/dist/storage/BucketStorageFactory.js +13 -12
  45. package/dist/storage/BucketStorageFactory.js.map +1 -1
  46. package/dist/storage/PersistedSyncRulesContent.d.ts +6 -4
  47. package/dist/storage/PersistedSyncRulesContent.js +15 -8
  48. package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
  49. package/dist/storage/SourceEntity.d.ts +8 -1
  50. package/dist/storage/SourceTable.d.ts +32 -11
  51. package/dist/storage/SourceTable.js +41 -15
  52. package/dist/storage/SourceTable.js.map +1 -1
  53. package/dist/storage/StorageVersionConfig.d.ts +1 -1
  54. package/dist/storage/StorageVersionConfig.js +1 -1
  55. package/dist/storage/SyncRulesBucketStorage.d.ts +63 -18
  56. package/dist/storage/SyncRulesBucketStorage.js +14 -0
  57. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  58. package/dist/storage/WriteCheckpointAPI.d.ts +6 -6
  59. package/dist/storage/WriteCheckpointAPI.js +1 -1
  60. package/dist/storage/bson.d.ts +0 -1
  61. package/dist/storage/bson.js +0 -4
  62. package/dist/storage/bson.js.map +1 -1
  63. package/dist/sync/BucketChecksumState.d.ts +6 -9
  64. package/dist/sync/BucketChecksumState.js +117 -58
  65. package/dist/sync/BucketChecksumState.js.map +1 -1
  66. package/dist/sync/sync.d.ts +2 -2
  67. package/dist/sync/sync.js.map +1 -1
  68. package/dist/tracing/PerformanceTracer.d.ts +60 -0
  69. package/dist/tracing/PerformanceTracer.js +105 -0
  70. package/dist/tracing/PerformanceTracer.js.map +1 -0
  71. package/dist/tracing/TraceWriter.d.ts +22 -0
  72. package/dist/tracing/TraceWriter.js +63 -0
  73. package/dist/tracing/TraceWriter.js.map +1 -0
  74. package/dist/util/checkpointing.js +1 -1
  75. package/dist/util/config/compound-config-collector.d.ts +1 -1
  76. package/dist/util/config/compound-config-collector.js +2 -2
  77. package/dist/util/config/compound-config-collector.js.map +1 -1
  78. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -1
  79. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
  80. package/dist/util/env.js +1 -1
  81. package/dist/util/protocol-types.d.ts +1 -1
  82. package/dist/util/protocol-types.js +1 -1
  83. package/dist/util/util-index.d.ts +1 -0
  84. package/dist/util/util-index.js +1 -0
  85. package/dist/util/util-index.js.map +1 -1
  86. package/dist/util/utils.d.ts +5 -0
  87. package/dist/util/utils.js +7 -0
  88. package/dist/util/utils.js.map +1 -1
  89. package/package.json +11 -11
  90. package/src/api/RouteAPI.ts +3 -3
  91. package/src/api/diagnostics.ts +29 -6
  92. package/src/auth/RemoteJWKSCollector.ts +3 -1
  93. package/src/entry/commands/teardown-action.ts +1 -1
  94. package/src/index.ts +2 -0
  95. package/src/modules/AbstractModule.ts +1 -1
  96. package/src/replication/AbstractReplicationJob.ts +1 -1
  97. package/src/replication/AbstractReplicator.ts +23 -23
  98. package/src/replication/RelationCache.ts +23 -4
  99. package/src/routes/configure-fastify.ts +8 -1
  100. package/src/routes/endpoints/admin.ts +17 -8
  101. package/src/routes/endpoints/checkpointing.ts +1 -1
  102. package/src/routes/endpoints/socket-route.ts +1 -1
  103. package/src/routes/endpoints/sync-rules.ts +8 -8
  104. package/src/routes/endpoints/sync-stream.ts +2 -2
  105. package/src/routes/route-register.ts +73 -4
  106. package/src/runner/teardown.ts +4 -4
  107. package/src/storage/BucketStorage.ts +9 -9
  108. package/src/storage/BucketStorageBatch.ts +32 -0
  109. package/src/storage/BucketStorageFactory.ts +35 -23
  110. package/src/storage/PersistedSyncRulesContent.ts +20 -12
  111. package/src/storage/SourceEntity.ts +9 -1
  112. package/src/storage/SourceTable.ts +56 -22
  113. package/src/storage/StorageVersionConfig.ts +1 -1
  114. package/src/storage/SyncRulesBucketStorage.ts +74 -22
  115. package/src/storage/WriteCheckpointAPI.ts +6 -6
  116. package/src/storage/bson.ts +0 -5
  117. package/src/sync/BucketChecksumState.ts +142 -78
  118. package/src/sync/sync.ts +4 -4
  119. package/src/tracing/PerformanceTracer.ts +149 -0
  120. package/src/tracing/TraceWriter.ts +67 -0
  121. package/src/util/checkpointing.ts +1 -1
  122. package/src/util/config/compound-config-collector.ts +3 -3
  123. package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -1
  124. package/src/util/env.ts +1 -1
  125. package/src/util/protocol-types.ts +1 -1
  126. package/src/util/util-index.ts +1 -0
  127. package/src/util/utils.ts +8 -0
  128. package/test/src/auth.test.ts +120 -1
  129. package/test/src/diagnostics.test.ts +155 -0
  130. package/test/src/routes/error-handler.integration.test.ts +275 -0
  131. package/test/src/routes/stream.test.ts +15 -4
  132. package/test/src/storage/SourceTable.test.ts +89 -0
  133. package/test/src/sync/BucketChecksumState.test.ts +244 -80
  134. package/test/tsconfig.json +0 -1
  135. package/tsconfig.tsbuildinfo +1 -1
@@ -1,7 +1,8 @@
1
1
  import type fastify from 'fastify';
2
+ import * as zlib from 'node:zlib';
2
3
  import * as uuid from 'uuid';
3
4
 
4
- import { errors, HTTPMethod, logger, RouteNotFound, router, ServiceError } from '@powersync/lib-services-framework';
5
+ import { errors, HTTPMethod, logger, RouteNotFound, router } from '@powersync/lib-services-framework';
5
6
  import { FastifyReply } from 'fastify';
6
7
  import { Context, ContextProvider, RequestEndpoint, RequestEndpointHandlerPayload } from './router.js';
7
8
 
@@ -105,18 +106,86 @@ export function registerFastifyNotFoundHandler(app: fastify.FastifyInstance) {
105
106
  });
106
107
  }
107
108
 
108
- function serviceErrorToResponse(error: ServiceError): router.RouterResponse {
109
+ /** Registers a custom error handler that emits service-error JSON honouring any negotiated `Content-Encoding`. */
110
+ export function registerFastifyErrorHandler(app: fastify.FastifyInstance) {
111
+ app.setErrorHandler<Error>(async (error, _request, reply) => {
112
+ if (reply.raw.headersSent) {
113
+ // Headers already flushed - reply.code/header would throw ERR_HTTP_HEADERS_SENT.
114
+ reply.raw.destroy(error);
115
+ return;
116
+ }
117
+
118
+ const { status, data } = fastifyErrorToResponse(error);
119
+ if (status >= 500) {
120
+ logger.error('Request failed', error);
121
+ } else {
122
+ logger.warn(`Request failed: ${error.message}`, {
123
+ status,
124
+ code: data.error.code
125
+ });
126
+ }
127
+
128
+ const json = JSON.stringify(data);
129
+
130
+ // Fastify's default handler leaves `content-encoding` intact when it rewrites the body.
131
+ const encoding = reply.getHeader('content-encoding');
132
+ let body: string | Buffer = json;
133
+ if (encoding === 'gzip') {
134
+ body = zlib.gzipSync(json);
135
+ } else if (encoding === 'zstd') {
136
+ body = zlib.zstdCompressSync(json);
137
+ } else {
138
+ reply.removeHeader('content-encoding');
139
+ }
140
+
141
+ reply
142
+ .code(status)
143
+ .header('content-type', 'application/json; charset=utf-8')
144
+ .header('content-length', Buffer.byteLength(body))
145
+ .send(body);
146
+ });
147
+ }
148
+
149
+ type ErrorResponseData = { error: errors.ErrorData };
150
+
151
+ function serviceErrorToResponse(serviceError: errors.ServiceError): router.RouterResponse<ErrorResponseData> {
109
152
  return new router.RouterResponse({
110
- status: error.errorData.status || 500,
153
+ status: serviceError.errorData.status || 500,
111
154
  headers: {
112
155
  'Content-Type': 'application/json'
113
156
  },
114
157
  data: {
115
- error: error.errorData
158
+ error: serviceError.errorData
116
159
  }
117
160
  });
118
161
  }
119
162
 
163
+ function fastifyErrorToResponse(error: any): router.RouterResponse<ErrorResponseData> {
164
+ // Preserve 4xx status from Fastify built-ins (validation, invalid JSON body, etc.) instead of collapsing to 500.
165
+ if (
166
+ typeof error?.statusCode === 'number' &&
167
+ error.statusCode >= 400 &&
168
+ error.statusCode < 500 &&
169
+ typeof error.code === 'string'
170
+ ) {
171
+ return new router.RouterResponse({
172
+ status: error.statusCode,
173
+ headers: {
174
+ 'Content-Type': 'application/json'
175
+ },
176
+ data: {
177
+ error: {
178
+ name: error.name,
179
+ code: error.code,
180
+ status: error.statusCode,
181
+ description: error.message
182
+ }
183
+ }
184
+ });
185
+ }
186
+ return serviceErrorToResponse(errors.asServiceError(error));
187
+ }
188
+
120
189
  async function respond(reply: FastifyReply, response: router.RouterResponse) {
121
190
  Object.keys(response.headers).forEach((key) => {
122
191
  reply.header(key, response.headers[key]);
@@ -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
  }
@@ -5,6 +5,7 @@ import { InternalOpId } from '../util/utils.js';
5
5
  import { ReplicationEventPayload } from './ReplicationEventPayload.js';
6
6
  import { SourceTable, TableSnapshotStatus } from './SourceTable.js';
7
7
  import { BatchedCustomWriteCheckpointOptions } from './storage-index.js';
8
+ import { ResolveTablesOptions, ResolveTablesResult } from './SyncRulesBucketStorage.js';
8
9
 
9
10
  export const DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS: ResolvedBucketBatchCommitOptions = {
10
11
  createEmptyCheckpoints: true,
@@ -22,6 +23,11 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
22
23
  */
23
24
  last_flushed_op: InternalOpId | null;
24
25
 
26
+ /**
27
+ * True for snapshot batches that should skip rows already present in current_data.
28
+ */
29
+ readonly skipExistingRows: boolean;
30
+
25
31
  /**
26
32
  * Save an op, and potentially flush.
27
33
  *
@@ -91,10 +97,36 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
91
97
 
92
98
  markTableSnapshotDone(tables: SourceTable[], no_checkpoint_before_lsn?: string): Promise<SourceTable[]>;
93
99
  markTableSnapshotRequired(table: SourceTable): Promise<void>;
100
+
101
+ /**
102
+ * Mark the full replication snapshot as done without validating individual source table snapshot state.
103
+ *
104
+ * This is primarily intended for storage tests and setup helpers that manually construct storage state.
105
+ * Replicators should use `markSnapshotDone()` instead, because that validates that all known source tables
106
+ * have completed snapshotting before allowing checkpoints to be unblocked.
107
+ */
94
108
  markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void>;
95
109
 
110
+ /**
111
+ * Mark the full replication snapshot as done after validating that all known source tables have completed snapshotting.
112
+ *
113
+ * Replicators should use this method when completing an initial snapshot. The validation prevents races where
114
+ * new source tables are marked as requiring a snapshot while global snapshot finalization is running.
115
+ *
116
+ * If `throwOnConflict` is false, this will instead return early without throwing if there are source tables that still require snapshotting.
117
+ * Use that only in cases where concurrency is expected, and can automatically retry/continue.
118
+ */
119
+ markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void>;
120
+
96
121
  updateTableProgress(table: SourceTable, progress: Partial<TableSnapshotStatus>): Promise<SourceTable>;
97
122
 
123
+ /**
124
+ * Get the current status for an existing source table without creating or resolving a replacement table.
125
+ */
126
+ getSourceTableStatus(table: SourceTable): Promise<SourceTable | null>;
127
+
128
+ resolveTables(options: ResolveTablesOptions): Promise<ResolveTablesResult>;
129
+
98
130
  /**
99
131
  * Queues the creation of a custom Write Checkpoint. This will be persisted after operations are flushed.
100
132
  */
@@ -17,17 +17,17 @@ import { SyncRulesBucketStorage } from './SyncRulesBucketStorage.js';
17
17
  /**
18
18
  * Represents a configured storage provider.
19
19
  *
20
- * The provider can handle multiple copies of sync rules concurrently, each with their own storage.
21
- * This is to handle replication of a new version of sync rules, while the old version is still active.
20
+ * The provider can handle multiple replication streams concurrently, each with their own storage.
21
+ * This is to handle replication of a new version of sync config, while the old replication stream is still active.
22
22
  *
23
- * Storage APIs for a specific copy of sync rules are provided by the `SyncRulesBucketStorage` instances.
23
+ * Storage APIs for a specific replication stream are provided by the `SyncRulesBucketStorage` instances.
24
24
  */
25
25
  export abstract class BucketStorageFactory
26
26
  extends BaseObserver<BucketStorageFactoryListener>
27
27
  implements AsyncDisposable
28
28
  {
29
29
  /**
30
- * Update sync rules from configuration, if changed.
30
+ * Update sync config from configuration, if changed.
31
31
  */
32
32
  async configureSyncRules(
33
33
  options: UpdateSyncRulesOptions
@@ -36,42 +36,42 @@ export abstract class BucketStorageFactory
36
36
  const active = await this.getActiveSyncRulesContent();
37
37
 
38
38
  if (next?.sync_rules_content == options.config.yaml) {
39
- logger.info('Sync rules from configuration unchanged');
39
+ logger.info('Sync config unchanged');
40
40
  return { updated: false };
41
41
  } else if (next == null && active?.sync_rules_content == options.config.yaml) {
42
- logger.info('Sync rules from configuration unchanged');
42
+ logger.info('Sync config unchanged');
43
43
  return { updated: false };
44
44
  } else {
45
- logger.info('Sync rules updated from configuration');
45
+ logger.info('Sync config updated');
46
46
  const persisted_sync_rules = await this.updateSyncRules(options);
47
47
  return { updated: true, persisted_sync_rules, lock: persisted_sync_rules.current_lock ?? undefined };
48
48
  }
49
49
  }
50
50
 
51
51
  /**
52
- * Get a storage instance to query sync data for specific sync rules.
52
+ * Get a storage instance to query sync data for specific sync config.
53
53
  */
54
54
  abstract getInstance(syncRules: PersistedSyncRulesContent, options?: GetIntanceOptions): SyncRulesBucketStorage;
55
55
 
56
56
  /**
57
- * Deploy new sync rules.
57
+ * Deploy new sync config.
58
58
  */
59
59
  abstract updateSyncRules(options: UpdateSyncRulesOptions): Promise<PersistedSyncRulesContent>;
60
60
 
61
61
  /**
62
62
  * Indicate that a slot was removed, and we should re-sync by creating
63
- * a new sync rules instance.
63
+ * a new replication stream.
64
64
  *
65
65
  * This is roughly the same as deploying a new version of the current sync
66
- * rules, but also accounts for cases where the current sync rules are not
67
- * the latest ones.
66
+ * config, but also accounts for cases where the current sync config is not
67
+ * the latest one.
68
68
  *
69
69
  * Replication should be restarted after this.
70
70
  */
71
71
  abstract restartReplication(sync_rules_group_id: number): Promise<void>;
72
72
 
73
73
  /**
74
- * Get the sync rules used for querying.
74
+ * Get the sync config used for querying.
75
75
  */
76
76
  async getActiveSyncRules(options: ParseSyncRulesOptions): Promise<PersistedSyncRules | null> {
77
77
  const content = await this.getActiveSyncRulesContent();
@@ -79,12 +79,12 @@ export abstract class BucketStorageFactory
79
79
  }
80
80
 
81
81
  /**
82
- * Get the sync rules used for querying.
82
+ * Get the sync config used for querying.
83
83
  */
84
84
  abstract getActiveSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
85
85
 
86
86
  /**
87
- * Get the sync rules that will be active next once done with initial replicatino.
87
+ * Get the sync config that will be active next once done with initial replicatino.
88
88
  */
89
89
  async getNextSyncRules(options: ParseSyncRulesOptions): Promise<PersistedSyncRules | null> {
90
90
  const content = await this.getNextSyncRulesContent();
@@ -92,17 +92,17 @@ export abstract class BucketStorageFactory
92
92
  }
93
93
 
94
94
  /**
95
- * Get the sync rules that will be active next once done with initial replicatino.
95
+ * Get the sync config that will be active next once done with initial replicatino.
96
96
  */
97
97
  abstract getNextSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
98
98
 
99
99
  /**
100
- * Get all sync rules currently replicating. Typically this is the "active" and "next" sync rules.
100
+ * Get all sync config currently replicating. Typically this is the "active" and "next" sync config.
101
101
  */
102
102
  abstract getReplicatingSyncRules(): Promise<PersistedSyncRulesContent[]>;
103
103
 
104
104
  /**
105
- * Get all sync rules stopped but not terminated yet.
105
+ * Get all sync config stopped but not terminated yet.
106
106
  */
107
107
  abstract getStoppedSyncRules(): Promise<PersistedSyncRulesContent[]>;
108
108
 
@@ -112,12 +112,12 @@ export abstract class BucketStorageFactory
112
112
  abstract getActiveStorage(): Promise<SyncRulesBucketStorage | null>;
113
113
 
114
114
  /**
115
- * Get storage size of active sync rules.
115
+ * Get storage size of active replication stream.
116
116
  */
117
117
  abstract getStorageMetrics(): Promise<StorageMetrics>;
118
118
 
119
119
  /**
120
- * Get the unique identifier for this instance of Powersync
120
+ * Get the unique identifier for this instance of Powersync.
121
121
  */
122
122
  abstract getPowerSyncInstanceId(): Promise<string>;
123
123
 
@@ -161,9 +161,20 @@ export interface UpdateSyncRulesOptions {
161
161
  * compiler.
162
162
  */
163
163
  plan: SerializedSyncPlan | null;
164
+
165
+ /**
166
+ * Parsed sync config, primarily to generate a definition mapping.
167
+ * Not persisted, and the defaultSchema used for parsing is not relevant.
168
+ */
169
+ parsed: SyncConfigWithErrors;
164
170
  };
165
171
  lock?: boolean;
166
172
  storageVersion?: number;
173
+
174
+ /**
175
+ * Only relevant if the result is used. This does not affect the persisted config.
176
+ */
177
+ defaultSchema?: string;
167
178
  }
168
179
 
169
180
  export interface SerializedSyncPlan {
@@ -190,7 +201,7 @@ export function updateSyncRulesFromYaml(
190
201
  const config = SqlSyncRules.fromYaml(content, {
191
202
  // No schema-based validation at this point
192
203
  schema: undefined,
193
- defaultSchema: 'not_applicable', // Not needed for validation
204
+ defaultSchema: options?.defaultSchema ?? 'not_applicable', // Not needed for validation
194
205
  throwOnError: options?.validate ?? false
195
206
  });
196
207
 
@@ -198,10 +209,11 @@ export function updateSyncRulesFromYaml(
198
209
  }
199
210
 
200
211
  export function updateSyncRulesFromConfig(
201
- { config, errors }: SyncConfigWithErrors,
212
+ parsed: SyncConfigWithErrors,
202
213
  options?: Omit<UpdateSyncRulesOptions, 'config'>
203
214
  ): UpdateSyncRulesOptions {
204
215
  let plan: SerializedSyncPlan | null = null;
216
+ const { config, errors } = parsed;
205
217
  if (config instanceof PrecompiledSyncConfig) {
206
218
  const eventDescriptors: Record<string, string[]> = {};
207
219
  for (const event of config.eventDescriptors) {
@@ -216,7 +228,7 @@ export function updateSyncRulesFromConfig(
216
228
  };
217
229
  }
218
230
 
219
- return { config: { yaml: config.content, plan }, ...options };
231
+ return { config: { yaml: config.content, plan, parsed }, ...options };
220
232
  }
221
233
 
222
234
  export interface GetIntanceOptions {
@@ -1,13 +1,13 @@
1
- import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
1
+ import { logger as defaultLogger, ErrorCode, Logger, ServiceError } from '@powersync/lib-services-framework';
2
2
  import {
3
3
  CompatibilityContext,
4
4
  CompatibilityOption,
5
5
  DEFAULT_HYDRATION_STATE,
6
6
  deserializeSyncPlan,
7
7
  ErrorLocation,
8
- HydratedSyncRules,
8
+ HydratedSyncConfig,
9
9
  HydrationState,
10
- javaScriptExpressionEngine,
10
+ nodeSqlite,
11
11
  PrecompiledSyncConfig,
12
12
  SqlEventDescriptor,
13
13
  SqlSyncRules,
@@ -15,6 +15,7 @@ import {
15
15
  versionedHydrationState,
16
16
  YamlError
17
17
  } from '@powersync/service-sync-rules';
18
+ import * as sqlite from 'node:sqlite';
18
19
  import { SerializedSyncPlan, UpdateSyncRulesOptions } from './BucketStorageFactory.js';
19
20
  import { ReplicationLock } from './ReplicationLock.js';
20
21
  import { STORAGE_VERSION_CONFIG, StorageVersionConfig } from './StorageVersionConfig.js';
@@ -29,7 +30,7 @@ export interface PersistedSyncRulesContentData {
29
30
  readonly compiled_plan: SerializedSyncPlan | null;
30
31
  readonly slot_name: string;
31
32
  /**
32
- * True if this is the "active" copy of the sync rules.
33
+ * True if this is the "active" copy of the sync config.
33
34
  */
34
35
  readonly active: boolean;
35
36
  readonly storageVersion: number;
@@ -49,6 +50,7 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
49
50
  readonly slot_name!: string;
50
51
  readonly active!: boolean;
51
52
  readonly storageVersion!: number;
53
+ readonly logger: Logger;
52
54
 
53
55
  readonly last_checkpoint_lsn!: string | null;
54
56
 
@@ -61,6 +63,7 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
61
63
 
62
64
  constructor(data: PersistedSyncRulesContentData) {
63
65
  Object.assign(this, data);
66
+ this.logger = defaultLogger.child({ prefix: `[${this.slot_name}] ` });
64
67
  }
65
68
 
66
69
  /**
@@ -73,7 +76,7 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
73
76
  if (storageConfig == null) {
74
77
  throw new ServiceError(
75
78
  ErrorCode.PSYNC_S1005,
76
- `Unsupported storage version ${this.storageVersion} for sync rules ${this.id}`
79
+ `Unsupported storage version ${this.storageVersion} for replication stream ${this.id}`
77
80
  );
78
81
  }
79
82
  return storageConfig;
@@ -99,10 +102,13 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
99
102
 
100
103
  const precompiled = new PrecompiledSyncConfig(plan, compatibility, eventDefinitions, {
101
104
  defaultSchema: options.defaultSchema,
102
- engine: javaScriptExpressionEngine(compatibility),
103
105
  sourceText: this.sync_rules_content
104
106
  });
105
107
 
108
+ // Note: If the original content did not define a storage version, this will still set the storage version.
109
+ // This means asUpdateOptions will not change the storage version, even if the default changes.
110
+ precompiled.storageVersion = this.storageVersion;
111
+
106
112
  const errors: YamlError[] = [];
107
113
  if (this.compiled_plan.errors) {
108
114
  for (const error of this.compiled_plan.errors) {
@@ -135,17 +141,19 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
135
141
  return {
136
142
  id: this.id,
137
143
  slot_name: this.slot_name,
138
- sync_rules: config,
144
+ syncConfigWithErrors: config,
139
145
  hydrationState,
140
- hydratedSyncRules: () => {
141
- return config.config.hydrate({ hydrationState });
146
+ hydratedSyncConfig: () => {
147
+ return config.config.hydrate({ hydrationState, sqlite: nodeSqlite(sqlite) });
142
148
  }
143
149
  };
144
150
  }
145
151
 
146
152
  asUpdateOptions(options?: Omit<UpdateSyncRulesOptions, 'config'>): UpdateSyncRulesOptions {
153
+ // defaultSchema is not relevant for the parsed version here
154
+ const parsed = this.parsed({ defaultSchema: 'not_applicable' });
147
155
  return {
148
- config: { yaml: this.sync_rules_content, plan: this.compiled_plan },
156
+ config: { yaml: this.sync_rules_content, plan: this.compiled_plan, parsed: parsed.syncConfigWithErrors },
149
157
  ...options
150
158
  };
151
159
  }
@@ -155,12 +163,12 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
155
163
 
156
164
  export interface PersistedSyncRules {
157
165
  readonly id: number;
158
- readonly sync_rules: SyncConfigWithErrors;
166
+ readonly syncConfigWithErrors: SyncConfigWithErrors;
159
167
  readonly slot_name: string;
160
168
  /**
161
169
  * For testing only.
162
170
  */
163
171
  readonly hydrationState: HydrationState;
164
172
 
165
- hydratedSyncRules(): HydratedSyncRules;
173
+ hydratedSyncConfig(): HydratedSyncConfig;
166
174
  }
@@ -1,3 +1,5 @@
1
+ import { SourceTableRef } from '@powersync/service-sync-rules';
2
+
1
3
  export interface ColumnDescriptor {
2
4
  name: string;
3
5
  /**
@@ -10,7 +12,7 @@ export interface ColumnDescriptor {
10
12
  typeId?: number;
11
13
  }
12
14
 
13
- export interface SourceEntityDescriptor {
15
+ export interface SourceEntityDescriptor extends SourceTableRef {
14
16
  /**
15
17
  * The internal id of the source entity structure in the database.
16
18
  * If undefined, the schema and name are used as the identifier.
@@ -23,4 +25,10 @@ export interface SourceEntityDescriptor {
23
25
  * The columns that are used to uniquely identify a record in the source entity.
24
26
  */
25
27
  replicaIdColumns: ColumnDescriptor[];
28
+ /**
29
+ * Whether the source always sends complete row data with each operation (e.g. Postgres REPLICA
30
+ * IDENTITY FULL). When true, no current_data copy is needed. Undefined means the source does not
31
+ * report this, in which case we default to keeping a copy.
32
+ */
33
+ sendsCompleteRows?: boolean;
26
34
  }