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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/CHANGELOG.md +86 -7
  2. package/dist/api/RouteAPI.d.ts +17 -3
  3. package/dist/api/api-index.d.ts +1 -1
  4. package/dist/api/api-index.js +1 -1
  5. package/dist/api/api-index.js.map +1 -1
  6. package/dist/api/api-metrics.js.map +1 -1
  7. package/dist/api/diagnostics.d.ts +1 -1
  8. package/dist/api/diagnostics.js +32 -14
  9. package/dist/api/diagnostics.js.map +1 -1
  10. package/dist/auth/CachedKeyCollector.js +1 -1
  11. package/dist/auth/CachedKeyCollector.js.map +1 -1
  12. package/dist/auth/CompoundKeyCollector.js.map +1 -1
  13. package/dist/auth/KeyStore.js.map +1 -1
  14. package/dist/auth/RemoteJWKSCollector.js.map +1 -1
  15. package/dist/auth/StaticKeyCollector.d.ts +1 -1
  16. package/dist/auth/StaticKeyCollector.js.map +1 -1
  17. package/dist/auth/StaticSupabaseKeyCollector.d.ts +1 -1
  18. package/dist/auth/StaticSupabaseKeyCollector.js.map +1 -1
  19. package/dist/entry/commands/compact-action.js +26 -5
  20. package/dist/entry/commands/compact-action.js.map +1 -1
  21. package/dist/entry/commands/teardown-action.js +2 -2
  22. package/dist/entry/commands/teardown-action.js.map +1 -1
  23. package/dist/entry/entry-index.d.ts +1 -1
  24. package/dist/entry/entry-index.js +1 -1
  25. package/dist/entry/entry-index.js.map +1 -1
  26. package/dist/events/EventsEngine.js +1 -1
  27. package/dist/events/EventsEngine.js.map +1 -1
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +1 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/metrics/MetricsEngine.d.ts +1 -1
  32. package/dist/metrics/RollingBucketMax.d.ts +28 -0
  33. package/dist/metrics/RollingBucketMax.js +80 -0
  34. package/dist/metrics/RollingBucketMax.js.map +1 -0
  35. package/dist/metrics/metrics-index.d.ts +3 -2
  36. package/dist/metrics/metrics-index.js +3 -2
  37. package/dist/metrics/metrics-index.js.map +1 -1
  38. package/dist/metrics/open-telemetry/util.js +1 -1
  39. package/dist/metrics/open-telemetry/util.js.map +1 -1
  40. package/dist/metrics/register-metrics.js +2 -2
  41. package/dist/metrics/register-metrics.js.map +1 -1
  42. package/dist/modules/AbstractModule.d.ts +2 -2
  43. package/dist/modules/AbstractModule.js.map +1 -1
  44. package/dist/modules/modules-index.d.ts +1 -1
  45. package/dist/modules/modules-index.js +1 -1
  46. package/dist/modules/modules-index.js.map +1 -1
  47. package/dist/replication/AbstractReplicationJob.d.ts +2 -2
  48. package/dist/replication/AbstractReplicationJob.js +1 -1
  49. package/dist/replication/AbstractReplicationJob.js.map +1 -1
  50. package/dist/replication/AbstractReplicator.d.ts +7 -7
  51. package/dist/replication/AbstractReplicator.js +31 -28
  52. package/dist/replication/AbstractReplicator.js.map +1 -1
  53. package/dist/replication/ReplicationLagTracker.d.ts +50 -0
  54. package/dist/replication/ReplicationLagTracker.js +78 -0
  55. package/dist/replication/ReplicationLagTracker.js.map +1 -0
  56. package/dist/replication/replication-index.d.ts +3 -2
  57. package/dist/replication/replication-index.js +3 -2
  58. package/dist/replication/replication-index.js.map +1 -1
  59. package/dist/replication/replication-metrics.js.map +1 -1
  60. package/dist/routes/configure-fastify.d.ts +59 -32
  61. package/dist/routes/endpoints/admin.d.ts +108 -54
  62. package/dist/routes/endpoints/admin.js +7 -3
  63. package/dist/routes/endpoints/admin.js.map +1 -1
  64. package/dist/routes/endpoints/checkpointing.js +1 -1
  65. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  66. package/dist/routes/endpoints/socket-route.js +1 -1
  67. package/dist/routes/endpoints/socket-route.js.map +1 -1
  68. package/dist/routes/endpoints/sync-rules.js +10 -10
  69. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  70. package/dist/routes/endpoints/sync-stream.d.ts +10 -10
  71. package/dist/routes/endpoints/sync-stream.js +2 -2
  72. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  73. package/dist/routes/hooks.js +1 -1
  74. package/dist/routes/hooks.js.map +1 -1
  75. package/dist/routes/route-register.js.map +1 -1
  76. package/dist/runner/teardown.js +4 -4
  77. package/dist/runner/teardown.js.map +1 -1
  78. package/dist/storage/BucketStorage.d.ts +9 -9
  79. package/dist/storage/BucketStorage.js +9 -9
  80. package/dist/storage/BucketStorageBatch.d.ts +1 -1
  81. package/dist/storage/BucketStorageFactory.d.ts +27 -20
  82. package/dist/storage/BucketStorageFactory.js +19 -16
  83. package/dist/storage/BucketStorageFactory.js.map +1 -1
  84. package/dist/storage/ChecksumCache.js.map +1 -1
  85. package/dist/storage/PersistedSyncRulesContent.d.ts +3 -1
  86. package/dist/storage/PersistedSyncRulesContent.js +24 -5
  87. package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
  88. package/dist/storage/ReplicationEventPayload.d.ts +1 -1
  89. package/dist/storage/ReportStorage.d.ts +3 -3
  90. package/dist/storage/SourceTable.d.ts +4 -4
  91. package/dist/storage/SourceTable.js +3 -3
  92. package/dist/storage/SourceTable.js.map +1 -1
  93. package/dist/storage/StorageVersionConfig.d.ts +1 -1
  94. package/dist/storage/StorageVersionConfig.js +1 -1
  95. package/dist/storage/SyncRulesBucketStorage.d.ts +38 -6
  96. package/dist/storage/SyncRulesBucketStorage.js +14 -0
  97. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  98. package/dist/storage/WriteCheckpointAPI.d.ts +6 -6
  99. package/dist/storage/WriteCheckpointAPI.js +1 -1
  100. package/dist/storage/bson.d.ts +0 -1
  101. package/dist/storage/bson.js +0 -4
  102. package/dist/storage/bson.js.map +1 -1
  103. package/dist/storage/storage-index.d.ts +8 -8
  104. package/dist/storage/storage-index.js +8 -8
  105. package/dist/storage/storage-index.js.map +1 -1
  106. package/dist/storage/storage-metrics.js.map +1 -1
  107. package/dist/streams/streams-index.d.ts +2 -2
  108. package/dist/streams/streams-index.js +2 -2
  109. package/dist/streams/streams-index.js.map +1 -1
  110. package/dist/sync/BucketChecksumState.d.ts +2 -5
  111. package/dist/sync/BucketChecksumState.js +119 -75
  112. package/dist/sync/BucketChecksumState.js.map +1 -1
  113. package/dist/sync/RequestTracker.js +1 -1
  114. package/dist/sync/RequestTracker.js.map +1 -1
  115. package/dist/sync/sync-index.d.ts +2 -2
  116. package/dist/sync/sync-index.js +2 -2
  117. package/dist/sync/sync-index.js.map +1 -1
  118. package/dist/sync/sync.js.map +1 -1
  119. package/dist/sync/util.js.map +1 -1
  120. package/dist/system/ServiceContext.d.ts +1 -1
  121. package/dist/system/ServiceContext.js +1 -1
  122. package/dist/system/ServiceContext.js.map +1 -1
  123. package/dist/tracing/PerformanceTracer.d.ts +44 -0
  124. package/dist/tracing/PerformanceTracer.js +102 -0
  125. package/dist/tracing/PerformanceTracer.js.map +1 -0
  126. package/dist/tracing/TraceWriter.d.ts +22 -0
  127. package/dist/tracing/TraceWriter.js +63 -0
  128. package/dist/tracing/TraceWriter.js.map +1 -0
  129. package/dist/util/checkpointing.js +1 -1
  130. package/dist/util/config/collectors/impl/base64-config-collector.d.ts +1 -1
  131. package/dist/util/config/collectors/impl/base64-config-collector.js.map +1 -1
  132. package/dist/util/config/collectors/impl/filesystem-config-collector.d.ts +1 -1
  133. package/dist/util/config/collectors/impl/filesystem-config-collector.js +1 -1
  134. package/dist/util/config/collectors/impl/filesystem-config-collector.js.map +1 -1
  135. package/dist/util/config/compound-config-collector.d.ts +1 -1
  136. package/dist/util/config/compound-config-collector.js +2 -2
  137. package/dist/util/config/compound-config-collector.js.map +1 -1
  138. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -1
  139. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
  140. package/dist/util/config/sync-rules/sync-rules-provider.js.map +1 -1
  141. package/dist/util/config.js +1 -1
  142. package/dist/util/config.js.map +1 -1
  143. package/dist/util/env.js +1 -1
  144. package/dist/util/errors.d.ts +3 -0
  145. package/dist/util/errors.js +15 -0
  146. package/dist/util/errors.js.map +1 -0
  147. package/dist/util/protocol-types.d.ts +3 -3
  148. package/dist/util/protocol-types.js +1 -1
  149. package/dist/util/util-index.d.ts +1 -1
  150. package/dist/util/util-index.js +1 -1
  151. package/dist/util/util-index.js.map +1 -1
  152. package/dist/util/utils.d.ts +1 -1
  153. package/package.json +11 -11
  154. package/src/api/RouteAPI.ts +20 -3
  155. package/src/api/api-index.ts +1 -1
  156. package/src/api/api-metrics.ts +1 -1
  157. package/src/api/diagnostics.ts +42 -20
  158. package/src/auth/CachedKeyCollector.ts +2 -3
  159. package/src/auth/CompoundKeyCollector.ts +2 -3
  160. package/src/auth/KeyStore.ts +1 -1
  161. package/src/auth/RemoteJWKSCollector.ts +0 -1
  162. package/src/auth/StaticKeyCollector.ts +1 -1
  163. package/src/auth/StaticSupabaseKeyCollector.ts +1 -1
  164. package/src/entry/commands/compact-action.ts +29 -5
  165. package/src/entry/commands/teardown-action.ts +2 -2
  166. package/src/entry/entry-index.ts +1 -1
  167. package/src/events/EventsEngine.ts +1 -1
  168. package/src/index.ts +2 -0
  169. package/src/metrics/MetricsEngine.ts +1 -1
  170. package/src/metrics/RollingBucketMax.ts +109 -0
  171. package/src/metrics/metrics-index.ts +3 -2
  172. package/src/metrics/open-telemetry/util.ts +1 -1
  173. package/src/metrics/register-metrics.ts +3 -3
  174. package/src/modules/AbstractModule.ts +2 -2
  175. package/src/modules/modules-index.ts +1 -1
  176. package/src/replication/AbstractReplicationJob.ts +3 -3
  177. package/src/replication/AbstractReplicator.ts +32 -30
  178. package/src/replication/ReplicationLagTracker.ts +86 -0
  179. package/src/replication/replication-index.ts +3 -2
  180. package/src/replication/replication-metrics.ts +1 -1
  181. package/src/routes/endpoints/admin.ts +7 -3
  182. package/src/routes/endpoints/checkpointing.ts +1 -1
  183. package/src/routes/endpoints/socket-route.ts +1 -1
  184. package/src/routes/endpoints/sync-rules.ts +10 -12
  185. package/src/routes/endpoints/sync-stream.ts +2 -2
  186. package/src/routes/hooks.ts +2 -2
  187. package/src/routes/route-register.ts +2 -10
  188. package/src/runner/teardown.ts +4 -4
  189. package/src/storage/BucketStorage.ts +9 -9
  190. package/src/storage/BucketStorageBatch.ts +1 -1
  191. package/src/storage/BucketStorageFactory.ts +45 -34
  192. package/src/storage/ChecksumCache.ts +1 -1
  193. package/src/storage/PersistedSyncRulesContent.ts +30 -6
  194. package/src/storage/ReplicationEventPayload.ts +1 -1
  195. package/src/storage/ReportStorage.ts +3 -3
  196. package/src/storage/SourceTable.ts +4 -4
  197. package/src/storage/StorageVersionConfig.ts +1 -1
  198. package/src/storage/SyncRulesBucketStorage.ts +46 -7
  199. package/src/storage/WriteCheckpointAPI.ts +6 -6
  200. package/src/storage/bson.ts +0 -5
  201. package/src/storage/storage-index.ts +8 -8
  202. package/src/storage/storage-metrics.ts +2 -2
  203. package/src/streams/streams-index.ts +2 -2
  204. package/src/sync/BucketChecksumState.ts +141 -93
  205. package/src/sync/RequestTracker.ts +1 -1
  206. package/src/sync/sync-index.ts +2 -2
  207. package/src/sync/sync.ts +2 -8
  208. package/src/sync/util.ts +1 -1
  209. package/src/system/ServiceContext.ts +1 -1
  210. package/src/tracing/PerformanceTracer.ts +126 -0
  211. package/src/tracing/TraceWriter.ts +67 -0
  212. package/src/util/checkpointing.ts +1 -1
  213. package/src/util/config/collectors/impl/base64-config-collector.ts +1 -1
  214. package/src/util/config/collectors/impl/filesystem-config-collector.ts +2 -2
  215. package/src/util/config/compound-config-collector.ts +3 -3
  216. package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -1
  217. package/src/util/config/sync-rules/sync-rules-provider.ts +1 -1
  218. package/src/util/config.ts +1 -1
  219. package/src/util/env.ts +1 -1
  220. package/src/util/errors.ts +21 -0
  221. package/src/util/protocol-types.ts +1 -1
  222. package/src/util/util-index.ts +1 -1
  223. package/src/util/utils.ts +1 -1
  224. package/test/src/ReplicationLagTracker.test.ts +53 -0
  225. package/test/src/RollingBucketMax.test.ts +106 -0
  226. package/test/src/auth.test.ts +115 -7
  227. package/test/src/diagnostics.test.ts +151 -0
  228. package/test/src/module-loader.test.ts +1 -1
  229. package/test/src/routes/mocks.ts +1 -1
  230. package/test/src/routes/stream.test.ts +1 -2
  231. package/test/src/sync/BucketChecksumState.test.ts +223 -67
  232. package/test/src/util/protocol_types.test.ts +1 -1
  233. package/test/tsconfig.json +0 -1
  234. package/tsconfig.tsbuildinfo +1 -1
  235. package/vitest.config.ts +1 -1
