@powersync/service-core 0.0.0-dev-20241007120318 → 0.0.0-dev-20241015084348

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 (90) hide show
  1. package/CHANGELOG.md +10 -5
  2. package/dist/api/diagnostics.js +167 -103
  3. package/dist/api/diagnostics.js.map +1 -1
  4. package/dist/entry/commands/compact-action.js +73 -9
  5. package/dist/entry/commands/compact-action.js.map +1 -1
  6. package/dist/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.d.ts +3 -0
  7. package/dist/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.js +31 -0
  8. package/dist/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.js.map +1 -0
  9. package/dist/replication/AbstractReplicationJob.d.ts +1 -1
  10. package/dist/replication/AbstractReplicationJob.js.map +1 -1
  11. package/dist/replication/AbstractReplicator.d.ts +2 -2
  12. package/dist/replication/AbstractReplicator.js +66 -3
  13. package/dist/replication/AbstractReplicator.js.map +1 -1
  14. package/dist/replication/ReplicationEngine.js.map +1 -1
  15. package/dist/replication/replication-index.d.ts +1 -1
  16. package/dist/replication/replication-index.js +1 -1
  17. package/dist/replication/replication-index.js.map +1 -1
  18. package/dist/routes/endpoints/checkpointing.js +5 -2
  19. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  20. package/dist/runner/teardown.js +66 -4
  21. package/dist/runner/teardown.js.map +1 -1
  22. package/dist/storage/BucketStorage.d.ts +25 -7
  23. package/dist/storage/BucketStorage.js.map +1 -1
  24. package/dist/storage/MongoBucketStorage.d.ts +12 -5
  25. package/dist/storage/MongoBucketStorage.js +44 -23
  26. package/dist/storage/MongoBucketStorage.js.map +1 -1
  27. package/dist/storage/ReplicationEventPayload.d.ts +14 -0
  28. package/dist/storage/ReplicationEventPayload.js +2 -0
  29. package/dist/storage/ReplicationEventPayload.js.map +1 -0
  30. package/dist/storage/SourceTable.d.ts +8 -0
  31. package/dist/storage/SourceTable.js +9 -1
  32. package/dist/storage/SourceTable.js.map +1 -1
  33. package/dist/storage/StorageEngine.d.ts +10 -2
  34. package/dist/storage/StorageEngine.js +23 -3
  35. package/dist/storage/StorageEngine.js.map +1 -1
  36. package/dist/storage/StorageProvider.d.ts +9 -2
  37. package/dist/storage/mongo/MongoBucketBatch.d.ts +12 -4
  38. package/dist/storage/mongo/MongoBucketBatch.js +59 -21
  39. package/dist/storage/mongo/MongoBucketBatch.js.map +1 -1
  40. package/dist/storage/mongo/MongoStorageProvider.d.ts +1 -1
  41. package/dist/storage/mongo/MongoStorageProvider.js +3 -2
  42. package/dist/storage/mongo/MongoStorageProvider.js.map +1 -1
  43. package/dist/storage/mongo/MongoSyncBucketStorage.d.ts +3 -2
  44. package/dist/storage/mongo/MongoSyncBucketStorage.js +71 -10
  45. package/dist/storage/mongo/MongoSyncBucketStorage.js.map +1 -1
  46. package/dist/storage/mongo/MongoWriteCheckpointAPI.d.ts +18 -0
  47. package/dist/storage/mongo/MongoWriteCheckpointAPI.js +90 -0
  48. package/dist/storage/mongo/MongoWriteCheckpointAPI.js.map +1 -0
  49. package/dist/storage/mongo/db.d.ts +3 -2
  50. package/dist/storage/mongo/db.js +1 -0
  51. package/dist/storage/mongo/db.js.map +1 -1
  52. package/dist/storage/mongo/models.d.ts +7 -1
  53. package/dist/storage/storage-index.d.ts +2 -0
  54. package/dist/storage/storage-index.js +2 -0
  55. package/dist/storage/storage-index.js.map +1 -1
  56. package/dist/storage/write-checkpoint.d.ts +55 -0
  57. package/dist/storage/write-checkpoint.js +16 -0
  58. package/dist/storage/write-checkpoint.js.map +1 -0
  59. package/dist/util/config/compound-config-collector.js +2 -1
  60. package/dist/util/config/compound-config-collector.js.map +1 -1
  61. package/dist/util/config/types.d.ts +1 -0
  62. package/package.json +5 -5
  63. package/src/api/diagnostics.ts +6 -5
  64. package/src/entry/commands/compact-action.ts +4 -2
  65. package/src/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.ts +37 -0
  66. package/src/replication/AbstractReplicationJob.ts +2 -2
  67. package/src/replication/AbstractReplicator.ts +5 -4
  68. package/src/replication/ReplicationEngine.ts +1 -1
  69. package/src/replication/replication-index.ts +1 -1
  70. package/src/routes/endpoints/checkpointing.ts +5 -2
  71. package/src/runner/teardown.ts +3 -3
  72. package/src/storage/BucketStorage.ts +32 -9
  73. package/src/storage/MongoBucketStorage.ts +70 -29
  74. package/src/storage/ReplicationEventPayload.ts +16 -0
  75. package/src/storage/SourceTable.ts +10 -1
  76. package/src/storage/StorageEngine.ts +34 -5
  77. package/src/storage/StorageProvider.ts +10 -2
  78. package/src/storage/mongo/MongoBucketBatch.ts +82 -27
  79. package/src/storage/mongo/MongoStorageProvider.ts +4 -3
  80. package/src/storage/mongo/MongoSyncBucketStorage.ts +17 -15
  81. package/src/storage/mongo/MongoWriteCheckpointAPI.ts +136 -0
  82. package/src/storage/mongo/db.ts +4 -1
  83. package/src/storage/mongo/models.ts +8 -1
  84. package/src/storage/storage-index.ts +2 -0
  85. package/src/storage/write-checkpoint.ts +67 -0
  86. package/src/util/config/compound-config-collector.ts +2 -1
  87. package/src/util/config/types.ts +1 -0
  88. package/test/src/data_storage.test.ts +42 -10
  89. package/test/src/util.ts +1 -2
  90. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,136 @@
