@powersync/service-core 1.19.2 → 1.20.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 (73) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/api/diagnostics.js +11 -4
  3. package/dist/api/diagnostics.js.map +1 -1
  4. package/dist/entry/commands/compact-action.js +13 -2
  5. package/dist/entry/commands/compact-action.js.map +1 -1
  6. package/dist/entry/commands/config-command.js +2 -2
  7. package/dist/entry/commands/config-command.js.map +1 -1
  8. package/dist/replication/AbstractReplicator.js +2 -5
  9. package/dist/replication/AbstractReplicator.js.map +1 -1
  10. package/dist/routes/configure-fastify.d.ts +84 -0
  11. package/dist/routes/endpoints/admin.d.ts +168 -0
  12. package/dist/routes/endpoints/admin.js +33 -20
  13. package/dist/routes/endpoints/admin.js.map +1 -1
  14. package/dist/routes/endpoints/sync-rules.js +6 -9
  15. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  16. package/dist/storage/BucketStorageFactory.d.ts +43 -15
  17. package/dist/storage/BucketStorageFactory.js +70 -1
  18. package/dist/storage/BucketStorageFactory.js.map +1 -1
  19. package/dist/storage/PersistedSyncRulesContent.d.ts +28 -2
  20. package/dist/storage/PersistedSyncRulesContent.js +79 -1
  21. package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
  22. package/dist/storage/StorageVersionConfig.d.ts +20 -0
  23. package/dist/storage/StorageVersionConfig.js +20 -0
  24. package/dist/storage/StorageVersionConfig.js.map +1 -0
  25. package/dist/storage/SyncRulesBucketStorage.d.ts +2 -1
  26. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  27. package/dist/storage/storage-index.d.ts +1 -0
  28. package/dist/storage/storage-index.js +1 -0
  29. package/dist/storage/storage-index.js.map +1 -1
  30. package/dist/sync/BucketChecksumState.d.ts +6 -2
  31. package/dist/sync/BucketChecksumState.js +85 -10
  32. package/dist/sync/BucketChecksumState.js.map +1 -1
  33. package/dist/util/config/collectors/config-collector.js +13 -0
  34. package/dist/util/config/collectors/config-collector.js.map +1 -1
  35. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.d.ts +1 -1
  36. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js +4 -4
  37. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -1
  38. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.d.ts +1 -1
  39. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +2 -2
  40. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
  41. package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.d.ts +1 -1
  42. package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js +3 -3
  43. package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js.map +1 -1
  44. package/dist/util/config/types.d.ts +1 -1
  45. package/dist/util/config/types.js.map +1 -1
  46. package/dist/util/env.d.ts +1 -0
  47. package/dist/util/env.js +5 -0
  48. package/dist/util/env.js.map +1 -1
  49. package/package.json +6 -6
  50. package/src/api/diagnostics.ts +12 -4
  51. package/src/entry/commands/compact-action.ts +15 -2
  52. package/src/entry/commands/config-command.ts +3 -3
  53. package/src/replication/AbstractReplicator.ts +3 -5
  54. package/src/routes/endpoints/admin.ts +42 -25
  55. package/src/routes/endpoints/sync-rules.ts +14 -13
  56. package/src/storage/BucketStorageFactory.ts +110 -19
  57. package/src/storage/PersistedSyncRulesContent.ts +114 -4
  58. package/src/storage/StorageVersionConfig.ts +30 -0
  59. package/src/storage/SyncRulesBucketStorage.ts +2 -1
  60. package/src/storage/storage-index.ts +1 -0
  61. package/src/sync/BucketChecksumState.ts +129 -16
  62. package/src/util/config/collectors/config-collector.ts +16 -0
  63. package/src/util/config/sync-rules/impl/base64-sync-rules-collector.ts +5 -5
  64. package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +3 -3
  65. package/src/util/config/sync-rules/impl/inline-sync-rules-collector.ts +4 -4
  66. package/src/util/config/types.ts +1 -2
  67. package/src/util/env.ts +5 -0
  68. package/test/src/config.test.ts +115 -0
  69. package/test/src/routes/admin.test.ts +48 -0
  70. package/test/src/routes/mocks.ts +22 -1
  71. package/test/src/routes/stream.test.ts +3 -2
  72. package/test/src/sync/BucketChecksumState.test.ts +285 -78
  73. package/tsconfig.tsbuildinfo +1 -1
@@ -1,9 +1,10 @@
1
- import { ObserverClient } from '@powersync/lib-services-framework';
1
+ import { BaseObserver } from '@powersync/lib-services-framework';
2
2
  import { ParseSyncRulesOptions, PersistedSyncRules, PersistedSyncRulesContent } from './PersistedSyncRulesContent.js';