@@ -14,6 +14,16 @@ export interface ReplicationLagOptions {
14
14
  bucketStorage: SyncRulesBucketStorage;
15
15
  }
16
16
 
17
+ export interface SlotWalBudgetInfo {
18
+ wal_status: string;
19
+ safe_wal_size?: number;
20
+ max_slot_wal_keep_size?: number;
21
+ }
22
+
23
+ export interface SlotWalBudgetOptions {
24
+ slotName: string;
25
+ }
26
+
17
27
  /**
18
28
  * Describes all the methods currently required to service the sync API endpoints.
19
29
  */
@@ -33,7 +43,7 @@ export interface RouteAPI {
33
43
  * Generates replication table information from a given pattern of tables.
34
44
  *
35
45
  * @param tablePatterns A set of table patterns which typically come from
36
- * the tables listed in sync rules definitions.
46
+ * the tables listed in sync config definitions.
37
47
  *
38
48
  * @param sqlSyncRules
39
49
  * @returns A result of all the tables and columns which should be replicated
@@ -49,6 +59,13 @@ export interface RouteAPI {
49
59
  */
50
60
  getReplicationLagBytes(options: ReplicationLagOptions): Promise<number | undefined>;
51
61
 
62
+ /**
63
+ * @returns WAL budget information for the replication slot, or undefined
64
+ * if the slot doesn't exist or the source doesn't support this.
65
+ * Only implemented by the Postgres adapter.
66
+ */
67
+ getSlotWalBudget?(options: SlotWalBudgetOptions): Promise<SlotWalBudgetInfo | undefined>;
68
+
52
69
  /**
53
70
  * Get the current LSN or equivalent replication HEAD position identifier.
54
71
  *
@@ -59,7 +76,7 @@ export interface RouteAPI {
59
76
 
60
77
  /**
61
78
  * @returns The schema for tables inside the connected database. This is typically
62
- * used to validate sync rules.
79
+ * used to validate sync config.
63
80
  */
64
81
  getConnectionSchema(): Promise<types.DatabaseSchema[]>;
65
82
 
@@ -75,7 +92,7 @@ export interface RouteAPI {
75
92
  shutdown(): Promise<void>;
76
93
 
77
94
  /**
78
- * Get the default schema (or database) when only a table name is specified in sync rules.
95
+ * Get the default schema (or database) when only a table name is specified in sync config.
79
96
  */
80
97
  getParseSyncRulesOptions(): ParseSyncRulesOptions;
81
98
  }
@@ -1,4 +1,4 @@
1
+ export * from './api-metrics.js';
1
2
  export * from './diagnostics.js';
2
3
  export * from './RouteAPI.js';
3
4
  export * from './schema.js';
4
- export * from './api-metrics.js';
@@ -1,5 +1,5 @@
1
- import { MetricsEngine } from '../metrics/MetricsEngine.js';
2
1
  import { APIMetric } from '@powersync/service-types';
2
+ import { MetricsEngine } from '../metrics/MetricsEngine.js';
3
3
 
4
4
  /**
5
5
  * Create and register the core API metrics.
@@ -1,13 +1,14 @@
1
1
  import { logger } from '@powersync/lib-services-framework';
2
- import { DEFAULT_TAG, SourceTableInterface, SqlSyncRules, SyncConfigWithErrors } from '@powersync/service-sync-rules';
2
+ import { DEFAULT_TAG, SourceTableInterface, 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';
6
- import { RouteAPI } from './RouteAPI.js';
6
+ import { syncConfigYamlErrorToReplicationError } from '../util/errors.js';
7
+ import { RouteAPI, SlotWalBudgetInfo } from './RouteAPI.js';
7
8
 
8
9
  export interface DiagnosticsOptions {
9
10
  /**
10
- * Include sync rules content in response.
11
+ * Include sync config content in response.
11
12
  */
12
13
  include_content?: boolean;
13
14
 
@@ -62,6 +63,7 @@ export async function getSyncRulesStatus(
62
63
  const systemStorage = live_status ? bucketStorage.getInstance(sync_rules) : undefined;
63
64
  const status = await systemStorage?.getStatus();
64
65
  let replication_lag_bytes: number | undefined = undefined;
66
+ let slot_wal_budget: SlotWalBudgetInfo | undefined = undefined;
65
67
 
66
68
  let tables_flat: TableInfo[] = [];
67
69
 
@@ -87,6 +89,16 @@ export async function getSyncRulesStatus(
87
89
  // Ignore
88
90
  logger.warn(`Unable to get replication lag`, e);
89
91
  }
92
+
93
+ if (apiHandler.getSlotWalBudget && sync_rules.slot_name) {
94
+ try {
95
+ slot_wal_budget = await apiHandler.getSlotWalBudget({
96
+ slotName: sync_rules.slot_name
97
+ });
98
+ } catch (e) {
99
+ logger.warn(`Unable to get WAL budget`, e);
100
+ }
101
+ }
90
102
  }
91
103
  } else {
92
104
  const source_table_patterns = rules.getSourceTables();
@@ -131,26 +143,33 @@ export async function getSyncRulesStatus(
131
143
  ts: sync_rules.last_fatal_error_ts?.toISOString()
132
144
  });
133
145
  }