1
+ import * as framework from '@powersync/lib-services-framework';
2
+ import {
3
+ CustomWriteCheckpointFilters,
4
+ CustomWriteCheckpointOptions,
5
+ LastWriteCheckpointFilters,
6
+ ManagedWriteCheckpointFilters,
7
+ ManagedWriteCheckpointOptions,
8
+ WriteCheckpointAPI,
9
+ WriteCheckpointMode
10
+ } from '../write-checkpoint.js';
11
+ import { PowerSyncMongo } from './db.js';
12
+
13
+ export type MongoCheckpointAPIOptions = {
14
+ db: PowerSyncMongo;
15
+ mode: WriteCheckpointMode;
16
+ };
17
+
18
+ export class MongoWriteCheckpointAPI implements WriteCheckpointAPI {
19
+ readonly db: PowerSyncMongo;
20
+ readonly mode: WriteCheckpointMode;
21
+
22
+ constructor(options: MongoCheckpointAPIOptions) {
23
+ this.db = options.db;
24
+ this.mode = options.mode;
25
+ }
26
+
27
+ async batchCreateCustomWriteCheckpoints(checkpoints: CustomWriteCheckpointOptions[]): Promise<void> {
28
+ return batchCreateCustomWriteCheckpoints(this.db, checkpoints);
29
+ }
30
+
31
+ async createCustomWriteCheckpoint(options: CustomWriteCheckpointOptions): Promise<bigint> {
32
+ if (this.mode !== WriteCheckpointMode.CUSTOM) {
33
+ throw new framework.errors.ValidationError(
34
+ `Creating a custom Write Checkpoint when the current Write Checkpoint mode is set to "${this.mode}"`
35
+ );
36
+ }
37
+
38
+ const { checkpoint, user_id, sync_rules_id } = options;
39
+ const doc = await this.db.custom_write_checkpoints.findOneAndUpdate(
40
+ {
41
+ user_id: user_id,
42
+ sync_rules_id
43
+ },
44
+ {
45
+ $set: {
46
+ checkpoint
47
+ }
48
+ },
49
+ { upsert: true, returnDocument: 'after' }
50
+ );
51
+ return doc!.checkpoint;
52
+ }
53
+
54
+ async createManagedWriteCheckpoint(checkpoint: ManagedWriteCheckpointOptions): Promise<bigint> {
55
+ if (this.mode !== WriteCheckpointMode.MANAGED) {
56
+ throw new framework.errors.ValidationError(
57
+ `Creating a managed Write Checkpoint when the current Write Checkpoint mode is set to "${this.mode}"`
58
+ );
59
+ }
60
+
61
+ const { user_id, heads: lsns } = checkpoint;
62
+ const doc = await this.db.write_checkpoints.findOneAndUpdate(
63
+ {
64
+ user_id: user_id
65
+ },
66
+ {
67
+ $set: {
68
+ lsns
69
+ },
70
+ $inc: {
71
+ client_id: 1n
72
+ }
73
+ },
74
+ { upsert: true, returnDocument: 'after' }
75
+ );
76
+ return doc!.client_id;
77
+ }
78
+
79
+ async lastWriteCheckpoint(filters: LastWriteCheckpointFilters): Promise<bigint | null> {
80
+ switch (this.mode) {
81
+ case WriteCheckpointMode.CUSTOM:
82
+ if (false == 'sync_rules_id' in filters) {
83
+ throw new framework.errors.ValidationError(`Sync rules ID is required for custom Write Checkpoint filtering`);
84
+ }
85
+ return this.lastCustomWriteCheckpoint(filters);
86
+ case WriteCheckpointMode.MANAGED:
87
+ if (false == 'heads' in filters) {
88
+ throw new framework.errors.ValidationError(
89
+ `Replication HEAD is required for managed Write Checkpoint filtering`
90
+ );
91
+ }
92
+ return this.lastManagedWriteCheckpoint(filters);
93
+ }
94
+ }
95
+
96
+ protected async lastCustomWriteCheckpoint(filters: CustomWriteCheckpointFilters) {
97
+ const { user_id, sync_rules_id } = filters;
98
+ const lastWriteCheckpoint = await this.db.custom_write_checkpoints.findOne({
99
+ user_id,
100
+ sync_rules_id
101
+ });
102
+ return lastWriteCheckpoint?.checkpoint ?? null;
103
+ }
104
+
105
+ protected async lastManagedWriteCheckpoint(filters: ManagedWriteCheckpointFilters) {
106
+ const { user_id } = filters;
107
+ const lastWriteCheckpoint = await this.db.write_checkpoints.findOne({
108
+ user_id: user_id
109
+ });
110
+ return lastWriteCheckpoint?.client_id ?? null;
111
+ }
112
+ }
113
+
114
+ export async function batchCreateCustomWriteCheckpoints(
115
+ db: PowerSyncMongo,
116
+ checkpoints: CustomWriteCheckpointOptions[]
117
+ ): Promise<void> {
118
+ if (!checkpoints.length) {
119
+ return;
120
+ }
121
+
122
+ await db.custom_write_checkpoints.bulkWrite(
123
+ checkpoints.map((checkpointOptions) => ({
124
+ updateOne: {
125
+ filter: { user_id: checkpointOptions.user_id, sync_rules_id: checkpointOptions.sync_rules_id },
126
+ update: {
127
+ $set: {
128
+ checkpoint: checkpointOptions.checkpoint,
129
+ sync_rules_id: checkpointOptions.sync_rules_id
130
+ }
131
+ },
132
+ upsert: true
133
+ }
134
+ }))
135
+ );
136
+ }
@@ -1,11 +1,13 @@
1
1
  import * as mongo from 'mongodb';
