@powersync/service-core 0.0.0-dev-20241007145127 → 0.0.0-dev-20241015210820

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 (112) hide show
  1. package/CHANGELOG.md +9 -5
  2. package/dist/api/RouteAPI.d.ts +6 -4
  3. package/dist/api/diagnostics.js +169 -105
  4. package/dist/api/diagnostics.js.map +1 -1
  5. package/dist/api/schema.js +2 -2
  6. package/dist/api/schema.js.map +1 -1
  7. package/dist/entry/commands/compact-action.js +73 -9
  8. package/dist/entry/commands/compact-action.js.map +1 -1
  9. package/dist/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.d.ts +3 -0
  10. package/dist/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.js +31 -0
  11. package/dist/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.js.map +1 -0
  12. package/dist/replication/AbstractReplicationJob.d.ts +1 -1
  13. package/dist/replication/AbstractReplicationJob.js +2 -2
  14. package/dist/replication/AbstractReplicationJob.js.map +1 -1
  15. package/dist/replication/AbstractReplicator.d.ts +2 -2
  16. package/dist/replication/AbstractReplicator.js +66 -3
  17. package/dist/replication/AbstractReplicator.js.map +1 -1
  18. package/dist/replication/ReplicationEngine.js.map +1 -1
  19. package/dist/replication/ReplicationModule.js +3 -0
  20. package/dist/replication/ReplicationModule.js.map +1 -1
  21. package/dist/replication/replication-index.d.ts +1 -1
  22. package/dist/replication/replication-index.js +1 -1
  23. package/dist/replication/replication-index.js.map +1 -1
  24. package/dist/routes/configure-fastify.js +12 -12
  25. package/dist/routes/configure-fastify.js.map +1 -1
  26. package/dist/routes/configure-rsocket.js +4 -1
  27. package/dist/routes/configure-rsocket.js.map +1 -1
  28. package/dist/routes/endpoints/admin.js.map +1 -1
  29. package/dist/routes/endpoints/checkpointing.js +5 -2
  30. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  31. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  32. package/dist/routes/router.d.ts +8 -1
  33. package/dist/routes/router.js.map +1 -1
  34. package/dist/runner/teardown.js +66 -4
  35. package/dist/runner/teardown.js.map +1 -1
  36. package/dist/storage/BucketStorage.d.ts +41 -18
  37. package/dist/storage/BucketStorage.js +6 -0
  38. package/dist/storage/BucketStorage.js.map +1 -1
  39. package/dist/storage/MongoBucketStorage.d.ts +12 -5
  40. package/dist/storage/MongoBucketStorage.js +44 -23
  41. package/dist/storage/MongoBucketStorage.js.map +1 -1
  42. package/dist/storage/ReplicationEventPayload.d.ts +14 -0
  43. package/dist/storage/ReplicationEventPayload.js +2 -0
  44. package/dist/storage/ReplicationEventPayload.js.map +1 -0
  45. package/dist/storage/SourceTable.d.ts +8 -0
  46. package/dist/storage/SourceTable.js +9 -1
  47. package/dist/storage/SourceTable.js.map +1 -1
  48. package/dist/storage/StorageEngine.d.ts +10 -2
  49. package/dist/storage/StorageEngine.js +23 -3
  50. package/dist/storage/StorageEngine.js.map +1 -1
  51. package/dist/storage/StorageProvider.d.ts +9 -2
  52. package/dist/storage/mongo/MongoBucketBatch.d.ts +12 -4
  53. package/dist/storage/mongo/MongoBucketBatch.js +60 -21
  54. package/dist/storage/mongo/MongoBucketBatch.js.map +1 -1
  55. package/dist/storage/mongo/MongoStorageProvider.d.ts +1 -1
  56. package/dist/storage/mongo/MongoStorageProvider.js +3 -2
  57. package/dist/storage/mongo/MongoStorageProvider.js.map +1 -1
  58. package/dist/storage/mongo/MongoSyncBucketStorage.d.ts +4 -5
  59. package/dist/storage/mongo/MongoSyncBucketStorage.js +74 -12
  60. package/dist/storage/mongo/MongoSyncBucketStorage.js.map +1 -1
  61. package/dist/storage/mongo/MongoWriteCheckpointAPI.d.ts +18 -0
  62. package/dist/storage/mongo/MongoWriteCheckpointAPI.js +90 -0
  63. package/dist/storage/mongo/MongoWriteCheckpointAPI.js.map +1 -0
  64. package/dist/storage/mongo/db.d.ts +3 -2
  65. package/dist/storage/mongo/db.js +1 -0
  66. package/dist/storage/mongo/db.js.map +1 -1
  67. package/dist/storage/mongo/models.d.ts +7 -1
  68. package/dist/storage/storage-index.d.ts +2 -0
  69. package/dist/storage/storage-index.js +2 -0
  70. package/dist/storage/storage-index.js.map +1 -1
  71. package/dist/storage/write-checkpoint.d.ts +55 -0
  72. package/dist/storage/write-checkpoint.js +16 -0
  73. package/dist/storage/write-checkpoint.js.map +1 -0
  74. package/dist/util/protocol-types.d.ts +2 -1
  75. package/package.json +5 -5
  76. package/src/api/RouteAPI.ts +7 -4
  77. package/src/api/diagnostics.ts +4 -2
  78. package/src/api/schema.ts +3 -3
  79. package/src/entry/commands/compact-action.ts +4 -2
  80. package/src/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.ts +37 -0
  81. package/src/replication/AbstractReplicationJob.ts +4 -4
  82. package/src/replication/AbstractReplicator.ts +5 -4
  83. package/src/replication/ReplicationEngine.ts +1 -1
  84. package/src/replication/ReplicationModule.ts +4 -0
  85. package/src/replication/replication-index.ts +1 -1
  86. package/src/routes/configure-fastify.ts +16 -17
  87. package/src/routes/configure-rsocket.ts +7 -2
  88. package/src/routes/endpoints/admin.ts +2 -2
  89. package/src/routes/endpoints/checkpointing.ts +5 -2
  90. package/src/routes/endpoints/sync-rules.ts +1 -0
  91. package/src/routes/router.ts +7 -1
  92. package/src/runner/teardown.ts +3 -3
  93. package/src/storage/BucketStorage.ts +50 -19
  94. package/src/storage/MongoBucketStorage.ts +70 -29
  95. package/src/storage/ReplicationEventPayload.ts +16 -0
  96. package/src/storage/SourceTable.ts +10 -1
  97. package/src/storage/StorageEngine.ts +34 -5
  98. package/src/storage/StorageProvider.ts +10 -2
  99. package/src/storage/mongo/MongoBucketBatch.ts +83 -27
  100. package/src/storage/mongo/MongoStorageProvider.ts +4 -3
  101. package/src/storage/mongo/MongoSyncBucketStorage.ts +22 -18
  102. package/src/storage/mongo/MongoWriteCheckpointAPI.ts +136 -0
  103. package/src/storage/mongo/db.ts +4 -1
  104. package/src/storage/mongo/models.ts +8 -1
  105. package/src/storage/storage-index.ts +2 -0
  106. package/src/storage/write-checkpoint.ts +67 -0
  107. package/src/util/protocol-types.ts +1 -1
  108. package/test/src/compacting.test.ts +13 -15
  109. package/test/src/data_storage.test.ts +95 -63
  110. package/test/src/sync.test.ts +10 -9
  111. package/test/src/util.ts +1 -2
  112. 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;