134
- errors.push(
135
- ...syncRuleErrors.map(({ type, message, location }) => {
136
- const error: ReplicationError = {
137
- level: type,
138
- message,
146
+ errors.push(...syncRuleErrors.map((error) => syncConfigYamlErrorToReplicationError(error, now)));
147
+
148
+ if (
149
+ slot_wal_budget &&
150
+ slot_wal_budget.wal_status !== 'lost' &&
151
+ slot_wal_budget.safe_wal_size != null &&
152
+ slot_wal_budget.max_slot_wal_keep_size != null &&
153
+ slot_wal_budget.max_slot_wal_keep_size > 0
154
+ ) {
155
+ const budgetPct = Math.max(
156
+ 0,
157
+ Math.round((slot_wal_budget.safe_wal_size / slot_wal_budget.max_slot_wal_keep_size) * 100)
158
+ );
159
+ if (budgetPct <= 50) {
160
+ errors.push({
161
+ level: 'warning',
162
+ message:
163
+ `WAL budget is low: ${budgetPct}% remaining. ` +
164
+ `The replication slot may be invalidated if WAL consumption ` +
165
+ `continues at this rate. Consider increasing max_slot_wal_keep_size.`,
139
166
  ts: now
140
- };
141
- if (location != null) {
142
- error.location = {
143
- start_offset: location.start,
144
- end_offset: location.end
145
- };
146
- }
147
-
148
- return error;
149
- })
150
- );
167
+ });
168
+ }
169
+ }
151
170
 