2
2
 
3
+ import { configFile } from '@powersync/service-types';
3
4
  import * as db from '../../db/db-index.js';
4
5
  import * as locks from '../../locks/locks-index.js';
5
6
  import {
6
7
  BucketDataDocument,
7
8
  BucketParameterDocument,
8
9
  CurrentDataDocument,
10
+ CustomWriteCheckpointDocument,
9
11
  IdSequenceDocument,
10
12
  InstanceDocument,
11
13
  SourceTableDocument,
@@ -13,7 +15,6 @@ import {
13
15
  WriteCheckpointDocument
14
16
  } from './models.js';
15
17
  import { BSON_DESERIALIZE_OPTIONS } from './util.js';
16
- import { configFile } from '@powersync/service-types';
17
18
 
18
19
  export interface PowerSyncMongoOptions {
19
20
  /**
@@ -33,6 +34,7 @@ export class PowerSyncMongo {
33
34
  readonly op_id_sequence: mongo.Collection<IdSequenceDocument>;
34
35
  readonly sync_rules: mongo.Collection<SyncRuleDocument>;
35
36
  readonly source_tables: mongo.Collection<SourceTableDocument>;
37
+ readonly custom_write_checkpoints: mongo.Collection<CustomWriteCheckpointDocument>;
36
38
  readonly write_checkpoints: mongo.Collection<WriteCheckpointDocument>;
37
39
  readonly instance: mongo.Collection<InstanceDocument>;
38
40
  readonly locks: mongo.Collection<locks.Lock>;
@@ -54,6 +56,7 @@ export class PowerSyncMongo {
54
56
  this.op_id_sequence = db.collection('op_id_sequence');
55
57
  this.sync_rules = db.collection('sync_rules');
56
58
  this.source_tables = db.collection('source_tables');
59
+ this.custom_write_checkpoints = db.collection('custom_write_checkpoints');
57
60
  this.write_checkpoints = db.collection('write_checkpoints');
58
61
  this.instance = db.collection('instance');
59
62
  this.locks = this.db.collection('locks');
@@ -1,5 +1,5 @@
1
- import * as bson from 'bson';
2
1
  import { SqliteJsonValue } from '@powersync/service-sync-rules';
2
+ import * as bson from 'bson';
3
3
 
4
4
  /**
5
5
  * Replica id uniquely identifying a row on the source database.
@@ -159,6 +159,13 @@ export interface SyncRuleDocument {
159
159
  content: string;
160
160
  }
161
161
 
162
+ export interface CustomWriteCheckpointDocument {
163
+ _id: bson.ObjectId;
164
+ user_id: string;
165
+ checkpoint: bigint;
166
+ sync_rules_id: number;
167
+ }
168
+
162
169
  export interface WriteCheckpointDocument {
163
170
  _id: bson.ObjectId;
164
171
  user_id: string;
@@ -1,5 +1,6 @@
1
1
  export * from './BucketStorage.js';
2
2
  export * from './MongoBucketStorage.js';
3
+ export * from './ReplicationEventPayload.js';
3
4
  export * from './SourceEntity.js';
4
5
  export * from './SourceTable.js';
5
6
  export * from './StorageEngine.js';
@@ -17,3 +18,4 @@ export * from './mongo/OperationBatch.js';
17
18
  export * from './mongo/PersistedBatch.js';
18
19
  export * from './mongo/util.js';
19
20
  export * from './mongo/config.js';
21
+ export * from './write-checkpoint.js';
@@ -0,0 +1,67 @@
1
+ export enum WriteCheckpointMode {
2
+ /**
3
+ * Raw mappings of `user_id` to `write_checkpoint`s should
4
+ * be supplied for each set of sync rules.
5
+ */
6
+ CUSTOM = 'manual',
7
+ /**
8
+ * Write checkpoints are stored as a mapping of `user_id` plus
9
+ * replication HEAD (lsn in Postgres) to an automatically generated
10
+ * incrementing `write_checkpoint` (stored as`client_id`).
11
+ */
12
+ MANAGED = 'managed'
13
+ }
14
+
15
+ export interface BaseWriteCheckpointIdentifier {
16
+ /**
17
+ * Identifier for User's account.
18
+ */
19
+ user_id: string;
20
+ }
21
+
22
+ export interface CustomWriteCheckpointFilters extends BaseWriteCheckpointIdentifier {
23
+ /**
24
+ * Sync rules which were active when this checkpoint was created.
25
+ */
26
+ sync_rules_id: number;
27
+ }
28
+
29
+ export interface CustomWriteCheckpointOptions extends CustomWriteCheckpointFilters {
30
+ /**
31
+ * A supplied incrementing Write Checkpoint number
32
+ */
33
+ checkpoint: bigint;
34
+ }
35
+
36
+ /**
37
+ * Options for creating a custom Write Checkpoint in a batch.
38
+ * A {@link BucketStorageBatch} is already associated with a Sync Rules instance.
39
+ * The `sync_rules_id` is not required here.
40
+ */
41
+ export type BatchedCustomWriteCheckpointOptions = Omit<CustomWriteCheckpointOptions, 'sync_rules_id'>;
42
+
43
+ /**
44
+ * Managed Write Checkpoints are a mapping of User ID to replication HEAD
45
+ */
46
+ export interface ManagedWriteCheckpointFilters extends BaseWriteCheckpointIdentifier {
47
+ /**
48
+ * Replication HEAD(s) at the creation of the checkpoint.
49
+ */
50
+ heads: Record<string, string>;
51
+ }
52
+
53
+ export type ManagedWriteCheckpointOptions = ManagedWriteCheckpointFilters;
54
+
55
+ export type LastWriteCheckpointFilters = CustomWriteCheckpointFilters | ManagedWriteCheckpointFilters;
56
+
57
+ export interface WriteCheckpointAPI {
58
+ batchCreateCustomWriteCheckpoints(checkpoints: CustomWriteCheckpointOptions[]): Promise<void>;
59
+
60
+ createCustomWriteCheckpoint(checkpoint: CustomWriteCheckpointOptions): Promise<bigint>;
61
+
62
+ createManagedWriteCheckpoint(checkpoint: ManagedWriteCheckpointOptions): Promise<bigint>;
63
+
64
+ lastWriteCheckpoint(filters: LastWriteCheckpointFilters): Promise<bigint | null>;
65
+ }
66
+
67
+ export const DEFAULT_WRITE_CHECKPOINT_MODE = WriteCheckpointMode.MANAGED;
@@ -122,7 +122,8 @@ export class CompoundConfigCollector {
122
122
  },
123
123
  // TODO maybe move this out of the connection or something
124
124
  // slot_name_prefix: connections[0]?.slot_name_prefix ?? 'powersync_'
125
- slot_name_prefix: 'powersync_'
125
+ slot_name_prefix: 'powersync_',
126
+ parameters: baseConfig.parameters ?? {}
126
127
  };
