@powersync/service-module-mongodb-storage 0.16.0 → 0.17.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 (102) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +6 -4
  3. package/dist/storage/MongoBucketStorage.js +110 -36
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +4 -6
  6. package/dist/storage/implementation/BucketDefinitionMapping.js +3 -3
  7. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -1
  8. package/dist/storage/implementation/CheckpointState.d.ts +20 -0
  9. package/dist/storage/implementation/CheckpointState.js +31 -0
  10. package/dist/storage/implementation/CheckpointState.js.map +1 -0
  11. package/dist/storage/implementation/MongoBucketBatch.d.ts +33 -22
  12. package/dist/storage/implementation/MongoBucketBatch.js +45 -271
  13. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  14. package/dist/storage/implementation/MongoChecksums.d.ts +2 -1
  15. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  16. package/dist/storage/implementation/MongoCompactor.d.ts +1 -1
  17. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +4 -4
  18. package/dist/storage/implementation/MongoPersistedSyncRules.js +11 -8
  19. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -1
  20. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +19 -5
  21. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +53 -19
  22. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  23. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +21 -10
  24. package/dist/storage/implementation/MongoSyncBucketStorage.js +18 -163
  25. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  26. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -1
  27. package/dist/storage/implementation/MongoSyncRulesLock.js +7 -3
  28. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  29. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
  30. package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
  31. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
  32. package/dist/storage/implementation/common/BucketDataDoc.d.ts +1 -1
  33. package/dist/storage/implementation/common/PersistedBatch.d.ts +2 -2
  34. package/dist/storage/implementation/common/SourceRecordStore.d.ts +1 -2
  35. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +1 -1
  36. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
  37. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
  38. package/dist/storage/implementation/db.d.ts +10 -2
  39. package/dist/storage/implementation/db.js.map +1 -1
  40. package/dist/storage/implementation/models.d.ts +31 -47
  41. package/dist/storage/implementation/models.js.map +1 -1
  42. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +15 -1
  43. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +385 -0
  44. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  45. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +1 -1
  46. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +16 -7
  47. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +77 -6
  48. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  49. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +1 -2
  50. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  51. package/dist/storage/implementation/v1/models.d.ts +12 -1
  52. package/dist/storage/implementation/v1/models.js.map +1 -1
  53. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +17 -0
  54. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +429 -0
  55. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  56. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +1 -1
  57. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +1 -2
  58. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -1
  59. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +29 -7
  60. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +117 -16
  61. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  62. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +1 -2
  63. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  64. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +3 -2
  65. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
  66. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  67. package/dist/storage/implementation/v3/models.d.ts +61 -3
  68. package/dist/storage/implementation/v3/models.js.map +1 -1
  69. package/package.json +6 -6
  70. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +1 -1
  71. package/src/storage/MongoBucketStorage.ts +166 -44
  72. package/src/storage/implementation/BucketDefinitionMapping.ts +12 -9
  73. package/src/storage/implementation/CheckpointState.ts +59 -0
  74. package/src/storage/implementation/MongoBucketBatch.ts +81 -355
  75. package/src/storage/implementation/MongoChecksums.ts +2 -1
  76. package/src/storage/implementation/MongoCompactor.ts +1 -1
  77. package/src/storage/implementation/MongoPersistedSyncRules.ts +13 -7
  78. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +69 -24
  79. package/src/storage/implementation/MongoSyncBucketStorage.ts +40 -215
  80. package/src/storage/implementation/MongoSyncRulesLock.ts +9 -3
  81. package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
  82. package/src/storage/implementation/common/BucketDataDoc.ts +1 -1
  83. package/src/storage/implementation/common/PersistedBatch.ts +2 -2
  84. package/src/storage/implementation/common/SourceRecordStore.ts +1 -2
  85. package/src/storage/implementation/createMongoSyncBucketStorage.ts +2 -2
  86. package/src/storage/implementation/db.ts +5 -2
  87. package/src/storage/implementation/models.ts +35 -58
  88. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +478 -1
  89. package/src/storage/implementation/v1/MongoCompactorV1.ts +1 -1
  90. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +111 -16
  91. package/src/storage/implementation/v1/PersistedBatchV1.ts +1 -2
  92. package/src/storage/implementation/v1/models.ts +15 -0
  93. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +564 -1
  94. package/src/storage/implementation/v3/MongoCompactorV3.ts +1 -1
  95. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +1 -2
  96. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +150 -22
  97. package/src/storage/implementation/v3/PersistedBatchV3.ts +1 -2
  98. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +7 -2
  99. package/src/storage/implementation/v3/models.ts +70 -2
  100. package/test/src/storage_sync.test.ts +422 -6
  101. package/test/src/storeCurrentData.test.ts +211 -0
  102. package/tsconfig.tsbuildinfo +1 -1