152
171
  if (live_status && status?.active) {
153
- // Check replication lag for active sync rules.
172
+ // Check replication lag for active replication stream.
154
173
  // Right now we exclude mysql, since it we don't have consistent keepalives for it.
155
174
  if (sync_rules.last_checkpoint_ts == null && sync_rules.last_keepalive_ts == null) {
156
175
  errors.push({
@@ -197,6 +216,9 @@ export async function getSyncRulesStatus(
197
216
  last_checkpoint_ts: sync_rules.last_checkpoint_ts?.toISOString(),
198
217
  last_keepalive_ts: sync_rules.last_keepalive_ts?.toISOString(),
199
218
  replication_lag_bytes: replication_lag_bytes,
219
+ wal_status: slot_wal_budget?.wal_status,
220
+ safe_wal_size: slot_wal_budget?.safe_wal_size,
221
+ max_slot_wal_keep_size: slot_wal_budget?.max_slot_wal_keep_size,
200
222
  tables: tables_flat
201
223
  }
202
224
  ],
@@ -1,9 +1,8 @@
1
- import * as jose from 'jose';
1
+ import { AuthorizationError, ErrorCode, logger } from '@powersync/lib-services-framework';
2
2
  import timers from 'timers/promises';
3
+ import { KeyCollector, KeyResult } from './KeyCollector.js';
3
4
  import { KeySpec } from './KeySpec.js';
4
5
  import { LeakyBucket } from './LeakyBucket.js';
5
- import { KeyCollector, KeyResult } from './KeyCollector.js';
6
- import { AuthorizationError, ErrorCode, logger } from '@powersync/lib-services-framework';
7
6
  import { mapAuthConfigError } from './utils.js';
8
7
 
9
8
  /**
@@ -1,7 +1,6 @@
1
- import * as jose from 'jose';
2
- import { KeySpec } from './KeySpec.js';
3
- import { KeyCollector, KeyResult } from './KeyCollector.js';
4
1
  import { AuthorizationError } from '@powersync/lib-services-framework';
2
+ import { KeyCollector, KeyResult } from './KeyCollector.js';
3
+ import { KeySpec } from './KeySpec.js';
5
4
 
6
5
  export class CompoundKeyCollector implements KeyCollector {
7
6
  private collectors: KeyCollector[];
@@ -4,7 +4,7 @@ import secs from '../util/secs.js';
4
4
  import { JwtPayload } from './JwtPayload.js';
5
5
  import { KeyCollector } from './KeyCollector.js';
6
6
  import { KeyOptions, KeySpec, SUPPORTED_ALGORITHMS } from './KeySpec.js';
7
- import { debugKeyNotFound, mapAuthError, SupabaseAuthDetails, tokenDebugDetails } from './utils.js';
7
+ import { debugKeyNotFound, mapAuthError, SupabaseAuthDetails } from './utils.js';
8
8
 
9
9
  /**
10
10
  * KeyStore to get keys and verify tokens.
@@ -1,6 +1,5 @@
1
1
  import * as http from 'http';
2
2
  import * as https from 'https';
3
- import * as jose from 'jose';
4
3
  import fetch from 'node-fetch';
5
4
 
6
5
  import {
@@ -1,6 +1,6 @@
1
1
  import * as jose from 'jose';
2
- import { KeySpec } from './KeySpec.js';
3
2
  import { KeyCollector, KeyResult } from './KeyCollector.js';
3
+ import { KeySpec } from './KeySpec.js';
4
4
 
5
5
  /**
6
6
  * Set of static keys.
@@ -1,6 +1,6 @@
1
1
  import * as jose from 'jose';
2
- import { KeySpec, KeyOptions } from './KeySpec.js';
3
2
  import { KeyCollector, KeyResult } from './KeyCollector.js';
3
+ import { KeyOptions, KeySpec } from './KeySpec.js';
4
4
 
5
5
  export const SUPABASE_KEY_OPTIONS: KeyOptions = {
6
6
  requiresAudience: ['authenticated'],
@@ -25,7 +25,9 @@ const COMPACT_MEMORY_LIMIT_MB = Math.min(HEAP_LIMIT / 1024 / 1024 - 128, 1024);
25
25
  export function registerCompactAction(program: Command) {
26
26
  const compactCommand = program
27
27
  .command(COMMAND_NAME)
28
- .option(`-b, --buckets [buckets]`, 'Full bucket names, comma-separated (e.g., "global[],mybucket[\\"user1\\"]")');
28
+ .option(`-b, --buckets [buckets]`, 'Full bucket names, comma-separated (e.g., "global[],mybucket[\\"user1\\"]")')
29
+ .option('-p, --parameter-indexes', 'Compacting parameter indexes. Defaults to set unless --buckets is provided.')
30
+ .option('--no-parameter-indexes', 'Disabling compacting parameter indexes.');
29
31
 
30
32
  wrapConfigCommand(compactCommand);
31
33
 
@@ -44,16 +46,31 @@ export function registerCompactAction(program: Command) {
44
46
  process.exit(1);
45
47
  }
46
48
  }
49
+
50
+ let compactParameters: boolean | null = options.parameterIndexes;
51
+
47
52
  if (buckets == null) {
48
53
  logger.info('Compacting storage for all buckets...');
54
+ } else if (buckets.length == 0) {
55
+ logger.info('Skipping bucket compaction');
49
56
  } else {
50
57
  logger.info(`Compacting storage for ${buckets?.join(', ')}...`);
51
58
  }
59
+
52
60
  const config = await utils.loadConfig(extractRunnerOptions(options));
53
61
  const serviceContext = new system.ServiceContextContainer({
54
62
  serviceMode: system.ServiceContextMode.COMPACT,
55
63
  configuration: config
56
64
  });
65
+ const abortController = new AbortController();
66
+ const completion = Promise.withResolvers<void>();
67
+
68
+ serviceContext.lifeCycleEngine.withLifecycle(null, {
69
+ stop: async () => {
70
+ abortController.abort();
71
+ await completion.promise;
72
+ }
73
+ });
57
74
 
58
75
  // Register modules in order to allow custom module compacting
59
76
  const moduleManager = container.getImplementation(modules.ModuleManager);
@@ -71,22 +88,29 @@ export function registerCompactAction(program: Command) {
71
88
  logger.info('No active instance to compact');
72
89
  return;
73
90
  }
74
- logger.info('Performing compaction...');
75
91
  if (buckets != null) {
92
+ logger.info('Performing compaction...');
76
93
  await active.compact({
77
94
  memoryLimitMB: COMPACT_MEMORY_LIMIT_MB,
78
95
  compactBuckets: buckets,
79
- compactParameterData: false
96
+ compactParameterData: compactParameters ?? false,
97
+ signal: abortController.signal
80
98
  });
81
99
  } else {
82
- await active.compact({ memoryLimitMB: COMPACT_MEMORY_LIMIT_MB, compactParameterData: true });
100
+ await active.compact({
101
+ memoryLimitMB: COMPACT_MEMORY_LIMIT_MB,
102
+ compactParameterData: compactParameters ?? true,
103
+ signal: abortController.signal
104
+ });
83
105
  }
84
106
  logger.info('Successfully compacted storage.');
85
107
  } catch (e) {
86
- logger.error(`Failed to compact: ${e.toString()}`);
108
+ logger.error(`Failed to compact:`, e);
87
109
  // Indirectly triggers lifeCycleEngine.stop
88
110
  process.exit(1);
89
111
  } finally {
112
+ // No need to propagate errors on completion - this merely signals that the process can exit.
113
+ completion.resolve();
90
114
  // Indirectly triggers lifeCycleEngine.stop
91
115
  process.exit(0);
92
116
  }
@@ -1,8 +1,8 @@
1
1
  import { Command } from 'commander';
2
2
 
3
+ import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
3
4
  import { teardown } from '../../runner/teardown.js';
4
5
  import { extractRunnerOptions, wrapConfigCommand } from './config-command.js';
5
- import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
6
6
 
7
7
  const COMMAND_NAME = 'teardown';
8
8
 
@@ -13,7 +13,7 @@ export function registerTearDownAction(program: Command) {
13
13
 
14
14
  return teardownCommand
15
15
  .argument('[ack]', 'Type `TEARDOWN` to confirm teardown should occur')
16
- .description('Terminate all replicating sync rules, clear remote configuration and remove all data')
16
+ .description('Terminate all replicating replication streams, clear remote configuration and remove all data')
17
17
  .action(async (ack, options) => {
18
18
  if (ack !== 'TEARDOWN') {
19
19
  throw new ServiceError(ErrorCode.PSYNC_S0102, 'TEARDOWN was not acknowledged.');
@@ -1,6 +1,6 @@
1
1
  export * from './cli-entry.js';
2
+ export * from './commands/compact-action.js';
2
3
  export * from './commands/config-command.js';
3
4
  export * from './commands/migrate-action.js';
4
5
  export * from './commands/start-action.js';
5
6
  export * from './commands/teardown-action.js';
6
- export * from './commands/compact-action.js';
@@ -1,6 +1,6 @@
1
- import EventEmitter from 'node:events';
2
1
  import { logger } from '@powersync/lib-services-framework';
3
2
  import { event_types } from '@powersync/service-types';
3
+ import EventEmitter from 'node:events';
4
4
 
5
5
  export class EventsEngine {
6
6
  private emitter: EventEmitter;
package/src/index.ts CHANGED
@@ -42,4 +42,6 @@ export * as utils from './util/util-index.js';
42
42
  export * from './streams/streams-index.js';
43
43
  export * as streams from './streams/streams-index.js';
44
44
 
45
+ export * from './tracing/PerformanceTracer.js';
46
+
45
47
  export * as bson from 'bson';
@@ -1,5 +1,5 @@
1
1
  import { logger, ServiceAssertionError } from '@powersync/lib-services-framework';
2
- import { Counter, UpDownCounter, ObservableGauge, MetricMetadata, MetricsFactory } from './metrics-interfaces.js';
2
+ import { Counter, MetricMetadata, MetricsFactory, ObservableGauge, UpDownCounter } from './metrics-interfaces.js';
3
3
 
4
4
  export interface MetricsEngineOptions {
5
5
  factory: MetricsFactory;
@@ -0,0 +1,109 @@
1
+ export interface RollingBucketMaxOptions {
2
+ bucketSizeMs?: number;
3
+ windowSizeMs?: number;
4
+ }
5
+
6
+ interface Bucket {
7
+ // Absolute bucket id derived from floor(timestamp / bucketSizeMs).
8
+ id: number;
9
+ // Maximum reported value seen within this bucket.
10
+ max: number | undefined;
11
+ }
12
+
13
+ /**
14
+ * Tracks a rolling max over a fixed number of time buckets.
15
+ *
16
+ * The window is bucket-aligned: with the default 30s window and 5s buckets,
17
+ * the rolling max covers the current 5s bucket plus the previous 5 buckets.
18
+ */
19
+ export class RollingBucketMax {
20
+ private readonly bucketSizeMs: number;
21
+ private readonly bucketCount: number;
22
+ // Fixed-size ring buffer keyed by bucket id modulo bucketCount.
23
+ private readonly buckets: Bucket[];
24
+
25
+ constructor(options: RollingBucketMaxOptions = {}) {
26
+ this.bucketSizeMs = options.bucketSizeMs ?? 5_000;
27
+ const windowSizeMs = options.windowSizeMs ?? 30_000;
28
+
29
+ if (!Number.isInteger(this.bucketSizeMs) || this.bucketSizeMs <= 0) {
30
+ throw new Error('bucketSizeMs must be a positive integer.');
31
+ }
32
+
33
+ if (!Number.isInteger(windowSizeMs) || windowSizeMs <= 0) {
34
+ throw new Error('windowSizeMs must be a positive integer.');
35
+ }
36
+
37
+ if (windowSizeMs % this.bucketSizeMs !== 0) {
38
+ throw new Error('windowSizeMs must be an exact multiple of bucketSizeMs.');
39
+ }
40
+
41
+ this.bucketCount = windowSizeMs / this.bucketSizeMs;
42
+ this.buckets = Array.from({ length: this.bucketCount }, () => ({
43
+ id: Number.NaN,
44
+ max: undefined
45
+ }));
46
+ }
47
+
48
+ /**
49
+ * Reports a new observed value into the bucket for the provided timestamp.
50
+ */
51
+ report(value: number | undefined, timestampMs = Date.now()): void {
52
+ if (value == null) {
53
+ return;
54
+ }
55
+ this.assertFiniteNumber(value, 'value');
56
+ this.assertFiniteNumber(timestampMs, 'timestampMs');
57
+
58
+ const bucket = this.getBucket(this.getBucketId(timestampMs));
59
+ bucket.max = bucket.max === undefined ? value : Math.max(bucket.max, value);
60
+ }
61
+
62
+ /**
63
+ * Returns the maximum value across the current bucket and prior buckets still
64
+ * inside the rolling window, or undefined when the window has no samples.
65
+ */
66
+ getRollingMax(timestampMs = Date.now()): number | undefined {
67
+ this.assertFiniteNumber(timestampMs, 'timestampMs');
68
+
69
+ const currentBucketId = this.getBucketId(timestampMs);
70
+ const minimumBucketId = currentBucketId - this.bucketCount + 1;
71
+
72
+ let rollingMax: number | undefined;
73
+ for (const bucket of this.buckets) {
74
+ if (bucket.max === undefined) {
75
+ continue;
76
+ }
77
+
78
+ if (bucket.id < minimumBucketId || bucket.id > currentBucketId) {
79
+ continue;
80
+ }
81
+
82
+ rollingMax = rollingMax === undefined ? bucket.max : Math.max(rollingMax, bucket.max);
83
+ }
84
+
85
+ return rollingMax;
86
+ }
87
+
88
+ private getBucketId(timestampMs: number): number {
89
+ return Math.floor(timestampMs / this.bucketSizeMs);
90
+ }
91
+
92
+ private getBucket(bucketId: number): Bucket {
93
+ const index = ((bucketId % this.bucketCount) + this.bucketCount) % this.bucketCount;
94
+ const bucket = this.buckets[index];
95
+
96
+ if (bucket.id !== bucketId) {
97
+ bucket.id = bucketId;
98
+ bucket.max = undefined;
99
+ }
100
+
101
+ return bucket;
102
+ }
103
+
104
+ private assertFiniteNumber(value: number, name: string): void {
105
+ if (!Number.isFinite(value)) {
106
+ throw new Error(`${name} must be a finite number.`);
107
+ }
108
+ }
109
+ }
@@ -1,5 +1,6 @@
1
- export * from './MetricsEngine.js';
2
1
  export * from './metrics-interfaces.js';
3
- export * from './register-metrics.js';
2
+ export * from './MetricsEngine.js';
4
3
  export * from './open-telemetry/OpenTelemetryMetricsFactory.js';
5
4
  export * from './open-telemetry/util.js';
5
+ export * from './register-metrics.js';
6
+ export * from './RollingBucketMax.js';
@@ -6,8 +6,8 @@ import { ServiceContext } from '../../system/ServiceContext.js';
6
6
  import { MetricsFactory } from '../metrics-interfaces.js';
7
7
  import { OpenTelemetryMetricsFactory } from './OpenTelemetryMetricsFactory.js';
8
8
 
9
- import pkg from '../../../package.json' with { type: 'json' };
10
9
  import { resourceFromAttributes } from '@opentelemetry/resources';
10
+ import pkg from '../../../package.json' with { type: 'json' };
11
11
 
12
12
  export function createOpenTelemetryMetricsFactory(context: ServiceContext): MetricsFactory {
13
13
  const { configuration, lifeCycleEngine, storageEngine } = context;
@@ -1,9 +1,9 @@
1
- import { ServiceContextContainer } from '../system/ServiceContext.js';
2
- import { createOpenTelemetryMetricsFactory } from './open-telemetry/util.js';
3
- import { MetricsEngine } from './MetricsEngine.js';
4
1
  import { createCoreAPIMetrics, initializeCoreAPIMetrics } from '../api/api-metrics.js';
5
2
  import { createCoreReplicationMetrics, initializeCoreReplicationMetrics } from '../replication/replication-metrics.js';
6
3
  import { createCoreStorageMetrics, initializeCoreStorageMetrics } from '../storage/storage-metrics.js';
4
+ import { ServiceContextContainer } from '../system/ServiceContext.js';
5
+ import { MetricsEngine } from './MetricsEngine.js';
6
+ import { createOpenTelemetryMetricsFactory } from './open-telemetry/util.js';
7
7
 
8
8
  export enum MetricModes {
9
9
  API = 'api',
@@ -1,11 +1,11 @@
1
- import { ServiceContextContainer } from '../system/ServiceContext.js';
2
1
  import { logger } from '@powersync/lib-services-framework';
3
2
  import winston from 'winston';
4
3
  import { PersistedSyncRulesContent } from '../storage/storage-index.js';
4
+ import { ServiceContextContainer } from '../system/ServiceContext.js';
5
5
 
6
6
  export interface TearDownOptions {
7
7
  /**
8
- * If required, tear down any configuration/state for the specific sync rules
8
+ * If required, tear down any configuration/state for the specific replication stream
9
9
  */
10
10
  syncRules?: PersistedSyncRulesContent[];
11
11
  }
@@ -1,3 +1,3 @@
1
- export * from './ModuleManager.js';
2
1
  export * from './AbstractModule.js';
3
2
  export * from './loader.js';
3
+ export * from './ModuleManager.js';
@@ -1,8 +1,8 @@
1
1
  import { container, logger } from '@powersync/lib-services-framework';
2
2
  import winston from 'winston';
3
+ import { MetricsEngine } from '../metrics/MetricsEngine.js';
3
4
  import * as storage from '../storage/storage-index.js';
4
5
  import { ErrorRateLimiter } from './ErrorRateLimiter.js';
5
- import { MetricsEngine } from '../metrics/MetricsEngine.js';
6
6
 
7
7
  export interface AbstractReplicationJobOptions {
8
8
  id: string;
@@ -54,7 +54,7 @@ export abstract class AbstractReplicationJob {
54
54
  * Safely stop the replication process
55
55
  */
56
56
  public async stop(): Promise<void> {
57
- this.logger.info(`Stopping replication job for sync rule iteration: ${this.storage.group_id}`);
57
+ this.logger.info(`Stopping replication job`);
58
58
  this.abortController.abort();
59
59
  await this.isReplicatingPromise;
60
60
  }
@@ -82,5 +82,5 @@ export abstract class AbstractReplicationJob {
82
82
  /**
83
83
  * Get replication lag for this job in ms.
84
84
  */
85
- abstract getReplicationLagMillis(): Promise<number | undefined>;
85
+ abstract getReplicationLagMillis(): number | undefined;
86
86
  }