@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
@@ -1,9 +1,9 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
3
 
4
- import { ConfigCollector, ConfigFileFormat } from '../config-collector.js';
5
- import { RunnerConfig } from '../../types.js';
6
4
  import { logger } from '@powersync/lib-services-framework';
5
+ import { RunnerConfig } from '../../types.js';
6
+ import { ConfigCollector, ConfigFileFormat } from '../config-collector.js';
7
7
 
8
8
  export class FileSystemConfigCollector extends ConfigCollector {
9
9
  get name(): string {
@@ -26,7 +26,7 @@ export type CompoundConfigCollectorOptions = {
26
26
  */
27
27
  configCollectors: ConfigCollector[];
28
28
  /**
29
- * Collectors for PowerSync sync rules content.
29
+ * Collectors for PowerSync sync config content.
30
30
  * The configuration from first collector to provide a configuration
31
31
  * is used. The order of the collectors specifies precedence
32
32
  */
@@ -236,11 +236,11 @@ export class CompoundConfigCollector {
236
236
  return config;
237
237
  }
238
238
  logger.debug(
239
- `Could not collect sync rules with ${collector.name} method. Moving on to next method if available.`
239
+ `Could not collect sync config with ${collector.name} method. Moving on to next method if available.`
240
240
  );
241
241
  } catch (ex) {
242
242
  // An error in a collector is a hard stop
243
- throw new Error(`Could not collect sync rules using ${collector.name} method. Caught exception: ${ex}`);
243
+ throw new Error(`Could not collect sync config using ${collector.name} method. Caught exception: ${ex}`);
244
244
  }
245
245
  }
246
246
  return {
@@ -16,7 +16,7 @@ export class FileSystemSyncRulesCollector extends SyncRulesCollector {
16
16
 
17
17
  const { config_path } = runnerConfig;
18
18
 
19
- // Depending on the container, the sync rules may not actually be present.
19
+ // Depending on the container, the sync config may not actually be present.
20
20
  // Only persist the path here, and load on demand using `loadSyncRules()`.
21
21
  return {
22
22
  present: true,
@@ -1,5 +1,5 @@
1
- import { SyncRulesConfig } from '../types.js';
2
1
  import fs from 'fs/promises';
2
+ import { SyncRulesConfig } from '../types.js';
3
3
 
4
4
  export interface SyncRulesProvider {
5
5
  get(): Promise<string | undefined>;
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import winston from 'winston';
3
3
 
4
- import { container, logger, LogFormat, DEFAULT_LOG_LEVEL, DEFAULT_LOG_FORMAT } from '@powersync/lib-services-framework';
4
+ import { container, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL, LogFormat, logger } from '@powersync/lib-services-framework';
5
5
  import { configFile } from '@powersync/service-types';
6
6
  import { ResolvedPowerSyncConfig, RunnerConfig } from './config/types.js';
7
7
  import { CompoundConfigCollector } from './util-index.js';
package/src/util/env.ts CHANGED
@@ -13,7 +13,7 @@ export const env = utils.collectEnvironmentVariables({
13
13
  POWERSYNC_CONFIG_B64: utils.type.string.optional(),
14
14
  /**
15
15
  * @deprecated use POWERSYNC_SYNC_CONFIG_B64 instead.
16
- * Base64 encoded contents of sync rules YAML
16
+ * Base64 encoded contents of sync config YAML
17
17
  */
18
18
  POWERSYNC_SYNC_RULES_B64: utils.type.string.optional(),
19
19
  /**
@@ -0,0 +1,21 @@
1
+ import { YamlError } from '@powersync/service-sync-rules';
2
+ import { ReplicationError } from '@powersync/service-types';
3
+
4
+ export function syncConfigYamlErrorToReplicationError(
5
+ { type, message, location }: YamlError,
6
+ ts?: string
7
+ ): ReplicationError {
8
+ const error: ReplicationError = {
9
+ level: type,
10
+ message,
11
+ ts
12
+ };
13
+ if (location != null) {
14
+ error.location = {
15
+ start_offset: location.start,
16
+ end_offset: location.end
17
+ };
18
+ }
19
+
20
+ return error;
21
+ }
@@ -77,7 +77,7 @@ export const StreamingSyncRequest = t.object({
77
77
  raw_data: t.boolean.optional(),
78
78
 
79
79
  /**
80
- * Client parameters to be passed to the sync rules.
80
+ * Client parameters to be passed to the sync config.
81
81
  */
82
82
  parameters: t.record(t.any).optional(),
83
83
 
@@ -1,4 +1,5 @@
1
1
  export * from './alerting.js';
2
+ export * from './checkpointing.js';
2
3
  export * from './env.js';
3
4
  export * from './lsn.js';
4
5
  export * from './memory-tracking.js';
@@ -6,7 +7,6 @@ export * from './Mutex.js';
6
7
  export * from './protocol-types.js';
7
8
  export * from './secs.js';
8
9
  export * from './utils.js';
9
- export * from './checkpointing.js';
10
10
  export * from './version.js';
11
11
 
12
12
  export * from './config.js';
package/src/util/utils.ts CHANGED
@@ -2,7 +2,7 @@ import * as sync_rules from '@powersync/service-sync-rules';
2
2
  import * as bson from 'bson';
3
3
  import crypto from 'crypto';
4
4
  import * as uuid from 'uuid';
5
- import { BucketChecksum, ProtocolOpId, OplogEntry } from './protocol-types.js';
5
+ import { BucketChecksum, OplogEntry, ProtocolOpId } from './protocol-types.js';
6
6
 
7
7
  import * as storage from '../storage/storage-index.js';
8
8
 
@@ -0,0 +1,53 @@
1
+ import { ReplicationLagTracker } from '@/replication/ReplicationLagTracker.js';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('ReplicationLagTracker', () => {
5
+ it('returns undefined before replication has started', () => {
6
+ const tracker = new ReplicationLagTracker();
7
+
8
+ expect(tracker.getLagMillis(0)).toBeUndefined();
9
+ });
10
+
11
+ it('tracks the oldest in-flight change and returns current lag', () => {
12
+ const tracker = new ReplicationLagTracker();
13
+
14
+ tracker.trackUncommittedChange(new Date(1_000));
15
+ tracker.trackUncommittedChange(new Date(2_000));
16
+
17
+ expect(tracker.oldestUncommittedChange?.getTime()).toBe(1_000);
18
+ expect(tracker.getCurrentLagMillis(4_000)).toBe(3_000);
19
+ expect(tracker.getLagMillis(4_000)).toBe(3_000);
20
+ });
21
+
22
+ it('records commit lag into the rolling window and clears in-flight state', () => {
23
+ const tracker = new ReplicationLagTracker();
24
+
25
+ tracker.trackUncommittedChange(new Date(0));
26
+ tracker.markCommitted(5_000);
27
+
28
+ expect(tracker.oldestUncommittedChange).toBeNull();
29
+ expect(tracker.isStartingReplication).toBe(false);
30
+ expect(tracker.getLagMillis(5_000)).toBe(5_000);
31
+ expect(tracker.getLagMillis(35_000)).toBe(0);
32
+ });
33
+
34
+ it('can clear in-flight state without changing started state', () => {
35
+ const tracker = new ReplicationLagTracker();
36
+
37
+ tracker.trackUncommittedChange(new Date(0));
38
+ tracker.clearUncommittedChange();
39
+
40
+ expect(tracker.oldestUncommittedChange).toBeNull();
41
+ expect(tracker.isStartingReplication).toBe(true);
42
+ expect(tracker.getLagMillis(5_000)).toBeUndefined();
43
+ });
44
+
45
+ it('can mark replication as started without a committed transaction', () => {
46
+ const tracker = new ReplicationLagTracker();
47
+
48
+ tracker.markStarted();
49
+
50
+ expect(tracker.isStartingReplication).toBe(false);
51
+ expect(tracker.getLagMillis(0)).toBe(0);
52
+ });
53
+ });
@@ -0,0 +1,106 @@
1
+ import { RollingBucketMax } from '@/metrics/RollingBucketMax.js';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('RollingBucketMax', () => {
5
+ it('returns undefined before any values are reported', () => {
6
+ const tracker = new RollingBucketMax();
7
+
8
+ expect(tracker.getRollingMax(0)).toBeUndefined();
9
+ });
10
+
11
+ it('tracks the maximum value within a single bucket', () => {
12
+ const tracker = new RollingBucketMax();
13
+
14
+ tracker.report(3, 100);
15
+ tracker.report(9, 1_000);
16
+ tracker.report(5, 4_999);
17
+
18
+ expect(tracker.getRollingMax(4_999)).toBe(9);
19
+ });
20
+
21
+ it('keeps the rolling max across the last six 5s buckets', () => {
22
+ const tracker = new RollingBucketMax();
23
+
24
+ tracker.report(20, 0);
25
+ tracker.report(11, 5_000);
26
+ tracker.report(12, 10_000);
27
+ tracker.report(13, 15_000);
28
+ tracker.report(14, 20_000);
29
+ tracker.report(15, 25_000);
30
+
31
+ expect(tracker.getRollingMax(29_999)).toBe(20);
32
+ expect(tracker.getRollingMax(30_000)).toBe(15);
33
+ });
34
+
35
+ it('slides the rolling max forward as older buckets age out', () => {
36
+ const tracker = new RollingBucketMax();
37
+
38
+ tracker.report(20, 0);
39
+ expect(tracker.getRollingMax(0)).toBe(20);
40
+
41
+ tracker.report(18, 5_000);
42
+ expect(tracker.getRollingMax(5_000)).toBe(20);
43
+
44
+ tracker.report(16, 10_000);
45
+ expect(tracker.getRollingMax(10_000)).toBe(20);
46
+
47
+ tracker.report(14, 15_000);
48
+ expect(tracker.getRollingMax(15_000)).toBe(20);
49
+
50
+ tracker.report(12, 20_000);
51
+ expect(tracker.getRollingMax(20_000)).toBe(20);
52
+
53
+ tracker.report(10, 25_000);
54
+ expect(tracker.getRollingMax(29_999)).toBe(20);
55
+
56
+ tracker.report(8, 30_000);
57
+ expect(tracker.getRollingMax(30_000)).toBe(18);
58
+
59
+ tracker.report(6, 35_000);
60
+ expect(tracker.getRollingMax(35_000)).toBe(16);
61
+
62
+ tracker.report(4, 40_000);
63
+ expect(tracker.getRollingMax(40_000)).toBe(14);
64
+ });
65
+
66
+ it('keeps newer buckets in the rolling window while older peaks fall out', () => {
67
+ const tracker = new RollingBucketMax();
68
+
69
+ tracker.report(50, 0);
70
+ tracker.report(11, 5_000);
71
+ tracker.report(12, 10_000);
72
+ tracker.report(13, 15_000);
73
+ tracker.report(14, 20_000);
74
+ tracker.report(15, 25_000);
75
+
76
+ expect(tracker.getRollingMax(29_999)).toBe(50);
77
+
78
+ tracker.report(40, 30_000);
79
+ expect(tracker.getRollingMax(30_000)).toBe(40);
80
+ expect(tracker.getRollingMax(34_999)).toBe(40);
81
+ });
82
+
83
+ it('expires values after the rolling window passes with no new reports', () => {
84
+ const tracker = new RollingBucketMax();
85
+
86
+ tracker.report(7, 0);
87
+
88
+ expect(tracker.getRollingMax(29_999)).toBe(7);
89
+ expect(tracker.getRollingMax(30_000)).toBeUndefined();
90
+ });
91
+
92
+ it('supports custom bucket and window sizes', () => {
93
+ const tracker = new RollingBucketMax({
94
+ bucketSizeMs: 1_000,
95
+ windowSizeMs: 3_000
96
+ });
97
+
98
+ tracker.report(4, 0);
99
+ tracker.report(6, 1_000);
100
+ tracker.report(5, 2_000);
101
+
102
+ expect(tracker.getRollingMax(2_999)).toBe(6);
103
+ expect(tracker.getRollingMax(3_000)).toBe(6);
104
+ expect(tracker.getRollingMax(4_000)).toBe(5);
105
+ });
106
+ });
@@ -1,13 +1,21 @@
1
- import { describe, expect, test } from 'vitest';
2
- import { StaticKeyCollector } from '../../src/auth/StaticKeyCollector.js';
1
+ import { StaticSupabaseKeyCollector } from '@/index.js';
2
+ import { configFile } from '@powersync/service-types';
3
3
  import * as jose from 'jose';
4
+ import { describe, expect, test } from 'vitest';
5
+ import { CachedKeyCollector } from '../../src/auth/CachedKeyCollector.js';
6
+ import { KeyResult } from '../../src/auth/KeyCollector.js';
7
+ import {
8
+ EC_ALGORITHMS,
9
+ HS_ALGORITHMS,
10
+ KeySpec,
11
+ OKP_ALGORITHMS,
12
+ RSA_ALGORITHMS,
13
+ SUPPORTED_ALGORITHMS
14
+ } from '../../src/auth/KeySpec.js';
4
15
  import { KeyStore } from '../../src/auth/KeyStore.js';
5
- import { KeySpec } from '../../src/auth/KeySpec.js';
6
16
  import { RemoteJWKSCollector } from '../../src/auth/RemoteJWKSCollector.js';
7
- import { KeyResult } from '../../src/auth/KeyCollector.js';
8
- import { CachedKeyCollector } from '../../src/auth/CachedKeyCollector.js';
9
- import { JwtPayload, StaticSupabaseKeyCollector } from '@/index.js';
10
- import { debugKeyNotFound, getSupabaseJwksUrl } from '../../src/auth/utils.js';
17
+ import { StaticKeyCollector } from '../../src/auth/StaticKeyCollector.js';
18
+ import { getSupabaseJwksUrl } from '../../src/auth/utils.js';
11
19
 
12
20
  const publicKeyRSA: jose.JWK = {
13
21
  use: 'sig',
@@ -52,6 +60,47 @@ const privateKeyECDSA: jose.JWK = {
52
60
  alg: 'ES256'
53
61
  };
54
62
 
63
+ const EC_ALGORITHM_CURVES = [
64
+ ['ES256', 'P-256'],
65
+ ['ES384', 'P-384'],
66
+ ['ES512', 'P-521']
67
+ ] satisfies [string, string][];
68
+
69
+ const EDDSA_CURVES = ['Ed25519', 'Ed448'];
70
+
71
+ function roundTripJwkThroughPowerSyncConfig(key: jose.JWK): jose.JWK {
72
+ const encoded = configFile.strictJwks.encode({
73
+ keys: [key as configFile.StrictJwk]
74
+ });
75
+
76
+ const decoded = configFile.strictJwks.decode(encoded);
77
+ return decoded.keys[0] as jose.JWK;
78
+ }
79
+
80
+ async function signAndVerifyWithKey(alg: string, key: jose.JWK, signKey: jose.KeyLike | Uint8Array) {
81
+ const parsedKey = roundTripJwkThroughPowerSyncConfig(key);
82
+ expect(parsedKey).toEqual(key);
83
+
84
+ const keys = await StaticKeyCollector.importKeys([parsedKey]);
85
+ const store = new KeyStore(keys);
86
+
87
+ const signedJwt = await new jose.SignJWT({ claim: alg })
88
+ .setProtectedHeader({ alg, kid: key.kid })
89
+ .setSubject('f1')
90
+ .setIssuedAt()
91
+ .setIssuer('tester')
92
+ .setAudience('tests')
93
+ .setExpirationTime('5m')
94
+ .sign(signKey);
95
+
96
+ const verified = await store.verifyJwt(signedJwt, {
97
+ defaultAudiences: ['tests'],
98
+ maxAge: '6m'
99
+ });
100
+
101
+ expect(verified.parsedPayload.claim).toEqual(alg);
102
+ }
103
+
55
104
  describe('JWT Auth', () => {
56
105
  test('KeyStore basics', async () => {
57
106
  const keys = await StaticKeyCollector.importKeys([sharedKey]);
@@ -208,6 +257,65 @@ describe('JWT Auth', () => {
208
257
  ).rejects.toThrow('Unexpected token algorithm HS256');
209
258
  });
210
259
 
260
+ describe('supported JWT algorithms', () => {
261
+ test('covers every declared supported algorithm', () => {
262
+ const testedAlgorithms = new Set([
263
+ ...HS_ALGORITHMS,
264
+ ...RSA_ALGORITHMS,
265
+ ...EC_ALGORITHM_CURVES.map(([alg]) => alg),
266
+ ...OKP_ALGORITHMS
267
+ ]);
268
+
269
+ expect([...testedAlgorithms].sort()).toEqual([...SUPPORTED_ALGORITHMS].sort());
270
+ });
271
+
272
+ test.each(HS_ALGORITHMS)('verifies %s tokens', async (alg) => {
273
+ const secret = await jose.generateSecret(alg);
274
+ const key = await jose.exportJWK(secret);
275
+ key.kid = `test-${alg}`;
276
+ key.alg = alg;
277
+
278
+ await signAndVerifyWithKey(alg, key, secret);
279
+ });
280
+
281
+ test.each(RSA_ALGORITHMS)('verifies %s tokens', async (alg) => {
282
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg);
283
+ const key = await jose.exportJWK(publicKey);
284
+ key.kid = `test-${alg}`;
285
+ key.alg = alg;
286
+ key.use = 'sig';
287
+
288
+ await signAndVerifyWithKey(alg, key, privateKey);
289
+ });
290
+
291
+ test.each(EC_ALGORITHM_CURVES)('verifies %s tokens with curve %s', async (alg, crv) => {
292
+ expect(EC_ALGORITHMS).toContain(alg);
293
+
294
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg);
295
+ const key = await jose.exportJWK(publicKey);
296
+ key.kid = `test-${alg}`;
297
+ key.alg = alg;
298
+ key.use = 'sig';
299
+
300
+ expect(key.crv).toEqual(crv);
301
+ await signAndVerifyWithKey(alg, key, privateKey);
302
+ });
303
+
304
+ test.each(EDDSA_CURVES)('verifies EdDSA tokens with curve %s', async (crv) => {
305
+ const alg = 'EdDSA';
306
+ expect(OKP_ALGORITHMS).toContain(alg);
307
+
308
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, { crv });
309
+ const key = await jose.exportJWK(publicKey);
310
+ key.kid = `test-${alg}-${crv}`;
311
+ key.alg = alg;
312
+ key.use = 'sig';
313
+
314
+ expect(key.crv).toEqual(crv);
315
+ await signAndVerifyWithKey(alg, key, privateKey);
316
+ });
317
+ });
318
+
211
319
  test('key selection for key with kid', async () => {
212
320
  const keys = await StaticKeyCollector.importKeys([publicKeyRSA, sharedKey, sharedKey2]);
213
321
  const store = new KeyStore(keys);
@@ -0,0 +1,151 @@
1
+ import { DiagnosticsOptions, getSyncRulesStatus } from '@/api/diagnostics.js';
2
+ import { RouteAPI, SlotWalBudgetInfo } from '@/api/RouteAPI.js';
3
+ import { BucketStorageFactory } from '@/index.js';
4
+ import { SqlSyncRules } from '@powersync/service-sync-rules';
5
+ import { describe, expect, test } from 'vitest';
6
+
7
+ const GB = 1024 * 1024 * 1024;
8
+
9
+ const MINIMAL_SYNC_RULES = `
10
+ bucket_definitions:
11
+ global:
12
+ data:
13
+ - SELECT id FROM test_table
14
+ `;
15
+
16
+ function makeSyncRulesContent(overrides?: { slot_name?: string }) {
17
+ return {
18
+ id: 1,
19
+ slot_name: overrides?.slot_name ?? 'test_slot',
20
+ sync_rules_content: MINIMAL_SYNC_RULES,
21
+ compiled_plan: null,
22
+ active: true,
23
+ storageVersion: 1,
24
+ last_checkpoint_lsn: 'some_lsn',
25
+ last_fatal_error: null,
26
+ last_fatal_error_ts: null,
27
+ last_keepalive_ts: new Date(),
28
+ last_checkpoint_ts: new Date(),
29
+ parsed(options?: any) {
30
+ const syncRules = SqlSyncRules.fromYaml(MINIMAL_SYNC_RULES, {
31
+ ...options,
32
+ defaultSchema: 'public'
33
+ });
34
+ return {
35
+ sync_rules: syncRules
36
+ };
37
+ },
38
+ lock() {
39
+ throw new Error('Not implemented in mock');
40
+ },
41
+ current_lock: undefined
42
+ } as any;
43
+ }
44
+
45
+ function makeBucketStorage() {
46
+ return {
47
+ getInstance() {
48
+ return {
49
+ async getStatus() {
50
+ return {
51
+ snapshot_done: true,
52
+ checkpoint_lsn: 'some_lsn',
53
+ active: true
54
+ };
55
+ }
56
+ };
57
+ }
58
+ } as unknown as BucketStorageFactory;
59
+ }
60
+
61
+ function makeRouteAPI(walBudget?: SlotWalBudgetInfo | undefined): RouteAPI {
62
+ return {
63
+ getParseSyncRulesOptions() {
64
+ return { defaultSchema: 'public' };
65
+ },
66
+ async getSourceConfig() {
67
+ return { tag: 'test', id: 'test', type: 'postgresql' };
68
+ },
69
+ async getConnectionStatus() {
70
+ return { connected: true };
71
+ },
72
+ async getDebugTablesInfo() {
73
+ return [];
74
+ },
75
+ async getReplicationLagBytes() {
76
+ return 0;
77
+ },
78
+ ...(walBudget !== undefined
79
+ ? {
80
+ async getSlotWalBudget() {
81
+ return walBudget;
82
+ }
83
+ }
84
+ : {})
85
+ } as unknown as RouteAPI;
86
+ }
87
+
88
+ const OPTIONS: DiagnosticsOptions = {
89
+ live_status: true,
90
+ check_connection: true,
91
+ include_content: false
92
+ };
93
+
94
+ describe('getSyncRulesStatus WAL budget warnings', () => {
95
+ test('warns when WAL budget is at 40%', async () => {
96
+ const api = makeRouteAPI({
97
+ wal_status: 'extended',
98
+ safe_wal_size: 4 * GB,
99
+ max_slot_wal_keep_size: 10 * GB
100
+ });
101
+ const result = await getSyncRulesStatus(makeBucketStorage(), api, makeSyncRulesContent(), OPTIONS);
102
+ const walWarnings = result!.errors.filter((e) => e.message.includes('WAL budget'));
103
+ expect(walWarnings).toHaveLength(1);
104
+ expect(walWarnings[0].level).toBe('warning');
105
+ expect(walWarnings[0].message).toContain('40%');
106
+ });
107
+
108
+ test('no warning when WAL budget is at 80%', async () => {
109
+ const api = makeRouteAPI({
110
+ wal_status: 'reserved',
111
+ safe_wal_size: 8 * GB,
112
+ max_slot_wal_keep_size: 10 * GB
113
+ });
114
+ const result = await getSyncRulesStatus(makeBucketStorage(), api, makeSyncRulesContent(), OPTIONS);
115
+ const walWarnings = result!.errors.filter((e) => e.message.includes('WAL budget'));
116
+ expect(walWarnings).toHaveLength(0);
117
+ });
118
+
119
+ test('clamps negative safe_wal_size to 0%', async () => {
120
+ const api = makeRouteAPI({
121
+ wal_status: 'unreserved',
122
+ safe_wal_size: -2.4 * GB,
123
+ max_slot_wal_keep_size: 1 * 1024 * 1024 // 1MB
124
+ });
125
+ const result = await getSyncRulesStatus(makeBucketStorage(), api, makeSyncRulesContent(), OPTIONS);
126
+ const walWarnings = result!.errors.filter((e) => e.message.includes('WAL budget'));
127
+ expect(walWarnings).toHaveLength(1);
128
+ expect(walWarnings[0].message).toContain('0%');
129
+ expect(walWarnings[0].message).not.toMatch(/-\d+%/);
130
+ });
131
+
132
+ test('no WAL budget error when slot status is lost', async () => {
133
+ const api = makeRouteAPI({
134
+ wal_status: 'lost'
135
+ });
136
+ const result = await getSyncRulesStatus(makeBucketStorage(), api, makeSyncRulesContent(), OPTIONS);
137
+ const walErrors = result!.errors.filter(
138
+ (e) => e.message.includes('WAL budget') || e.message.includes('PSYNC_S1146')
139
+ );
140
+ expect(walErrors).toHaveLength(0);
141
+ });
142
+
143
+ test('no WAL error when getSlotWalBudget is not defined', async () => {
144
+ const api = makeRouteAPI();
145
+ const result = await getSyncRulesStatus(makeBucketStorage(), api, makeSyncRulesContent(), OPTIONS);
146
+ const walErrors = result!.errors.filter(
147
+ (e) => e.message.includes('WAL budget') || e.message.includes('PSYNC_S1146')
148
+ );
149
+ expect(walErrors).toHaveLength(0);
150
+ });
151
+ });
@@ -1,5 +1,5 @@
1
1
  import { AbstractModule, loadModules, ServiceContextContainer, TearDownOptions } from '@/index.js';
2
- import { describe, expect, it, vi } from 'vitest';
2
+ import { describe, expect, it } from 'vitest';
3
3
 
4
4
  interface MockConfig {
5
5
  connections?: { type: string }[];
@@ -1,3 +1,4 @@
1
+ import { EventsEngine } from '@/events/EventsEngine.js';
1
2
  import {
2
3
  BucketStorageFactory,
3
4
  createCoreAPIMetrics,
@@ -11,7 +12,6 @@ import {
11
12
  SyncRulesBucketStorage
12
13
  } from '@/index.js';
13
14
  import { MeterProvider } from '@opentelemetry/sdk-metrics';
14
- import { EventsEngine } from '@/events/EventsEngine.js';
15
15
 
16
16
  export function mockServiceContext(storage: Partial<SyncRulesBucketStorage> | null) {
17
17
  // This is very incomplete - just enough to get the current tests passing.
@@ -1,6 +1,6 @@
1
1
  import { BasicRouterRequest, Context, JwtPayload, SyncRulesBucketStorage } from '@/index.js';
2
2
  import { RouterResponse, ServiceError, logger } from '@powersync/lib-services-framework';
3
- import { SqlSyncRules } from '@powersync/service-sync-rules';
3
+ import { DEFAULT_HYDRATION_STATE, SqlSyncRules } from '@powersync/service-sync-rules';
4
4
  import { Readable, Writable } from 'stream';
5
5
  import { pipeline } from 'stream/promises';
6
6
  import { describe, expect, it } from 'vitest';
@@ -8,7 +8,6 @@ import winston from 'winston';
8
8
  import { syncStreamed } from '../../../src/routes/endpoints/sync-stream.js';
9
9
  import { DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS, limitParamsForLogging } from '../../../src/util/param-logging.js';
10
10
  import { mockServiceContext } from './mocks.js';
11
- import { DEFAULT_HYDRATION_STATE } from '@powersync/service-sync-rules';
12
11
 
13
12
  describe('Stream Route', () => {
14
13
  describe('compressed stream', () => {