@@ -1,3 +1,5 @@
1
+ import * as sqlite from 'node:sqlite';
2
+
1
3
  import { ServiceAssertionError } from '@powersync/lib-services-framework';
2
4
  import { storage } from '@powersync/service-core';
3
5
  import {
@@ -5,9 +7,11 @@ import {
5
7
  BucketDataSource,
6
8
  CompatibilityOption,
7
9
  DEFAULT_HYDRATION_STATE,
8
- HydratedSyncRules,
10
+ HydratedSyncConfig,
9
11
  HydrationState,
12
+ nodeSqlite,
10
13
  ParameterIndexLookupCreator,
14
+ ParameterLookupScope,
11
15
  SyncConfigWithErrors,
12
16
  versionedHydrationState
13
17
  } from '@powersync/service-sync-rules';
@@ -19,7 +23,7 @@ export class MongoPersistedSyncRules implements storage.PersistedSyncRules {
19
23
 
20
24
  constructor(
21
25
  public readonly id: number,
22
- public readonly sync_rules: SyncConfigWithErrors,
26
+ public readonly syncConfigWithErrors: SyncConfigWithErrors,
23
27
  public readonly slot_name: string,
24
28
  private readonly mapping: BucketDefinitionMapping | null,
25
29
  private readonly storageConfig: StorageConfig
@@ -30,7 +34,7 @@ export class MongoPersistedSyncRules implements storage.PersistedSyncRules {
30
34
  }
31
35
  this.hydrationState = new MongoHydrationState(this.mapping, this.id);
32
36
  } else if (
33
- !this.sync_rules.config.compatibility.isEnabled(CompatibilityOption.versionedBucketIds) &&
37
+ !this.syncConfigWithErrors.config.compatibility.isEnabled(CompatibilityOption.versionedBucketIds) &&
34
38
  !this.storageConfig.versionedBuckets
35
39
  ) {
36
40
  this.hydrationState = DEFAULT_HYDRATION_STATE;
@@ -39,8 +43,11 @@ export class MongoPersistedSyncRules implements storage.PersistedSyncRules {
39
43
  }
40
44
  }
41
45
 
42
- hydratedSyncRules(): HydratedSyncRules {
43
- return this.sync_rules.config.hydrate({ hydrationState: this.hydrationState });
46
+ hydratedSyncConfig(): HydratedSyncConfig {
47
+ return this.syncConfigWithErrors.config.hydrate({
48
+ hydrationState: this.hydrationState,
49
+ sqlite: nodeSqlite(sqlite)
50
+ });
44
51
  }
45
52
  }
46
53
 
@@ -54,7 +61,6 @@ class MongoHydrationState implements HydrationState {
54
61
  // Keep this aligned with versionedHydrationState() for now.
55
62
  //
56
63
  // Previous Mongo-specific behavior:
57
- // const defId = this.mapping.bucketSourceId(source);
58
64
  // return {
59
65
  // bucketPrefix: defId,
60
66
  // source
@@ -65,7 +71,7 @@ class MongoHydrationState implements HydrationState {
65
71
  };
66
72
  }
67
73
 
68
- getParameterIndexLookupScope(source: ParameterIndexLookupCreator) {
74
+ getParameterIndexLookupScope(source: ParameterIndexLookupCreator): ParameterLookupScope {
69
75
  const defId = this.mapping.parameterLookupId(source);
70
76
  return {
71
77
  lookupName: defId,
@@ -1,34 +1,31 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { storage } from '@powersync/service-core';
2
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
3
+ import { storage, SyncRuleState } from '@powersync/service-core';
4
+ import * as bson from 'bson';
5
+ import { ReplicationStreamDocumentV3, SyncConfigDefinition } from '../storage-index.js';
3
6
  import { BucketDefinitionMapping } from './BucketDefinitionMapping.js';
4
7
  import { MongoPersistedSyncRules } from './MongoPersistedSyncRules.js';
5
8
  import { MongoSyncRulesLock } from './MongoSyncRulesLock.js';
6
9
  import { PowerSyncMongo } from './db.js';
7
- import { getMongoStorageConfig, SyncRuleDocument } from './models.js';
10
+ import { getMongoStorageConfig } from './models.js';
11
+ import { SyncRuleDocumentV1 } from './v1/models.js';
8
12
 
9
- export class MongoPersistedSyncRulesContent extends storage.PersistedSyncRulesContent {
13
+ abstract class MongoPersistedSyncRulesContentBase extends storage.PersistedSyncRulesContent {
10
14
  public current_lock: MongoSyncRulesLock | null = null;
11
15
  public readonly mapping: BucketDefinitionMapping;
16
+ public readonly syncConfigId: bson.ObjectId | null;
12
17
 
13
- constructor(
14
- private db: PowerSyncMongo,
15
- doc: mongo.WithId<SyncRuleDocument>
18
+ protected constructor(
19
+ protected readonly db: PowerSyncMongo,
20
+ options: ConstructorParameters<typeof storage.PersistedSyncRulesContent>[0] & {
21
+ mapping: BucketDefinitionMapping;
22
+ syncConfigId: bson.ObjectId | null;
23
+ }
16
24
  ) {
17
- super({
18
- id: doc._id,
19
- sync_rules_content: doc.content,
20
- compiled_plan: doc.serialized_plan ?? null,
21
- last_checkpoint_lsn: doc.last_checkpoint_lsn,
22
- // Handle legacy values
23
- slot_name: doc.slot_name ?? `powersync_${doc._id}`,
24
- last_fatal_error: doc.last_fatal_error,
25
- last_fatal_error_ts: doc.last_fatal_error_ts,
26
- last_checkpoint_ts: doc.last_checkpoint_ts,
27
- last_keepalive_ts: doc.last_keepalive_ts,
28
- active: doc.state == 'ACTIVE',
29
- storageVersion: doc.storage_version ?? storage.LEGACY_STORAGE_VERSION
30
- });
31
- this.mapping = BucketDefinitionMapping.fromSyncRules(doc);
25
+ const { mapping, syncConfigId, ...base } = options;
26
+ super(base);
27
+ this.mapping = mapping;
28
+ this.syncConfigId = syncConfigId;
32
29
  }
33
30
 
34
31
  getStorageConfig() {
@@ -41,16 +38,64 @@ export class MongoPersistedSyncRulesContent extends storage.PersistedSyncRulesCo
41
38
 
42
39
  return new MongoPersistedSyncRules(
43
40
  parsed.id,
44
- parsed.sync_rules,
41
+ parsed.syncConfigWithErrors,
45
42
  parsed.slot_name,
46
43
  storageConfig.incrementalReprocessing ? this.mapping : null,
47
44
  storageConfig
48
45
  );
49
46
  }
50
47
 
51
- async lock() {
52
- const lock = await MongoSyncRulesLock.createLock(this.db.versioned(this.getStorageConfig()), this);
48
+ async lock(session?: mongo.ClientSession) {
49
+ const lock = await MongoSyncRulesLock.createLock(this.db.versioned(this.getStorageConfig()), this, session);
53
50
  this.current_lock = lock;
54
51
  return lock;
55
52
  }
56
53
  }
54
+
55
+ export class MongoPersistedSyncRulesContentV1 extends MongoPersistedSyncRulesContentBase {
56
+ constructor(db: PowerSyncMongo, doc: SyncRuleDocumentV1) {
57
+ super(db, {
58
+ id: doc._id,
59
+ sync_rules_content: doc.content,
60
+ compiled_plan: doc.serialized_plan ?? null,
61
+ last_checkpoint_lsn: doc.last_checkpoint_lsn,
62
+ // Handle legacy values
63
+ slot_name: doc.slot_name ?? `powersync_${doc._id}`,
64
+ last_fatal_error: doc.last_fatal_error,
65
+ last_fatal_error_ts: doc.last_fatal_error_ts,
66
+ last_checkpoint_ts: doc.last_checkpoint_ts,
67
+ last_keepalive_ts: doc.last_keepalive_ts,
68
+ active: doc.state == SyncRuleState.ACTIVE,
69
+ storageVersion: doc.storage_version ?? storage.LEGACY_STORAGE_VERSION,
70
+ mapping: new BucketDefinitionMapping(),
71
+ syncConfigId: null
72
+ });
73
+ }
74
+ }
75
+
76
+ export class MongoPersistedSyncRulesContentV3 extends MongoPersistedSyncRulesContentBase {
77
+ declare public readonly syncConfigId: bson.ObjectId;
78
+
79
+ constructor(db: PowerSyncMongo, doc: ReplicationStreamDocumentV3, config: SyncConfigDefinition) {
80
+ const state = doc.sync_configs.find((c) => c._id.equals(config._id));
81
+ if (state == null) {
82
+ throw new ServiceAssertionError(`Cannot find sync config ${config._id} in replication stream ${doc._id}`);
83
+ }
84
+ super(db, {
85
+ id: doc._id,
86
+ sync_rules_content: config.content,
87
+ compiled_plan: config.serialized_plan ?? null,
88
+
89
+ last_checkpoint_lsn: state?.last_checkpoint_lsn ?? null,
90
+ slot_name: doc.slot_name ?? `powersync_${doc._id}`,
91
+ last_fatal_error: doc.last_fatal_error,
92
+ last_fatal_error_ts: doc.last_fatal_error_ts,
93
+ last_checkpoint_ts: doc.last_checkpoint_ts,
94
+ last_keepalive_ts: doc.last_keepalive_ts,
95
+ active: doc.state == SyncRuleState.ACTIVE && state.state == SyncRuleState.ACTIVE,
96
+ storageVersion: doc.storage_version,
97
+ mapping: BucketDefinitionMapping.fromSyncConfig(config),
98
+ syncConfigId: config._id
99
+ });
100
+ }
101
+ }
@@ -13,7 +13,6 @@ import {
13
13
  CheckpointChanges,
14
14
  GetCheckpointChangesOptions,
15
15
  InternalOpId,
16
- maxLsn,
17
16
  mergeAsyncIterables,
18
17
  PopulateChecksumCacheOptions,
19
18
  PopulateChecksumCacheResults,
@@ -22,7 +21,7 @@ import {
22
21
  utils,
23
22
  WatchWriteCheckpointOptions
24
23
  } from '@powersync/service-core';
25
- import { HydratedSyncRules, ParameterLookupRows, ScopedParameterLookup } from '@powersync/service-sync-rules';
24
+ import { HydratedSyncConfig, ParameterLookupRows, ScopedParameterLookup } from '@powersync/service-sync-rules';
26
25
  import * as bson from 'bson';
27
26
  import { LRUCache } from 'lru-cache';
28
27
  import * as timers from 'timers/promises';
@@ -30,12 +29,12 @@ import { retryOnMongoMaxTimeMSExpired } from '../../utils/util.js';
30
29
  import { MongoBucketStorage } from '../MongoBucketStorage.js';
31
30
  import { MongoSyncBucketStorageContext } from './common/MongoSyncBucketStorageContext.js';
32
31
  import type { VersionedPowerSyncMongo } from './db.js';
33
- import { CommonSourceTableDocument, StorageConfig } from './models.js';
32
+ import { StorageConfig } from './models.js';
34
33
  import { MongoBucketBatchOptions } from './MongoBucketBatch.js';
35
34
  import { MongoChecksumOptions, MongoChecksums } from './MongoChecksums.js';
36
35
  import { MongoCompactOptions, MongoCompactor } from './MongoCompactor.js';
37
36
  import { MongoParameterCompactor } from './MongoParameterCompactor.js';
38
- import { MongoPersistedSyncRulesContent } from './MongoPersistedSyncRulesContent.js';
37
+ import { MongoPersistedSyncRulesContentV1 } from './MongoPersistedSyncRulesContent.js';
39
38
  import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
40
39
 
41
40
  export interface MongoSyncBucketStorageOptions {
@@ -48,6 +47,13 @@ interface InternalCheckpointChanges extends CheckpointChanges {
48
47
  invalidateWriteCheckpoints: boolean;
49
48
  }
50
49
 
50
+ interface WriterSyncState {
51
+ lastCheckpointLsn: string | null;
52
+ resumeFromLsn: string | null;
53
+ keepaliveOp: InternalOpId | null;
54
+ syncConfigId?: bson.ObjectId | null;
55
+ }
56
+
51
57
  /**
52
58
  * Only keep checkpoints around for a minute, before fetching a fresh one.
53
59
  *
@@ -68,21 +74,23 @@ export abstract class MongoSyncBucketStorage
68
74
 
69
75
  readonly checksums: MongoChecksums;
70
76
 
71
- private parsedSyncRulesCache: { parsed: HydratedSyncRules; options: storage.ParseSyncRulesOptions } | undefined;
77
+ private parsedSyncRulesCache: { parsed: HydratedSyncConfig; options: storage.ParseSyncRulesOptions } | undefined;
72
78
  private writeCheckpointAPI: MongoWriteCheckpointAPI;
73
79
  public readonly logger: Logger;
80
+ public readonly storageConfig: StorageConfig;
74
81
  #storageInitialized = false;
75
82
 
76
83
  constructor(
77
84
  public readonly factory: MongoBucketStorage,
78
85
  public readonly group_id: number,
79
- protected readonly sync_rules: MongoPersistedSyncRulesContent,
86
+ protected readonly sync_rules: MongoPersistedSyncRulesContentV1,
80
87
  public readonly slot_name: string,
81
88
  writeCheckpointMode: storage.WriteCheckpointMode | undefined,
82
89
  options: MongoSyncBucketStorageOptions
83
90
  ) {
84
91
  super();
85
- this.db = factory.db.versioned(sync_rules.getStorageConfig());
92
+ this.storageConfig = options.storageConfig;
93
+ this.db = factory.db.versioned(this.storageConfig);
86
94
  this.checksums = this.createMongoChecksums(options);
87
95
  this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
88
96
  db: this.db,
@@ -136,10 +144,10 @@ export abstract class MongoSyncBucketStorage
136
144
  });
137
145
  }
138
146
 
139
- getParsedSyncRules(options: storage.ParseSyncRulesOptions): HydratedSyncRules {
147
+ getParsedSyncRules(options: storage.ParseSyncRulesOptions): HydratedSyncConfig {
140
148
  const { parsed, options: cachedOptions } = this.parsedSyncRulesCache ?? {};
141
149
  if (!parsed || options.defaultSchema != cachedOptions?.defaultSchema) {
142
- this.parsedSyncRulesCache = { parsed: this.sync_rules.parsed(options).hydratedSyncRules(), options };
150
+ this.parsedSyncRulesCache = { parsed: this.sync_rules.parsed(options).hydratedSyncConfig(), options };
143
151
  }
144
152
 
145
153
  return this.parsedSyncRulesCache!.parsed;
@@ -149,16 +157,14 @@ export abstract class MongoSyncBucketStorage
149
157
  return (await this.getCheckpointInternal()) ?? new EmptyReplicationCheckpoint();
150
158
  }
151
159
 
160
+ protected abstract fetchCheckpointState(
161
+ session: mongo.ClientSession
162
+ ): Promise<{ checkpoint: bigint; lsn: string | null } | null>;
163
+
152
164
  async getCheckpointInternal(): Promise<storage.ReplicationCheckpoint | null> {
153
165
  return await this.db.client.withSession({ snapshot: true }, async (session) => {
154
- const doc = await this.db.sync_rules.findOne(
155
- { _id: this.group_id },
156
- {
157
- session,
158
- projection: { _id: 1, state: 1, last_checkpoint: 1, last_checkpoint_lsn: 1, snapshot_done: 1 }
159
- }
160
- );
161
- if (!doc?.snapshot_done || !['ACTIVE', 'ERRORED'].includes(doc.state)) {
166
+ const state = await this.fetchCheckpointState(session);
167
+ if (state == null) {
162
168
  return null;
163
169
  }
164
170
 
@@ -166,12 +172,7 @@ export abstract class MongoSyncBucketStorage
166
172
  if (snapshotTime == null) {
167
173
  throw new ServiceAssertionError('Missing snapshotTime in getCheckpoint()');
168
174
  }
169
- return new MongoReplicationCheckpoint(
170
- this,
171
- doc.last_checkpoint ?? 0n,
172
- doc.last_checkpoint_lsn ?? null,
173
- snapshotTime
174
- );
175
+ return new MongoReplicationCheckpoint(this, state.checkpoint, state.lsn, snapshotTime);
175
176
  });
176
177
  }
177
178
 
@@ -188,31 +189,28 @@ export abstract class MongoSyncBucketStorage
188
189
  }
189
190
 
190
191
  protected abstract createWriterImpl(batchOptions: MongoBucketBatchOptions): storage.BucketStorageBatch;
192
+ protected abstract getWriterSyncState(): Promise<WriterSyncState>;
191
193
 
192
194
  async createWriter(options: storage.CreateWriterOptions): Promise<storage.BucketStorageBatch> {
193
195
  await this.initializeStorage();
194
196
 
195
- const doc = await this.db.sync_rules.findOne(
196
- {
197
- _id: this.group_id
198
- },
199
- { projection: { last_checkpoint_lsn: 1, no_checkpoint_before: 1, keepalive_op: 1, snapshot_lsn: 1 } }
200
- );
201
- const checkpoint_lsn = doc?.last_checkpoint_lsn ?? null;
197
+ const state = await this.getWriterSyncState();
202
198
 
203
199
  const batchOptions: MongoBucketBatchOptions = {
204
200
  logger: options.logger ?? this.logger,
205
201
  db: this.db,
206
- syncRules: this.sync_rules.parsed(options).hydratedSyncRules(),
202
+ syncRules: this.sync_rules.parsed(options).hydratedSyncConfig(),
207
203
  mapping: this.sync_rules.mapping,
208
204
  groupId: this.group_id,
209
205
  slotName: this.slot_name,
210
- lastCheckpointLsn: checkpoint_lsn,
211
- resumeFromLsn: maxLsn(checkpoint_lsn, doc?.snapshot_lsn),
212
- keepaliveOp: doc?.keepalive_op ? BigInt(doc.keepalive_op) : null,
206
+ lastCheckpointLsn: state.lastCheckpointLsn,
207
+ resumeFromLsn: state.resumeFromLsn,
208
+ keepaliveOp: state.keepaliveOp,
213
209
  storeCurrentData: options.storeCurrentData,
214
210
  skipExistingRows: options.skipExistingRows ?? false,
215
211
  markRecordUnavailable: options.markRecordUnavailable,
212
+ hooks: options.hooks,
213
+ syncConfigId: state.syncConfigId,
216
214
  tracer: options.tracer
217
215
  };
218
216
  const writer = this.createWriterImpl(batchOptions);
@@ -230,134 +228,6 @@ export abstract class MongoSyncBucketStorage
230
228
  return writer.last_flushed_op != null ? { flushed_op: writer.last_flushed_op } : null;
231
229
  }
232
230
 
233
- protected abstract sourceTableBaseId(): Partial<CommonSourceTableDocument>;
234
-
235
- protected abstract augmentCreatedSourceTableDocument(
236
- createDoc: CommonSourceTableDocument,
237
- options: storage.ResolveTableOptions,
238
- candidateSourceTable: storage.SourceTable
239
- ): void;
240
-
241
- protected abstract initializeResolvedSourceRecords(sourceTableId: bson.ObjectId): Promise<void>;
242
-
243
- async resolveTable(options: storage.ResolveTableOptions): Promise<storage.ResolveTableResult> {
244
- const { group_id, connection_id, connection_tag, entity_descriptor } = options;
245
-
246
- const { schema, name, objectId, replicaIdColumns } = entity_descriptor;
247
-
248
- const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
249
- name: column.name,
250
- type: column.type,
251
- type_oid: column.typeId
252
- }));
253
- let result: storage.ResolveTableResult | null = null;
254
- let initializeSourceRecordsFor: bson.ObjectId | null = null;
255
-
256
- const baseId = this.sourceTableBaseId();
257
- await this.db.client.withSession(async (session) => {
258
- const col = this.db.commonSourceTables(group_id);
259
- let filter: Partial<CommonSourceTableDocument> = {
260
- ...baseId,
261
- connection_id: connection_id,
262
- schema_name: schema,
263
- table_name: name,
264
- replica_id_columns2: normalizedReplicaIdColumns
265
- };
266
-
267
- if (objectId != null) {
268
- filter.relation_id = objectId;
269
- }
270
- let doc = await col.findOne(filter, { session });
271
- if (doc == null) {
272
- const candidateSourceTable = new storage.SourceTable({
273
- id: new bson.ObjectId(),
274
- connectionTag: connection_tag,
275
- objectId: objectId,
276
- schema: schema,
277
- name: name,
278
- replicaIdColumns: replicaIdColumns,
279
- snapshotComplete: false
280
- });
281
- const createDoc: CommonSourceTableDocument = {
282
- _id: candidateSourceTable.id as bson.ObjectId,
283
- ...(baseId as any),
284
- connection_id: connection_id,
285
- relation_id: objectId,
286
- schema_name: schema,
287
- table_name: name,
288
- replica_id_columns: null,
289
- replica_id_columns2: normalizedReplicaIdColumns,
290
- snapshot_done: false,
291
- snapshot_status: undefined
292
- };
293
- this.augmentCreatedSourceTableDocument(createDoc, options, candidateSourceTable);
294
- doc = createDoc;
295
-
296
- await col.insertOne(doc, { session });
297
- initializeSourceRecordsFor = doc._id;
298
- }
299
- const sourceTable = new storage.SourceTable({
300
- id: doc._id,
301
- connectionTag: connection_tag,
302
- objectId: objectId,
303
- schema: schema,
304
- name: name,
305
- replicaIdColumns: replicaIdColumns,
306
- snapshotComplete: doc.snapshot_done ?? true
307
- });
308
- sourceTable.syncEvent = options.sync_rules.tableTriggersEvent(sourceTable);
309
- sourceTable.syncData = options.sync_rules.tableSyncsData(sourceTable);
310
- sourceTable.syncParameters = options.sync_rules.tableSyncsParameters(sourceTable);
311
- sourceTable.snapshotStatus =
312
- doc.snapshot_status == null
313
- ? undefined
314
- : {
315
- lastKey: doc.snapshot_status.last_key?.buffer ?? null,
316
- totalEstimatedCount: doc.snapshot_status.total_estimated_count,
317
- replicatedCount: doc.snapshot_status.replicated_count
318
- };
319
-
320
- let dropTables: storage.SourceTable[] = [];
321
- let truncateFilter = [{ schema_name: schema, table_name: name }] as any[];
322
- if (objectId != null) {
323
- truncateFilter.push({ relation_id: objectId });
324
- }
325
- const truncate = await col
326
- .find(
327
- {
328
- ...baseId,
329
- connection_id: connection_id,
330
- _id: { $ne: doc._id },
331
- $or: truncateFilter
332
- },
333
- { session }
334
- )
335
- .toArray();
336
- dropTables = truncate.map(
337
- (doc) =>
338
- new storage.SourceTable({
339
- id: doc._id,
340
- connectionTag: connection_tag,
341
- objectId: doc.relation_id,
342
- schema: doc.schema_name,
343
- name: doc.table_name,
344
- replicaIdColumns:
345
- doc.replica_id_columns2?.map((c) => ({ name: c.name, typeOid: c.type_oid, type: c.type })) ?? [],
346
- snapshotComplete: doc.snapshot_done ?? true
347
- })
348
- );
349
-
350
- result = {
351
- table: sourceTable,
352
- dropTables: dropTables
353
- };
354
- });
355
- if (initializeSourceRecordsFor != null) {
356
- await this.initializeResolvedSourceRecords(initializeSourceRecordsFor);
357
- }
358
- return result!;
359
- }
360
-
361
231
  protected abstract getParameterSetsImpl(
362
232
  checkpoint: MongoReplicationCheckpoint,
363
233
  lookups: ScopedParameterLookup[],
@@ -397,49 +267,20 @@ export abstract class MongoSyncBucketStorage
397
267
  this.checksums.clearCache();
398
268
  }
399
269
 
270
+ protected abstract terminateSyncRuleState(): Promise<void>;
271
+
400
272
  async terminate(options?: storage.TerminateOptions) {
401
273
  if (!options || options?.clearStorage) {
402
274
  await this.clear(options);
403
275
  }
404
- await this.db.sync_rules.updateOne(
405
- {
406
- _id: this.group_id
407
- },
408
- {
409
- $set: {
410
- state: storage.SyncRuleState.TERMINATED,
411
- persisted_lsn: null,
412
- snapshot_done: false
413
- }
414
- }
415
- );
276
+ await this.terminateSyncRuleState();
416
277
  await this.db.notifyCheckpoint();
417
278
  }
418
279
 
419
- async getStatus(): Promise<storage.SyncRuleStatus> {
420
- const doc = await this.db.sync_rules.findOne(
421
- {
422
- _id: this.group_id
423
- },
424
- {
425
- projection: {
426
- snapshot_done: 1,
427
- last_checkpoint_lsn: 1,
428
- state: 1,
429
- snapshot_lsn: 1
430
- }
431
- }
432
- );
433
- if (doc == null) {
434
- throw new ServiceAssertionError('Cannot find replication stream status');
435
- }
280
+ protected abstract getStatusImpl(): Promise<storage.SyncRuleStatus>;
436
281
 
437
- return {
438
- snapshot_done: doc.snapshot_done,
439
- snapshot_lsn: doc.snapshot_lsn ?? null,
440
- active: doc.state == 'ACTIVE',
441
- checkpoint_lsn: doc.last_checkpoint_lsn
442
- };
282
+ async getStatus(): Promise<storage.SyncRuleStatus> {
283
+ return this.getStatusImpl();
443
284
  }
444
285
 
445
286
  protected abstract clearBucketData(signal?: AbortSignal): Promise<void>;
@@ -451,6 +292,7 @@ export abstract class MongoSyncBucketStorage
451
292
  protected abstract clearBucketState(signal?: AbortSignal): Promise<void>;
452
293
 
453
294
  protected abstract clearSourceTables(signal?: AbortSignal): Promise<void>;
295
+ protected abstract clearSyncRuleState(): Promise<void>;
454
296
 
455
297
  async clear(options?: storage.ClearStorageOptions): Promise<void> {
456
298
  const signal = options?.signal;
@@ -459,24 +301,7 @@ export abstract class MongoSyncBucketStorage
459
301
  throw new ReplicationAbortedError('Aborted clearing data', signal.reason);
460
302
  }
461
303
 
462
- await this.db.sync_rules.updateOne(
463
- {
464
- _id: this.group_id
465
- },
466
- {
467
- $set: {
468
- snapshot_done: false,
469
- persisted_lsn: null,
470
- last_checkpoint_lsn: null,
471
- last_checkpoint: null,
472
- no_checkpoint_before: null
473
- },
474
- $unset: {
475
- snapshot_lsn: 1
476
- }
477
- },
478
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
479
- );
304
+ await this.clearSyncRuleState();
480
305
 
481
306
  await this.clearBucketData(signal);
482
307
  await this.clearParameterIndexes(signal);
@@ -1,5 +1,6 @@
1
1
  import crypto from 'crypto';
2
2
 
3
+ import { mongo } from '@powersync/lib-service-mongodb';
3
4
  import { ErrorCode, Logger, ServiceError } from '@powersync/lib-services-framework';
4
5
  import { storage } from '@powersync/service-core';
5
6
  import { VersionedPowerSyncMongo } from './db.js';
@@ -11,9 +12,13 @@ import { VersionedPowerSyncMongo } from './db.js';
11
12
  export class MongoSyncRulesLock implements storage.ReplicationLock {
12
13
  private readonly refreshInterval: NodeJS.Timeout;
13
14
 
15
+ /**
16
+ * @param session optional session to create the lock within another transaction
17
+ */
14
18
  static async createLock(
15
19
  db: VersionedPowerSyncMongo,
16
- sync_rules: storage.PersistedSyncRulesContent
20
+ sync_rules: storage.PersistedSyncRulesContent,
21
+ session?: mongo.ClientSession
17
22
  ): Promise<MongoSyncRulesLock> {
18
23
  const lockId = crypto.randomBytes(8).toString('hex');
19
24
  const doc = await db.sync_rules.findOneAndUpdate(
@@ -28,13 +33,14 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
28
33
  },
29
34
  {
30
35
  projection: { lock: 1 },
31
- returnDocument: 'before'
36
+ returnDocument: 'before',
37
+ session
32
38
  }
33
39
  );
34
40
 
35
41
  if (doc == null) {
36
42
  // Query the existing lock to get the expiration time (best effort - it may have been released in the meantime).
37
- const heldLock = await db.sync_rules.findOne({ _id: sync_rules.id }, { projection: { lock: 1 } });
43
+ const heldLock = await db.sync_rules.findOne({ _id: sync_rules.id }, { projection: { lock: 1 }, session });
38
44
  if (heldLock?.lock?.expires_at) {
39
45
  throw new ServiceError(
40
46
  ErrorCode.PSYNC_S1003,
@@ -0,0 +1,38 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { storage } from '@powersync/service-core';
3
+
4
+ /**
5
+ * Update pipeline to update replication stream status, covering all storage versions.
6
+ *
7
+ * Roughly equivalent to:
8
+ * $set: {
9
+ * state: state,
10
+ * 'sync_configs.$[].state': state
11
+ * }
12
+ *
13
+ * The difference is that this also handles v1 storage cases, where `sync_configs` is not present.
14
+ */
15
+ export function syncRuleStateUpdatePipeline(state: storage.SyncRuleState): mongo.Document[] {
16
+ return [
17
+ {
18
+ $set: {
19
+ state,
20
+ sync_configs: {
21
+ $cond: [
22
+ { $isArray: '$sync_configs' },
23
+ {
24
+ $map: {
25
+ input: '$sync_configs',
26
+ as: 'config',
27
+ in: {
28
+ $mergeObjects: ['$$config', { state }]
29
+ }
30
+ }
31
+ },
32
+ '$$REMOVE'
33
+ ]
34
+ }
35
+ }
36
+ }
37
+ ];
38
+ }
@@ -1,5 +1,5 @@
1
1
  import { InternalOpId } from '@powersync/service-core';
2
- import { BucketDefinitionId } from '../BucketDefinitionMapping.js';
2
+ import { BucketDefinitionId } from '@powersync/service-sync-rules';
3
3
  import { BucketDataProperties } from '../models.js';
4
4
 
5
5
  /**
@@ -1,12 +1,12 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { BucketDataSource, EvaluatedParameters, EvaluatedRow } from '@powersync/service-sync-rules';
2
+ import { BucketDataSource, BucketDefinitionId, EvaluatedParameters, EvaluatedRow } from '@powersync/service-sync-rules';
3
3
  import * as bson from 'bson';
4
4
 
5
5
  import { logger as defaultLogger, Logger } from '@powersync/lib-services-framework';
6
6
  import { InternalOpId, storage, utils } from '@powersync/service-core';
7
7
  import { JSONBig } from '@powersync/service-jsonbig';
8
8
  import { mongoTableId, replicaIdToSubkey } from '../../../utils/util.js';
9
- import { BucketDefinitionId, BucketDefinitionMapping } from '../BucketDefinitionMapping.js';
9
+ import { BucketDefinitionMapping } from '../BucketDefinitionMapping.js';
10
10
  import { currentBucketKey, MAX_ROW_SIZE } from '../MongoBucketBatchShared.js';
11
11
  import { MongoIdSequence } from '../MongoIdSequence.js';
12
12
  import type { VersionedPowerSyncMongo } from '../db.js';
@@ -1,9 +1,8 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import { Logger } from '@powersync/lib-services-framework';
3
3
  import { storage } from '@powersync/service-core';
4
- import { EvaluatedParameters, EvaluatedRow } from '@powersync/service-sync-rules';
4
+ import { BucketDefinitionId, EvaluatedParameters, EvaluatedRow, ParameterIndexId } from '@powersync/service-sync-rules';
5
5
  import * as bson from 'bson';
6
- import { BucketDefinitionId, ParameterIndexId } from '../BucketDefinitionMapping.js';
7
6
 
8
7
  export interface SourceRecordLookupEntry {
9
8
  sourceTableId: bson.ObjectId;