127
128
 
128
129
  return config;
@@ -64,4 +64,5 @@ export type ResolvedPowerSyncConfig = {
64
64
 
65
65
  /** Prefix for postgres replication slot names. May eventually be connection-specific. */
66
66
  slot_name_prefix: string;
67
+ parameters: Record<string, number | string | boolean | null>;
67
68
  };
@@ -1,10 +1,6 @@
1
- import {
2
- BucketDataBatchOptions,
3
- ParseSyncRulesOptions,
4
- PersistedSyncRulesContent,
5
- StartBatchOptions
6
- } from '@/storage/BucketStorage.js';
7
- import { RequestParameters, SqlSyncRules } from '@powersync/service-sync-rules';
1
+ import { BucketDataBatchOptions } from '@/storage/BucketStorage.js';
2
+ import { getUuidReplicaIdentityBson } from '@/util/util-index.js';
3
+ import { RequestParameters } from '@powersync/service-sync-rules';
8
4
  import { describe, expect, test } from 'vitest';
9
5
  import { fromAsync, oneFromAsync } from './stream_utils.js';
10
6
  import {
@@ -16,10 +12,8 @@ import {
16
12
  PARSE_OPTIONS,
17
13
  rid,
18
14
  StorageFactory,
19
- testRules,
20
- ZERO_LSN
15
+ testRules
21
16
  } from './util.js';
22
- import { getUuidReplicaIdentityBson } from '@/util/util-index.js';
23
17
 
24
18
  const TEST_TABLE = makeTestTable('test', ['id']);
25
19
 
@@ -1406,4 +1400,42 @@ bucket_definitions:
1406
1400
 
1407
1401
  expect(getBatchMeta(batch3)).toEqual(null);
1408
1402
  });