@@ -88,7 +88,7 @@ export type StreamingSyncLine =
88
88
  */
89
89
  export type OpId = string;
90
90
 
91
- export interface Checkpoint {
91
+ interface Checkpoint {
92
92
  last_op_id: OpId;
93
93
  write_checkpoint?: OpId;
94
94
  buckets: BucketChecksum[];
@@ -1,11 +1,9 @@
1
+ import { SaveOperationTag } from '@/storage/BucketStorage.js';
1
2
  import { MongoCompactOptions } from '@/storage/mongo/MongoCompactor.js';
2
- import { SqlSyncRules } from '@powersync/service-sync-rules';
3
3
  import { describe, expect, test } from 'vitest';
4
4
  import { validateCompactedBucket } from './bucket_validation.js';
5
5
  import { oneFromAsync } from './stream_utils.js';
6
- import { BATCH_OPTIONS, makeTestTable, MONGO_STORAGE_FACTORY, rid, testRules, ZERO_LSN } from './util.js';
7
- import { ParseSyncRulesOptions, PersistedSyncRulesContent, StartBatchOptions } from '@/storage/BucketStorage.js';
8
- import { getUuidReplicaIdentityBson } from '@/util/util-index.js';
6
+ import { BATCH_OPTIONS, makeTestTable, MONGO_STORAGE_FACTORY, rid, testRules } from './util.js';
9
7
 
10
8
  const TEST_TABLE = makeTestTable('test', ['id']);
11
9
 
@@ -31,7 +29,7 @@ bucket_definitions:
31
29
  const result = await storage.startBatch(BATCH_OPTIONS, async (batch) => {
32
30
  await batch.save({
33
31
  sourceTable: TEST_TABLE,
34
- tag: 'insert',
32
+ tag: SaveOperationTag.INSERT,
35
33
  after: {
36
34
  id: 't1'
37
35
  },
@@ -40,7 +38,7 @@ bucket_definitions:
40
38
 
41
39
  await batch.save({
42
40
  sourceTable: TEST_TABLE,
43
- tag: 'insert',
41
+ tag: SaveOperationTag.INSERT,
44
42
  after: {
45
43
  id: 't2'
46
44
  },
@@ -49,7 +47,7 @@ bucket_definitions:
49
47
 
50
48
  await batch.save({
51
49
  sourceTable: TEST_TABLE,
52
- tag: 'update',
50
+ tag: SaveOperationTag.UPDATE,
53
51
  after: {
54
52
  id: 't2'
55
53
  },
@@ -128,7 +126,7 @@ bucket_definitions:
128
126
  const result = await storage.startBatch(BATCH_OPTIONS, async (batch) => {
129
127
  await batch.save({
130
128
  sourceTable: TEST_TABLE,
131
- tag: 'insert',
129
+ tag: SaveOperationTag.INSERT,
132
130
  after: {
133
131
  id: 't1'
134
132
  },
@@ -137,7 +135,7 @@ bucket_definitions:
137
135
 
138
136
  await batch.save({
139
137
  sourceTable: TEST_TABLE,
140
- tag: 'insert',
138
+ tag: SaveOperationTag.INSERT,
141
139
  after: {
142
140
  id: 't2'
143
141
  },
@@ -146,7 +144,7 @@ bucket_definitions:
146
144
 
147
145
  await batch.save({
148
146
  sourceTable: TEST_TABLE,
149
- tag: 'delete',
147
+ tag: SaveOperationTag.DELETE,
150
148
  before: {
151
149
  id: 't1'
152
150
  },
@@ -155,7 +153,7 @@ bucket_definitions:
155
153
 
156
154
  await batch.save({
157
155
  sourceTable: TEST_TABLE,
158
- tag: 'update',
156
+ tag: SaveOperationTag.UPDATE,
159
157
  after: {
160
158
  id: 't2'
161
159
  },
@@ -233,7 +231,7 @@ bucket_definitions:
233
231
  const result = await storage.startBatch(BATCH_OPTIONS, async (batch) => {
234
232
  await batch.save({
235
233
  sourceTable: TEST_TABLE,
236
- tag: 'insert',
234
+ tag: SaveOperationTag.INSERT,
237
235
  after: {
238
236
  id: 't1'
239
237
  },
@@ -242,7 +240,7 @@ bucket_definitions:
242
240
 
243
241
  await batch.save({
244
242
  sourceTable: TEST_TABLE,
245
- tag: 'insert',
243
+ tag: SaveOperationTag.INSERT,
246
244
  after: {
247
245
  id: 't2'
248
246
  },
@@ -251,7 +249,7 @@ bucket_definitions:
251
249
 
252
250
  await batch.save({
253
251
  sourceTable: TEST_TABLE,
254
- tag: 'delete',
252
+ tag: SaveOperationTag.DELETE,
255
253
  before: {
256
254
  id: 't1'
257
255
  },
@@ -265,7 +263,7 @@ bucket_definitions:
265
263
  const result2 = await storage.startBatch(BATCH_OPTIONS, async (batch) => {
266
264
  await batch.save({
267
265
  sourceTable: TEST_TABLE,
268
- tag: 'delete',
266
+ tag: SaveOperationTag.DELETE,
269
267
  before: {
270
268
  id: 't2'
271
269
  },