3
3
  import { ReplicationEventPayload } from './ReplicationEventPayload.js';
4
4
  import { ReplicationLock } from './ReplicationLock.js';
5
5
  import { SyncRulesBucketStorage } from './SyncRulesBucketStorage.js';
6
6
  import { ReportStorage } from './ReportStorage.js';
7
+ import { SerializedCompatibilityContext, SyncConfig } from '@powersync/service-sync-rules';
7
8
  /**
8
9
  * Represents a configured storage provider.
9
10
  *
@@ -12,7 +13,7 @@ import { ReportStorage } from './ReportStorage.js';
12
13
  *
13
14
  * Storage APIs for a specific copy of sync rules are provided by the `SyncRulesBucketStorage` instances.
14
15
  */
15
- export interface BucketStorageFactory extends ObserverClient<BucketStorageFactoryListener>, AsyncDisposable {
16
+ export declare abstract class BucketStorageFactory extends BaseObserver<BucketStorageFactoryListener> implements AsyncDisposable {
16
17
  /**
17
18
  * Update sync rules from configuration, if changed.
18
19
  */
@@ -24,11 +25,11 @@ export interface BucketStorageFactory extends ObserverClient<BucketStorageFactor
24
25
  /**
25
26
  * Get a storage instance to query sync data for specific sync rules.
26
27
  */
27
- getInstance(syncRules: PersistedSyncRulesContent, options?: GetIntanceOptions): SyncRulesBucketStorage;
28
+ abstract getInstance(syncRules: PersistedSyncRulesContent, options?: GetIntanceOptions): SyncRulesBucketStorage;
28
29
  /**
29
30
  * Deploy new sync rules.
30
31
  */
31
- updateSyncRules(options: UpdateSyncRulesOptions): Promise<PersistedSyncRulesContent>;
32
+ abstract updateSyncRules(options: UpdateSyncRulesOptions): Promise<PersistedSyncRulesContent>;
32
33
  /**
33
34
  * Indicate that a slot was removed, and we should re-sync by creating
34
35
  * a new sync rules instance.
@@ -39,7 +40,7 @@ export interface BucketStorageFactory extends ObserverClient<BucketStorageFactor
39
40
  *
40
41
  * Replication should be restarted after this.
41
42
  */
42
- restartReplication(sync_rules_group_id: number): Promise<void>;
43
+ abstract restartReplication(sync_rules_group_id: number): Promise<void>;
43
44
  /**
44
45
  * Get the sync rules used for querying.
45
46
  */
@@ -47,7 +48,7 @@ export interface BucketStorageFactory extends ObserverClient<BucketStorageFactor
47
48
  /**
48
49
  * Get the sync rules used for querying.
49
50
  */
50
- getActiveSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
51
+ abstract getActiveSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
51
52
  /**
52
53
  * Get the sync rules that will be active next once done with initial replicatino.
53
54
  */
@@ -55,31 +56,32 @@ export interface BucketStorageFactory extends ObserverClient<BucketStorageFactor
55
56
  /**
56
57
  * Get the sync rules that will be active next once done with initial replicatino.
57
58
  */
58
- getNextSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
59
+ abstract getNextSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
59
60
  /**
60
61
  * Get all sync rules currently replicating. Typically this is the "active" and "next" sync rules.
61
62
  */
62
- getReplicatingSyncRules(): Promise<PersistedSyncRulesContent[]>;
63
+ abstract getReplicatingSyncRules(): Promise<PersistedSyncRulesContent[]>;
63
64
  /**
64
65
  * Get all sync rules stopped but not terminated yet.
65
66
  */
66
- getStoppedSyncRules(): Promise<PersistedSyncRulesContent[]>;
67
+ abstract getStoppedSyncRules(): Promise<PersistedSyncRulesContent[]>;
67
68
  /**
68
69
  * Get the active storage instance.
69
70
  */
70
- getActiveStorage(): Promise<SyncRulesBucketStorage | null>;
71
+ abstract getActiveStorage(): Promise<SyncRulesBucketStorage | null>;
71
72
  /**
72
73
  * Get storage size of active sync rules.
73
74
  */
74
- getStorageMetrics(): Promise<StorageMetrics>;
75
+ abstract getStorageMetrics(): Promise<StorageMetrics>;
75
76
  /**
76
77
  * Get the unique identifier for this instance of Powersync
77
78
  */
78
- getPowerSyncInstanceId(): Promise<string>;
79
+ abstract getPowerSyncInstanceId(): Promise<string>;
79
80
  /**
80
81
  * Get a unique identifier for the system used for storage.
81
82
  */
82
- getSystemIdentifier(): Promise<BucketStorageSystemIdentifier>;
83
+ abstract getSystemIdentifier(): Promise<BucketStorageSystemIdentifier>;
84
+ abstract [Symbol.asyncDispose](): PromiseLike<void>;
83
85
  }
84
86
  export interface BucketStorageFactoryListener {
85
87
  syncStorageCreated: (storage: SyncRulesBucketStorage) => void;
@@ -102,10 +104,36 @@ export interface StorageMetrics {
102
104
  replication_size_bytes: number;
103
105
  }
104
106
  export interface UpdateSyncRulesOptions {
105
- content: string;
107
+ config: {
108
+ yaml: string;
109
+ /**
110
+ * The serialized sync plan for the sync configuration, or `null` for configurations not using the sync stream
111
+ * compiler.
112
+ */
113
+ plan: SerializedSyncPlan | null;
114
+ };
106
115
  lock?: boolean;
107
- validate?: boolean;
116
+ storageVersion?: number;
117
+ }
118
+ export interface SerializedSyncPlan {
119
+ /**
120
+ * The serialized plan, from {@link serializeSyncPlan}.
121
+ */
122
+ plan: unknown;
123
+ compatibility: SerializedCompatibilityContext;
124
+ /**
125
+ * Event descriptors are not currently represented in the sync plan because they don't use the sync streams compiler
126
+ * yet.
127
+ *
128
+ * We might revisit that in the future, but for now we store SQL text of their definitions here to be able to restore
129
+ * them.
130
+ */
131
+ eventDescriptors: Record<string, string[]>;
108
132
  }
133
+ export declare function updateSyncRulesFromYaml(content: string, options?: Omit<UpdateSyncRulesOptions, 'config'> & {
134
+ validate?: boolean;
135
+ }): UpdateSyncRulesOptions;
136
+ export declare function updateSyncRulesFromConfig(parsed: SyncConfig, options?: Omit<UpdateSyncRulesOptions, 'config'>): UpdateSyncRulesOptions;
109
137
  export interface GetIntanceOptions {
110
138
  /**
111
139
  * Set to true to skip trigger any events for creating the instance.
@@ -1,2 +1,71 @@
1
- export {};
1
+ import { BaseObserver, logger } from '@powersync/lib-services-framework';
2
+ import { PrecompiledSyncConfig, serializeSyncPlan, SqlSyncRules } from '@powersync/service-sync-rules';
3
+ /**
4
+ * Represents a configured storage provider.
5
+ *
6
+ * The provider can handle multiple copies of sync rules concurrently, each with their own storage.
7
+ * This is to handle replication of a new version of sync rules, while the old version is still active.
8
+ *
9
+ * Storage APIs for a specific copy of sync rules are provided by the `SyncRulesBucketStorage` instances.
10
+ */
11
+ export class BucketStorageFactory extends BaseObserver {
12
+ /**
13
+ * Update sync rules from configuration, if changed.
14
+ */
15
+ async configureSyncRules(options) {
16
+ const next = await this.getNextSyncRulesContent();
17
+ const active = await this.getActiveSyncRulesContent();
18
+ if (next?.sync_rules_content == options.config.yaml) {
19
+ logger.info('Sync rules from configuration unchanged');
20
+ return { updated: false };
21
+ }
22
+ else if (next == null && active?.sync_rules_content == options.config.yaml) {
23
+ logger.info('Sync rules from configuration unchanged');
24
+ return { updated: false };
25
+ }
26
+ else {
27
+ logger.info('Sync rules updated from configuration');
28
+ const persisted_sync_rules = await this.updateSyncRules(options);
29
+ return { updated: true, persisted_sync_rules, lock: persisted_sync_rules.current_lock ?? undefined };
30
+ }
31
+ }
32
+ /**
33
+ * Get the sync rules used for querying.
34
+ */
35
+ async getActiveSyncRules(options) {
36
+ const content = await this.getActiveSyncRulesContent();
37
+ return content?.parsed(options) ?? null;
38
+ }
39
+ /**
40
+ * Get the sync rules that will be active next once done with initial replicatino.
41
+ */
42
+ async getNextSyncRules(options) {
43
+ const content = await this.getNextSyncRulesContent();
44
+ return content?.parsed(options) ?? null;
45
+ }
46
+ }
47
+ export function updateSyncRulesFromYaml(content, options) {
48
+ const { config } = SqlSyncRules.fromYaml(content, {
49
+ // No schema-based validation at this point
50
+ schema: undefined,
51
+ defaultSchema: 'not_applicable', // Not needed for validation
52
+ throwOnError: options?.validate ?? false
53
+ });
54
+ return updateSyncRulesFromConfig(config, options);
55
+ }
56
+ export function updateSyncRulesFromConfig(parsed, options) {
57
+ let plan = null;
58
+ if (parsed instanceof PrecompiledSyncConfig) {
59
+ const eventDescriptors = {};
60
+ for (const event of parsed.eventDescriptors) {
61
+ eventDescriptors[event.name] = event.sourceQueries.map((q) => q.sql);
62
+ }
63
+ plan = {
64
+ compatibility: parsed.compatibility.serialize(),
65
+ plan: serializeSyncPlan(parsed.plan),
66
+ eventDescriptors
67
+ };
68
+ }
69
+ return { config: { yaml: parsed.content, plan }, ...options };
70
+ }
2
71
  //# sourceMappingURL=BucketStorageFactory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"BucketStorageFactory.js","sourceRoot":"","sources":["../../src/storage/BucketStorageFactory.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"BucketStorageFactory.js","sourceRoot":"","sources":["../../src/storage/BucketStorageFactory.ts"],"names":[],"mappings":"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"}
@@ -1,23 +1,49 @@
1
1
  import { HydratedSyncRules, SyncConfigWithErrors } from '@powersync/service-sync-rules';
2
2
  import { ReplicationLock } from './ReplicationLock.js';
3
+ import { StorageVersionConfig } from './StorageVersionConfig.js';
4
+ import { SerializedSyncPlan, UpdateSyncRulesOptions } from './BucketStorageFactory.js';
3
5
  export interface ParseSyncRulesOptions {
4
6
  defaultSchema: string;
5
7
  }
6
- export interface PersistedSyncRulesContent {
8
+ export interface PersistedSyncRulesContentData {
7
9
  readonly id: number;
8
10
  readonly sync_rules_content: string;
11
+ readonly compiled_plan: SerializedSyncPlan | null;
9
12
  readonly slot_name: string;
10
13
  /**
11
14
  * True if this is the "active" copy of the sync rules.
12
15
  */
13
16
  readonly active: boolean;
17
+ readonly storageVersion: number;
14
18
  readonly last_checkpoint_lsn: string | null;
15
19
  readonly last_fatal_error?: string | null;
16
20
  readonly last_fatal_error_ts?: Date | null;
17
21
  readonly last_keepalive_ts?: Date | null;
18
22
  readonly last_checkpoint_ts?: Date | null;
23
+ }
24
+ export declare abstract class PersistedSyncRulesContent implements PersistedSyncRulesContentData {
25
+ readonly id: number;
26
+ readonly sync_rules_content: string;
27
+ readonly compiled_plan: SerializedSyncPlan | null;
28
+ readonly slot_name: string;
29
+ readonly active: boolean;
30
+ readonly storageVersion: number;
31
+ readonly last_checkpoint_lsn: string | null;
32
+ readonly last_fatal_error?: string | null;
33
+ readonly last_fatal_error_ts?: Date | null;
34
+ readonly last_keepalive_ts?: Date | null;
35
+ readonly last_checkpoint_ts?: Date | null;
36
+ abstract readonly current_lock: ReplicationLock | null;
37
+ constructor(data: PersistedSyncRulesContentData);
38
+ /**
39
+ * Load the storage config.
40
+ *
41
+ * This may throw if the persisted storage version is not supported.
42
+ */
43
+ getStorageConfig(): StorageVersionConfig;
19
44
  parsed(options: ParseSyncRulesOptions): PersistedSyncRules;
20
- lock(): Promise<ReplicationLock>;
45
+ asUpdateOptions(options?: Omit<UpdateSyncRulesOptions, 'config'>): UpdateSyncRulesOptions;
46
+ abstract lock(): Promise<ReplicationLock>;
21
47
  }
22
48
  export interface PersistedSyncRules {
23
49
  readonly id: number;
@@ -1,2 +1,80 @@
1
- export {};
1
+ import { CompatibilityContext, CompatibilityOption, DEFAULT_HYDRATION_STATE, deserializeSyncPlan, javaScriptExpressionEngine, PrecompiledSyncConfig, SqlEventDescriptor, SqlSyncRules, versionedHydrationState } from '@powersync/service-sync-rules';
2
+ import { STORAGE_VERSION_CONFIG } from './StorageVersionConfig.js';
3
+ import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
4
+ export class PersistedSyncRulesContent {
5
+ id;
6
+ sync_rules_content;
7
+ compiled_plan;
8
+ slot_name;
9
+ active;
10
+ storageVersion;
11
+ last_checkpoint_lsn;
12
+ last_fatal_error;
13
+ last_fatal_error_ts;
14
+ last_keepalive_ts;
15
+ last_checkpoint_ts;
16
+ constructor(data) {
17
+ Object.assign(this, data);
18
+ }
19
+ /**
20
+ * Load the storage config.
21
+ *
22
+ * This may throw if the persisted storage version is not supported.
23
+ */
24
+ getStorageConfig() {
25
+ const storageConfig = STORAGE_VERSION_CONFIG[this.storageVersion];
26
+ if (storageConfig == null) {
27
+ throw new ServiceError(ErrorCode.PSYNC_S1005, `Unsupported storage version ${this.storageVersion} for sync rules ${this.id}`);
28
+ }
29
+ return storageConfig;
30
+ }
31
+ parsed(options) {
32
+ let hydrationState;
33
+ // Do we have a compiled sync plan? If so, restore from there instead of parsing everything again.
34
+ let config;
35
+ if (this.compiled_plan != null) {
36
+ const plan = deserializeSyncPlan(this.compiled_plan.plan);
37
+ const compatibility = CompatibilityContext.deserialize(this.compiled_plan.compatibility);
38
+ const eventDefinitions = [];
39
+ for (const [name, queries] of Object.entries(this.compiled_plan.eventDescriptors)) {
40
+ const descriptor = new SqlEventDescriptor(name, compatibility);
41
+ for (const query of queries) {
42
+ descriptor.addSourceQuery(query, options);
43
+ }
44
+ eventDefinitions.push(descriptor);
45
+ }
46
+ const precompiled = new PrecompiledSyncConfig(plan, compatibility, eventDefinitions, {
47
+ defaultSchema: options.defaultSchema,
48
+ engine: javaScriptExpressionEngine(compatibility),
49
+ sourceText: this.sync_rules_content
50
+ });
51
+ config = { config: precompiled, errors: [] };
52
+ }
53
+ else {
54
+ config = SqlSyncRules.fromYaml(this.sync_rules_content, options);
55
+ }
56
+ const storageConfig = this.getStorageConfig();
57
+ if (storageConfig.versionedBuckets ||
58
+ config.config.compatibility.isEnabled(CompatibilityOption.versionedBucketIds)) {
59
+ hydrationState = versionedHydrationState(this.id);
60
+ }
61
+ else {
62
+ hydrationState = DEFAULT_HYDRATION_STATE;
63
+ }
64
+ return {
65
+ id: this.id,
66
+ slot_name: this.slot_name,
67
+ sync_rules: config,
68
+ hydratedSyncRules: () => {
69
+ return config.config.hydrate({ hydrationState });
70
+ }
71
+ };
72
+ }
73
+ asUpdateOptions(options) {
74
+ return {
75
+ config: { yaml: this.sync_rules_content, plan: this.compiled_plan },
76
+ ...options
77
+ };
78
+ }
79
+ }
2
80
  //# sourceMappingURL=PersistedSyncRulesContent.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PersistedSyncRulesContent.js","sourceRoot":"","sources":["../../src/storage/PersistedSyncRulesContent.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"PersistedSyncRulesContent.js","sourceRoot":"","sources":["../../src/storage/PersistedSyncRulesContent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export interface StorageVersionConfig {
2
+ /**
3
+ * Whether versioned bucket names are automatically enabled.
4
+ *
5
+ * If this is false, bucket names may still be versioned depending on the sync config.
6
+ */
7
+ versionedBuckets: boolean;
8
+ }
9
+ /**
10
+ * Oldest supported storage version.
11
+ */
12
+ export declare const LEGACY_STORAGE_VERSION = 1;
13
+ /**
14
+ * Default storage version for newly persisted sync rules.
15
+ */
16
+ export declare const CURRENT_STORAGE_VERSION = 2;
17
+ /**
18
+ * Shared storage-version behavior across storage implementations.
19
+ */
20
+ export declare const STORAGE_VERSION_CONFIG: Record<number, StorageVersionConfig | undefined>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Oldest supported storage version.
3
+ */
4
+ export const LEGACY_STORAGE_VERSION = 1;
5
+ /**
6
+ * Default storage version for newly persisted sync rules.
7
+ */
8
+ export const CURRENT_STORAGE_VERSION = 2;
9
+ /**
10
+ * Shared storage-version behavior across storage implementations.
11
+ */
12
+ export const STORAGE_VERSION_CONFIG = {
13
+ [LEGACY_STORAGE_VERSION]: {
14
+ versionedBuckets: false
15
+ },
16
+ [CURRENT_STORAGE_VERSION]: {
17
+ versionedBuckets: true
18
+ }
19
+ };
20
+ //# sourceMappingURL=StorageVersionConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StorageVersionConfig.js","sourceRoot":"","sources":["../../src/storage/StorageVersionConfig.ts"],"names":[],"mappings":"AASA;;GAEG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC;;GAEG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAEzC;;GAEG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAqD;IACtF,CAAC,sBAAsB,CAAC,EAAE;QACxB,gBAAgB,EAAE,KAAK;KACxB;IACD,CAAC,uBAAuB,CAAC,EAAE;QACzB,gBAAgB,EAAE,IAAI;KACvB;CACF,CAAC"}
@@ -163,7 +163,8 @@ export interface CompactOptions {
163
163
  *
164
164
  * If not specified, compacts all buckets.
165
165
  *
166
- * These can be individual bucket names, or bucket definition names.
166
+ * These must be full bucket names (e.g., "global[]", "mybucket[\"user1\"]").
167
+ * Bucket definition names (e.g., "global") are not supported.
167
168
  */
168
169
  compactBuckets?: string[];
169
170
  compactParameterData?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"SyncRulesBucketStorage.js","sourceRoot":"","sources":["../../src/storage/SyncRulesBucketStorage.ts"],"names":[],"mappings":"AA6UA,MAAM,CAAC,MAAM,yBAAyB,GAAsB;IAC1D,kBAAkB,EAAE,IAAI,GAAG,EAAU;IACrC,qBAAqB,EAAE,IAAI;IAC3B,uBAAuB,EAAE,IAAI,GAAG,EAAU;IAC1C,0BAA0B,EAAE,IAAI;CACjC,CAAC"}
1
+ {"version":3,"file":"SyncRulesBucketStorage.js","sourceRoot":"","sources":["../../src/storage/SyncRulesBucketStorage.ts"],"names":[],"mappings":"AA8UA,MAAM,CAAC,MAAM,yBAAyB,GAAsB;IAC1D,kBAAkB,EAAE,IAAI,GAAG,EAAU;IACrC,qBAAqB,EAAE,IAAI;IAC3B,uBAAuB,EAAE,IAAI,GAAG,EAAU;IAC1C,0BAA0B,EAAE,IAAI;CACjC,CAAC"}
@@ -14,3 +14,4 @@ export * from './SyncRulesBucketStorage.js';
14
14
  export * from './PersistedSyncRulesContent.js';
15
15
  export * from './ReplicationLock.js';
16
16
  export * from './ReportStorage.js';
17
+ export * from './StorageVersionConfig.js';
@@ -14,4 +14,5 @@ export * from './SyncRulesBucketStorage.js';
14
14
  export * from './PersistedSyncRulesContent.js';
15
15
  export * from './ReplicationLock.js';
16
16
  export * from './ReportStorage.js';
17
+ export * from './StorageVersionConfig.js';
17
18
  //# sourceMappingURL=storage-index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"storage-index.js","sourceRoot":"","sources":["../../src/storage/storage-index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"storage-index.js","sourceRoot":"","sources":["../../src/storage/storage-index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC"}
@@ -1,8 +1,7 @@
1
- import { BucketDescription, BucketSource, HydratedSyncRules, RequestParameters, ResolvedBucket } from '@powersync/service-sync-rules';
1
+ import { BucketDescription, BucketSource, HydratedSyncRules, QuerierError, RequestParameters, ResolvedBucket } from '@powersync/service-sync-rules';
2
2
  import * as storage from '../storage/storage-index.js';
3
3
  import * as util from '../util/util-index.js';
4
4
  import { Logger } from '@powersync/lib-services-framework';
5
- import { QuerierError } from '@powersync/service-sync-rules/src/BucketParameterQuerier.js';
6
5
  import { JwtPayload } from '../auth/JwtPayload.js';
7
6
  import { SyncContext } from './SyncContext.js';
8
7
  export interface BucketChecksumStateOptions {
@@ -80,6 +79,11 @@ export interface CheckpointUpdate {
80
79
  * If null, assume that any bucket in `buckets` may have been updated.
81
80
  */
82
81
  updatedBuckets: Set<string> | typeof INVALIDATE_ALL_BUCKETS;
82
+ /**
83
+ * Number of parameter query results per sync stream definition (before deduplication).
84
+ * Map from definition name to count.
85
+ */
86
+ parameterQueryResultsByDefinition?: Map<string, number>;
83
87
  }
84
88
  export declare class BucketParameterState {
85
89
  private readonly context;
@@ -66,7 +66,7 @@ export class BucketChecksumState {
66
66
  const userIdForLogs = this.parameterState.syncParams.userId;
67
67
  const storage = this.bucketStorage;
68
68
  const update = await this.parameterState.getCheckpointUpdate(next);
69
- const { buckets: allBuckets, updatedBuckets } = update;
69
+ const { buckets: allBuckets, updatedBuckets, parameterQueryResultsByDefinition } = update;
70
70
  /** Set of all buckets in this checkpoint. */
71
71
  const bucketDescriptionMap = new Map(allBuckets.map((b) => [b.bucket, b]));
72
72
  if (bucketDescriptionMap.size > this.context.maxBuckets) {
@@ -145,18 +145,22 @@ export class BucketChecksumState {
145
145
  };
146
146
  });
147
147
  deferredLog = () => {
148
+ const totalParamResults = computeTotalParamResults(parameterQueryResultsByDefinition);
148
149
  let message = `Updated checkpoint: ${base.checkpoint} | `;
149
150
  message += `write: ${writeCheckpoint} | `;
150
151
  message += `buckets: ${allBuckets.length} | `;
152
+ if (totalParamResults !== undefined) {
153
+ message += `param_results: ${totalParamResults} | `;
154
+ }
151
155
  message += `updated: ${limitedBuckets(diff.updatedBuckets, 20)} | `;
152
156
  message += `removed: ${limitedBuckets(diff.removedBuckets, 20)}`;
153
- this.logger.info(message, {
157
+ logCheckpoint(this.logger, message, {
154
158
  checkpoint: base.checkpoint,
155
159
  user_id: userIdForLogs,
156
160
  buckets: allBuckets.length,
157
161
  updated: diff.updatedBuckets.length,
158
162
  removed: diff.removedBuckets.length
159
- });
163
+ }, totalParamResults);
160
164
  };
161
165
  checkpointLine = {
162
166
  checkpoint_diff: {
@@ -169,9 +173,18 @@ export class BucketChecksumState {
169
173
  }
170
174
  else {
171
175
  deferredLog = () => {
176
+ const totalParamResults = computeTotalParamResults(parameterQueryResultsByDefinition);
172
177
  let message = `New checkpoint: ${base.checkpoint} | write: ${writeCheckpoint} | `;
173
- message += `buckets: ${allBuckets.length} ${limitedBuckets(allBuckets, 20)}`;
174
- this.logger.info(message, { checkpoint: base.checkpoint, user_id: userIdForLogs, buckets: allBuckets.length });
178
+ message += `buckets: ${allBuckets.length}`;
179
+ if (totalParamResults !== undefined) {
180
+ message += ` | param_results: ${totalParamResults}`;
181
+ }
182
+ message += ` ${limitedBuckets(allBuckets, 20)}`;
183
+ logCheckpoint(this.logger, message, {
184
+ checkpoint: base.checkpoint,
185
+ user_id: userIdForLogs,
186
+ buckets: allBuckets.length
187
+ }, totalParamResults);
175
188
  };
176
189
  bucketsToFetch = allBuckets.map((b) => ({ bucket: b.bucket, priority: b.priority }));
177
190
  const subscriptions = [];
@@ -388,11 +401,18 @@ export class BucketParameterState {
388
401
  // TODO: Limit number of results even before we get to this point
389
402
  // This limit applies _before_ we get the unique set
390
403
  const error = new ServiceError(ErrorCode.PSYNC_S2305, `Too many parameter query results: ${update.buckets.length} (limit of ${this.context.maxParameterQueryResults})`);
391
- this.logger.error(error.message, {
404
+ let errorMessage = error.message;
405
+ const logData = {
392
406
  checkpoint: checkpoint,
393
407
  user_id: this.syncParams.userId,
394
- buckets: update.buckets.length
395
- });
408
+ parameter_query_results: update.buckets.length
409
+ };
410
+ if (update.parameterQueryResultsByDefinition && update.parameterQueryResultsByDefinition.size > 0) {
411
+ const breakdown = formatParameterQueryBreakdown(update.parameterQueryResultsByDefinition);
412
+ errorMessage += breakdown.message;
413
+ logData.parameter_query_results_by_definition = breakdown.countsByDefinition;
414
+ }
415
+ this.logger.error(errorMessage, logData);
396
416
  throw error;
397
417
  }
398
418
  return update;
@@ -440,6 +460,7 @@ export class BucketParameterState {
440
460
  }
441
461
  }
442
462
  let dynamicBuckets;
463
+ let parameterQueryResultsByDefinition;
443
464
  if (hasParameterChange || this.cachedDynamicBuckets == null || this.cachedDynamicBucketSet == null) {
444
465
  const recordedLookups = new Set();
445
466
  dynamicBuckets = await querier.queryDynamicBucketDescriptions({
@@ -450,6 +471,12 @@ export class BucketParameterState {
450
471
  return checkpoint.base.getParameterSets(lookups);
451
472
  }
452
473
  });
474
+ // Count parameter query results per definition (before deduplication)
475
+ parameterQueryResultsByDefinition = new Map();
476
+ for (const bucket of dynamicBuckets) {
477
+ const count = parameterQueryResultsByDefinition.get(bucket.definition) ?? 0;
478
+ parameterQueryResultsByDefinition.set(bucket.definition, count + 1);
479
+ }
453
480
  this.cachedDynamicBuckets = dynamicBuckets;
454
481
  this.cachedDynamicBucketSet = new Set(dynamicBuckets.map((b) => b.bucket));
455
482
  this.lookupsFromPreviousCheckpoint = recordedLookups;
@@ -471,17 +498,65 @@ export class BucketParameterState {
471
498
  return {
472
499
  buckets: allBuckets,
473
500
  // We cannot track individual bucket updates for dynamic lookups yet
474
- updatedBuckets: INVALIDATE_ALL_BUCKETS
501
+ updatedBuckets: INVALIDATE_ALL_BUCKETS,
502
+ parameterQueryResultsByDefinition
475
503
  };
476
504
  }
477
505
  else {
478
506
  return {
479
507
  buckets: allBuckets,
480
- updatedBuckets: updatedBuckets
508
+ updatedBuckets: updatedBuckets,
509
+ parameterQueryResultsByDefinition
481
510
  };
482
511
  }
483
512
  }
484
513
  }
514
+ /**
515
+ * Compute the total number of parameter query results across all definitions.
516
+ */
517
+ function computeTotalParamResults(parameterQueryResultsByDefinition) {
518
+ if (!parameterQueryResultsByDefinition) {
519
+ return undefined;
520
+ }
521
+ return Array.from(parameterQueryResultsByDefinition.values()).reduce((sum, count) => sum + count, 0);
522
+ }
523
+ /**
524
+ * Log a checkpoint message, enriching it with parameter query result counts if available.
525
+ *
526
+ * @param logger The logger instance to use
527
+ * @param message The base message string (param_results will NOT be appended — caller includes it if needed)
528
+ * @param logData The base log data object
529
+ * @param totalParamResults The total parameter query results count, or undefined if not applicable
530
+ */
531
+ function logCheckpoint(logger, message, logData, totalParamResults) {
532
+ if (totalParamResults !== undefined) {
533
+ logData.parameter_query_results = totalParamResults;
534
+ }
535
+ logger.info(message, logData);
536
+ }
537
+ /**
538
+ * Format a breakdown of parameter query results by sync rule definition.
539
+ *
540
+ * Sorts definitions by count (descending), includes the top 10, and returns both the
541
+ * formatted message string and the counts record suitable for structured log data.
542
+ */
543
+ function formatParameterQueryBreakdown(parameterQueryResultsByDefinition) {
544
+ // Sort definitions by count (descending) and take top 10
545
+ const allSorted = Array.from(parameterQueryResultsByDefinition.entries()).sort((a, b) => b[1] - a[1]);
546
+ const sortedDefinitions = allSorted.slice(0, 10);
547
+ let message = '\nParameter query results by definition:';
548
+ const countsByDefinition = {};
549
+ for (const [definition, count] of sortedDefinitions) {
550
+ message += `\n ${definition}: ${count}`;
551
+ countsByDefinition[definition] = count;
552
+ }
553
+ if (allSorted.length > 10) {
554
+ const remainingResults = allSorted.slice(10).reduce((sum, [, count]) => sum + count, 0);
555
+ const remainingDefinitions = allSorted.length - 10;
556
+ message += `\n ... and ${remainingResults} more results from ${remainingDefinitions} definitions`;
557
+ }
558
+ return { message, countsByDefinition };
559
+ }
485
560
  function limitedBuckets(buckets, limit) {
486
561
  buckets = buckets.map((b) => {
487
562
  if (typeof b != 'string') {