1403
+
1404
+ test('batch should be disposed automatically', async () => {
1405
+ const sync_rules = testRules(`
1406
+ bucket_definitions:
1407
+ global:
1408
+ data: []
1409
+ `);
1410
+
1411
+ const storage = (await factory()).getInstance(sync_rules);
1412
+
1413
+ let isDisposed = false;
1414
+ await storage.startBatch(BATCH_OPTIONS, async (batch) => {
1415
+ batch.registerListener({
1416
+ disposed: () => {
1417
+ isDisposed = true;
1418
+ }
1419
+ });
1420
+ });
1421
+ expect(isDisposed).true;
1422
+
1423
+ isDisposed = false;
1424
+ let errorCaught = false;
1425
+ try {
1426
+ await storage.startBatch(BATCH_OPTIONS, async (batch) => {
1427
+ batch.registerListener({
1428
+ disposed: () => {
1429
+ isDisposed = true;
1430
+ }
1431
+ });
1432
+ throw new Error(`Testing exceptions`);
1433
+ });
1434
+ } catch (ex) {
1435
+ errorCaught = true;
1436
+ expect(ex.message.includes('Testing')).true;
1437
+ }
1438
+ expect(errorCaught).true;
1439
+ expect(isDisposed).true;
1440
+ });
1409
1441
  }
package/test/src/util.ts CHANGED
@@ -11,11 +11,10 @@ import { SourceTable } from '@/storage/SourceTable.js';
11
11
  import { PowerSyncMongo } from '@/storage/mongo/db.js';
12
12
  import { SyncBucketData } from '@/util/protocol-types.js';
13
13
  import { getUuidReplicaIdentityBson, hashData } from '@/util/utils.js';
14
+ import { SqlSyncRules } from '@powersync/service-sync-rules';
14
15
  import * as bson from 'bson';
15
16
  import * as mongo from 'mongodb';
16
17
  import { env } from './env.js';
17
- import { SqlSyncRules } from '@powersync/service-sync-rules';
18
- import { ReplicaId } from '@/storage/storage-index.js';
19
18
 
20
19
  // The metrics need to be initialised before they can be used
21
20
  await Metrics.initialise({