@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 @@
1
+ {"version":3,"file":"write-checkpoint.js","sourceRoot":"","sources":["../../src/storage/write-checkpoint.ts"],"names":[],"mappings":"AAAA,MAAM,CAAN,IAAY,mBAYX;AAZD,WAAY,mBAAmB;IAC7B;;;OAGG;IACH,wCAAiB,CAAA;IACjB;;;;OAIG;IACH,0CAAmB,CAAA;AACrB,CAAC,EAZW,mBAAmB,KAAnB,mBAAmB,QAY9B;AAsDD,MAAM,CAAC,MAAM,6BAA6B,GAAG,mBAAmB,CAAC,OAAO,CAAC"}
@@ -71,7 +71,7 @@ export type StreamingSyncLine = StreamingSyncData | StreamingSyncCheckpoint | St
71
71
  * 64-bit unsigned number, as a base-10 string.
72
72
  */
73
73
  export type OpId = string;
74
- export interface Checkpoint {
74
+ interface Checkpoint {
75
75
  last_op_id: OpId;
76
76
  write_checkpoint?: OpId;
77
77
  buckets: BucketChecksum[];
@@ -119,3 +119,4 @@ export interface BucketChecksum {
119
119
  */
120
120
  count: number;
121
121
  }
122
+ export {};
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
- "version": "0.0.0-dev-20241007145127",
8
+ "version": "0.0.0-dev-20241015210820",
9
9
  "main": "dist/index.js",
10
10
  "license": "FSL-1.1-Apache-2.0",
11
11
  "type": "module",
@@ -33,11 +33,11 @@
33
33
  "uuid": "^9.0.1",
34
34
  "winston": "^3.13.0",
35
35
  "yaml": "^2.3.2",
36
- "@powersync/lib-services-framework": "0.0.0-dev-20241007145127",
36
+ "@powersync/lib-services-framework": "0.0.0-dev-20241015210820",
37
37
  "@powersync/service-jsonbig": "0.17.10",
38
- "@powersync/service-rsocket-router": "0.0.0-dev-20241007145127",
39
- "@powersync/service-sync-rules": "0.0.0-dev-20241007145127",
40
- "@powersync/service-types": "0.0.0-dev-20241007145127"
38
+ "@powersync/service-rsocket-router": "0.0.0-dev-20241015210820",
39
+ "@powersync/service-sync-rules": "0.0.0-dev-20241015210820",
40
+ "@powersync/service-types": "0.0.0-dev-20241015210820"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/async": "^3.2.24",
@@ -1,6 +1,6 @@
1
1
  import { SqlSyncRules, TablePattern } from '@powersync/service-sync-rules';
2
2
  import * as types from '@powersync/service-types';
3
- import { ParseSyncRulesOptions } from '../storage/BucketStorage.js';
3
+ import { ParseSyncRulesOptions, SyncRulesBucketStorage } from '../storage/BucketStorage.js';
4
4
 
5
5
  export interface PatternResult {
6
6
  schema: string;
@@ -10,6 +10,10 @@ export interface PatternResult {
10
10
  table?: types.TableInfo;
11
11
  }
12
12
 
13
+ export interface ReplicationLagOptions {
14
+ bucketStorage: SyncRulesBucketStorage;
15
+ }
16
+
13
17
  /**
14
18
  * Describes all the methods currently required to service the sync API endpoints.
15
19
  */
@@ -17,7 +21,7 @@ export interface RouteAPI {
17
21
  /**
18
22
  * @returns basic identification of the connection
19
23
  */
20
- getSourceConfig(): Promise<types.configFile.DataSourceConfig>;
24
+ getSourceConfig(): Promise<types.configFile.ResolvedDataSourceConfig>;
21
25
 
22
26
  /**
23
27
  * Checks the current connection status of the data source.
@@ -42,9 +46,8 @@ export interface RouteAPI {
42
46
  /**
43
47
  * @returns The replication lag: that is the amount of data which has not been
44
48
  * replicated yet, in bytes.
45
- * @param {string} syncRulesId An identifier representing which set of sync rules the lag is required for.
46
49
  */
47
- getReplicationLag(syncRulesId: string): Promise<number | undefined>;
50
+ getReplicationLag(options: ReplicationLagOptions): Promise<number | undefined>;
48
51
 
49
52
  /**
50
53
  * Get the current LSN or equivalent replication HEAD position identifier
@@ -57,7 +57,7 @@ export async function getSyncRulesStatus(
57
57
  // This method can run under some situations if no connection is configured yet.
58
58
  // It will return a default tag in such a case. This default tag is not module specific.
59
59
  const tag = sourceConfig.tag ?? DEFAULT_TAG;
60
- const systemStorage = live_status ? bucketStorage.getInstance(sync_rules) : undefined;
60
+ using systemStorage = live_status ? bucketStorage.getInstance(sync_rules) : undefined;
61
61
  const status = await systemStorage?.getStatus();
62
62
  let replication_lag_bytes: number | undefined = undefined;
63
63
 
@@ -78,7 +78,9 @@ export async function getSyncRulesStatus(
78
78
 
79
79
  if (systemStorage) {
80
80
  try {
81
- replication_lag_bytes = await apiHandler.getReplicationLag(systemStorage.slot_name);
81
+ replication_lag_bytes = await apiHandler.getReplicationLag({
82
+ bucketStorage: systemStorage
83
+ });
82
84
  } catch (e) {
83
85
  // Ignore
84
86
  logger.warn(`Unable to get replication lag`, e);
package/src/api/schema.ts CHANGED
@@ -16,9 +16,9 @@ export async function getConnectionsSchema(api: api.RouteAPI): Promise<internal_
16
16
  return {
17
17
  connections: [
18
18
  {
19
- schemas: await api.getConnectionSchema(),
20
- tag: baseConfig.tag!,
21
- id: baseConfig.id
19
+ id: baseConfig.id,
20
+ tag: baseConfig.tag,
21
+ schemas: await api.getConnectionSchema()
22
22
  }
23
23
  ],
24
24
  defaultConnectionTag: baseConfig.tag!,
@@ -36,13 +36,15 @@ export function registerCompactAction(program: Command) {
36
36
  const client = psdb.client;
37
37
  await client.connect();
38
38
  try {
39
- const bucketStorage = new storage.MongoBucketStorage(psdb, { slot_name_prefix: configuration.slot_name_prefix });
39
+ const bucketStorage = new storage.MongoBucketStorage(psdb, {
40
+ slot_name_prefix: configuration.slot_name_prefix
41
+ });
40
42
  const active = await bucketStorage.getActiveSyncRulesContent();
41
43
  if (active == null) {
42
44
  logger.info('No active instance to compact');
43
45
  return;
44
46
  }
45
- const p = bucketStorage.getInstance(active);
47
+ using p = bucketStorage.getInstance(active);
46
48
  logger.info('Performing compaction...');
47
49
  await p.compact({ memoryLimitMB: COMPACT_MEMORY_LIMIT_MB });
48
50
  logger.info('Successfully compacted storage.');
@@ -0,0 +1,37 @@
1
+ import * as storage from '../../../storage/storage-index.js';
2
+ import * as utils from '../../../util/util-index.js';
3
+
4
+ const INDEX_NAME = 'user_sync_rule_unique';
5
+
6
+ export const up = async (context: utils.MigrationContext) => {
7
+ const { runner_config } = context;
8
+ const config = await utils.loadConfig(runner_config);
9
+ const db = storage.createPowerSyncMongo(config.storage);
10
+
11
+ try {
12
+ await db.custom_write_checkpoints.createIndex(
13
+ {
14
+ user_id: 1,
15
+ sync_rules_id: 1
16
+ },
17
+ { name: INDEX_NAME, unique: true }
18
+ );
19
+ } finally {
20
+ await db.client.close();
21
+ }
22
+ };
23
+
24
+ export const down = async (context: utils.MigrationContext) => {
25
+ const { runner_config } = context;
26
+ const config = await utils.loadConfig(runner_config);
27
+
28
+ const db = storage.createPowerSyncMongo(config.storage);
29
+
30
+ try {
31
+ if (await db.custom_write_checkpoints.indexExists(INDEX_NAME)) {
32
+ await db.custom_write_checkpoints.dropIndex(INDEX_NAME);
33
+ }
34
+ } finally {
35
+ await db.client.close();
36
+ }
37
+ };
@@ -1,7 +1,7 @@
1
- import * as storage from '../storage/storage-index.js';
2
- import { ErrorRateLimiter } from './ErrorRateLimiter.js';
3
1
  import { container, logger } from '@powersync/lib-services-framework';
4
2
  import winston from 'winston';
3
+ import * as storage from '../storage/storage-index.js';
4
+ import { ErrorRateLimiter } from './ErrorRateLimiter.js';
5
5
 
6
6
  export interface AbstractReplicationJobOptions {
7
7
  id: string;
@@ -16,7 +16,7 @@ export abstract class AbstractReplicationJob {
16
16
  protected isReplicatingPromise: Promise<void> | null = null;
17
17
 
18
18
  protected constructor(protected options: AbstractReplicationJobOptions) {
19
- this.logger = logger.child({ name: `ReplicationJob: ${options.id}` });
19
+ this.logger = logger.child({ name: `ReplicationJob: ${this.id}` });
20
20
  }
21
21
 
22
22
  /**
@@ -52,7 +52,7 @@ export abstract class AbstractReplicationJob {
52
52
  * Safely stop the replication process
53
53
  */
54
54
  public async stop(): Promise<void> {
55
- this.logger.info(`Stopping ${this.id} replication job for sync rule iteration: ${this.storage.group_id}`);
55
+ this.logger.info(`Stopping replication job for sync rule iteration: ${this.storage.group_id}`);
56
56
  this.abortController.abort();
57
57
  await this.isReplicatingPromise;
58
58
  }
@@ -1,10 +1,10 @@
1
+ import { container, logger } from '@powersync/lib-services-framework';
1
2
  import { hrtime } from 'node:process';
3
+ import winston from 'winston';
2
4
  import * as storage from '../storage/storage-index.js';
3
- import { container, logger } from '@powersync/lib-services-framework';
5
+ import { StorageEngine } from '../storage/storage-index.js';
4
6
  import { SyncRulesProvider } from '../util/config/sync-rules/sync-rules-provider.js';
5
- import winston from 'winston';
6
7
  import { AbstractReplicationJob } from './AbstractReplicationJob.js';
7
- import { StorageEngine } from '../storage/storage-index.js';
8
8
  import { ErrorRateLimiter } from './ErrorRateLimiter.js';
9
9
 
10
10
  // 5 minutes
@@ -192,6 +192,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
192
192
  try {
193
193
  await job.stop();
194
194
  await this.terminateSyncRules(job.storage);
195
+ job.storage[Symbol.dispose]();
195
196
  } catch (e) {
196
197
  // This will be retried
197
198
  this.logger.warn('Failed to terminate old replication job}', e);
@@ -202,7 +203,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
202
203
  const stopped = await this.storage.getStoppedSyncRules();
203
204
  for (let syncRules of stopped) {
204
205
  try {
205
- const syncRuleStorage = this.storage.getInstance(syncRules);
206
+ using syncRuleStorage = this.storage.getInstance(syncRules);
206
207
  await this.terminateSyncRules(syncRuleStorage);
207
208
  } catch (e) {
208
209
  this.logger.warn(`Failed clean up replication config for sync rule: ${syncRules.id}`, e);
@@ -1,5 +1,5 @@
1
- import { AbstractReplicator } from './AbstractReplicator.js';
2
1
  import { logger } from '@powersync/lib-services-framework';
2
+ import { AbstractReplicator } from './AbstractReplicator.js';
3
3
 
4
4
  export class ReplicationEngine {
5
5
  private readonly replicators: Map<string, AbstractReplicator> = new Map();
@@ -58,6 +58,10 @@ export abstract class ReplicationModule<TConfig extends DataSourceConfig> extend
58
58
  return;
59
59
  }
60
60
 
61
+ if (!matchingConfig.length) {
62
+ return;
63
+ }
64
+
61
65
  if (matchingConfig.length > 1) {
62
66
  this.logger.warning(
63
67
  `Multiple data sources of type ${this.type} found in the configuration. Only the first will be used.`
@@ -1,5 +1,5 @@
1
- export * from './ErrorRateLimiter.js';
2
1
  export * from './AbstractReplicationJob.js';
3
2
  export * from './AbstractReplicator.js';
3
+ export * from './ErrorRateLimiter.js';
4
4
  export * from './ReplicationEngine.js';
5
5
  export * from './ReplicationModule.js';
@@ -8,7 +8,7 @@ import { CHECKPOINT_ROUTES } from './endpoints/checkpointing.js';
8
8
  import { SYNC_RULES_ROUTES } from './endpoints/sync-rules.js';
9
9
  import { SYNC_STREAM_ROUTES } from './endpoints/sync-stream.js';
10
10
  import { createRequestQueueHook, CreateRequestQueueParams } from './hooks.js';
11
- import { RouteDefinition } from './router.js';
11
+ import { RouteDefinition, RouterServiceContext } from './router.js';
12
12
 
13
13
  /**
14
14
  * A list of route definitions to be registered as endpoints.
@@ -56,6 +56,19 @@ export const DEFAULT_ROUTE_OPTIONS = {
56
56
  */
57
57
  export function configureFastifyServer(server: fastify.FastifyInstance, options: FastifyServerConfig) {
58
58
  const { service_context, routes = DEFAULT_ROUTE_OPTIONS } = options;
59
+
60
+ const generateContext = async () => {
61
+ const { routerEngine } = service_context;
62
+ if (!routerEngine) {
63
+ throw new Error(`RouterEngine has not been registered`);
64
+ }
65
+
66
+ return {
67
+ user_id: undefined,
68
+ service_context: service_context as RouterServiceContext
69
+ };
70
+ };
71
+
59
72
  /**
60
73
  * Fastify creates an encapsulated context for each `.register` call.
61
74
  * Creating a separate context here to separate the concurrency limits for Admin APIs
@@ -63,16 +76,7 @@ export function configureFastifyServer(server: fastify.FastifyInstance, options:
63
76
  * https://github.com/fastify/fastify/blob/main/docs/Reference/Encapsulation.md
64
77
  */
65
78
  server.register(async function (childContext) {
66
- registerFastifyRoutes(
67
- childContext,
68
- async () => {
69
- return {
70
- user_id: undefined,
71
- service_context
72
- };
73
- },
74
- routes.api?.routes ?? DEFAULT_ROUTE_OPTIONS.api.routes
75
- );
79
+ registerFastifyRoutes(childContext, generateContext, routes.api?.routes ?? DEFAULT_ROUTE_OPTIONS.api.routes);
76
80
  // Limit the active concurrent requests
77
81
  childContext.addHook(
78
82
  'onRequest',
@@ -84,12 +88,7 @@ export function configureFastifyServer(server: fastify.FastifyInstance, options:
84
88
  server.register(async function (childContext) {
85
89
  registerFastifyRoutes(
86
90
  childContext,
87
- async () => {
88
- return {
89
- user_id: undefined,
90
- service_context
91
- };
92
- },
91
+ generateContext,
93
92
  routes.sync_stream?.routes ?? DEFAULT_ROUTE_OPTIONS.sync_stream.routes
94
93
  );
95
94
  // Limit the active concurrent requests
@@ -8,7 +8,7 @@ import { ServiceContext } from '../system/ServiceContext.js';
8
8
  import { generateContext, getTokenFromHeader } from './auth.js';
9
9
  import { syncStreamReactive } from './endpoints/socket-route.js';
10
10
  import { RSocketContextMeta, SocketRouteGenerator } from './router-socket.js';
11
- import { Context } from './router.js';
11
+ import { Context, RouterServiceContext } from './router.js';
12
12
 
13
13
  export type RSockerRouterConfig = {
14
14
  service_context: ServiceContext;
@@ -36,12 +36,17 @@ export function configureRSocket(router: ReactiveSocketRouter<Context>, options:
36
36
  if (context?.token_payload == null) {
37
37
  throw new errors.AuthorizationError(token_errors ?? 'Authentication required');
38
38
  }
39
+
40
+ if (!service_context.routerEngine) {
41
+ throw new Error(`RouterEngine has not been registered`);
42
+ }
43
+
39
44
  return {
40
45
  token,
41
46
  user_agent,
42
47
  ...context,
43
48
  token_errors: token_errors,
44
- service_context
49
+ service_context: service_context as RouterServiceContext
45
50
  };
46
51
  } else {
47
52
  throw new errors.AuthorizationError('No token provided');
@@ -137,8 +137,8 @@ export const reprocess = routeDefinition({
137
137
  connections: [
138
138
  {
139
139
  // Previously the connection was asserted with `!`
140
- tag: baseConfig!.tag!,
141
- id: baseConfig!.id,
140
+ tag: baseConfig.tag,
141
+ id: baseConfig.id,
142
142
  slot_name: new_rules.slot_name
143
143
  }
144
144
  ]
@@ -1,5 +1,5 @@
1
- import * as t from 'ts-codec';
2
1
  import { logger, router, schema } from '@powersync/lib-services-framework';
2
+ import * as t from 'ts-codec';
3
3
 
4
4
  import * as util from '../../util/util-index.js';
5
5
  import { authUser } from '../auth.js';
@@ -63,7 +63,10 @@ export const writeCheckpoint2 = routeDefinition({
63
63
  storageEngine: { activeBucketStorage }
64
64
  } = service_context;
65
65
 
66
- const writeCheckpoint = await activeBucketStorage.createWriteCheckpoint(full_user_id, { '1': currentCheckpoint });
66
+ const writeCheckpoint = await activeBucketStorage.createManagedWriteCheckpoint({
67
+ user_id: full_user_id,
68
+ heads: { '1': currentCheckpoint }
69
+ });
67
70
  logger.info(`Write checkpoint 2: ${JSON.stringify({ currentCheckpoint, id: String(full_user_id) })}`);
68
71
 
69
72
  return {
@@ -3,6 +3,7 @@ import { SqlSyncRules, SyncRulesErrors } from '@powersync/service-sync-rules';
3
3
  import type { FastifyPluginAsync } from 'fastify';
4
4
  import * as t from 'ts-codec';
5
5
 
6
+ import * as system from '../../system/system-index.js';
6
7
  import { authApi } from '../auth.js';
7
8
  import { routeDefinition } from '../router.js';
8
9
  import { RouteAPI } from '../../api/RouteAPI.js';
@@ -1,13 +1,19 @@
1
1
  import { router } from '@powersync/lib-services-framework';
2
2
  import * as auth from '../auth/auth-index.js';
3
3
  import { ServiceContext } from '../system/ServiceContext.js';
4
+ import { RouterEngine } from './RouterEngine.js';
4
5
 
6
+ /**
7
+ * The {@link RouterEngine} must be provided for these routes
8
+ */
9
+ export type RouterServiceContext = ServiceContext & { routerEngine: RouterEngine };
5
10
  /**
6
11
  * Common context for routes
7
12
  */
8
13
  export type Context = {
9
14
  user_id?: string;
10
- service_context: ServiceContext;
15
+
16
+ service_context: RouterServiceContext;
11
17
 
12
18
  token_payload?: auth.JwtPayload;
13
19
  token_errors?: string[];
@@ -4,11 +4,11 @@
4
4
  // 2. Delete the storage
5
5
 
6
6
  import { container, logger } from '@powersync/lib-services-framework';
7
+ import timers from 'timers/promises';
7
8
  import * as modules from '../modules/modules-index.js';
8
- import * as system from '../system/system-index.js';
9
9
  import * as storage from '../storage/storage-index.js';
10
+ import * as system from '../system/system-index.js';
10
11
  import * as utils from '../util/util-index.js';
11
- import timers from 'timers/promises';
12
12
 
13
13
  export async function teardown(runnerConfig: utils.RunnerConfig) {
14
14
  try {
@@ -51,7 +51,7 @@ async function terminateSyncRules(storageFactory: storage.BucketStorageFactory,
51
51
 
52
52
  // Mark the sync rules as terminated
53
53
  for (let syncRules of combinedSyncRules) {
54
- const syncRulesStorage = storageFactory.getInstance(syncRules);
54
+ using syncRulesStorage = storageFactory.getInstance(syncRules);
55
55
  // The storage will be dropped at the end of the teardown, so we don't need to clear it here
56
56
  await syncRulesStorage.terminate({ clearStorage: false });
57
57
  }
@@ -1,3 +1,4 @@
1
+ import { DisposableListener, DisposableObserverClient } from '@powersync/lib-services-framework';
1
2
  import {
2
3
  EvaluatedParameters,
3
4
  EvaluatedRow,
@@ -8,11 +9,19 @@ import {
8
9
  ToastableSqliteRow
9
10
  } from '@powersync/service-sync-rules';
10
11
  import * as util from '../util/util-index.js';
11
- import { SourceTable } from './SourceTable.js';
12
+ import { ReplicationEventPayload } from './ReplicationEventPayload.js';
12
13
  import { SourceEntityDescriptor } from './SourceEntity.js';
13
- import { ReplicaId } from './storage-index.js';
14
+ import { SourceTable } from './SourceTable.js';
15
+ import { BatchedCustomWriteCheckpointOptions, ReplicaId, WriteCheckpointAPI } from './storage-index.js';
16
+
17
+ export interface BucketStorageFactoryListener extends DisposableListener {
18
+ syncStorageCreated: (storage: SyncRulesBucketStorage) => void;
19
+ replicationEvent: (event: ReplicationEventPayload) => void;
20
+ }
14
21
 
15
- export interface BucketStorageFactory {
22
+ export interface BucketStorageFactory
23
+ extends DisposableObserverClient<BucketStorageFactoryListener>,
24
+ WriteCheckpointAPI {
16
25
  /**
17
26
  * Update sync rules from configuration, if changed.
18
27
  */
@@ -81,10 +90,9 @@ export interface BucketStorageFactory {
81
90
  */
82
91
  getActiveCheckpoint(): Promise<ActiveCheckpoint>;
83
92
 
84
- createWriteCheckpoint(user_id: string, lsns: Record<string, string>): Promise<bigint>;
85
-
86
- lastWriteCheckpoint(user_id: string, lsn: string): Promise<bigint | null>;
87
-
93
+ /**
94
+ * Yields the latest user write checkpoint whenever the sync checkpoint updates.
95
+ */
88
96
  watchWriteCheckpoint(user_id: string, signal: AbortSignal): AsyncIterable<WriteCheckpoint>;
89
97
 
90
98
  /**
@@ -98,20 +106,22 @@ export interface BucketStorageFactory {
98
106
  getPowerSyncInstanceId(): Promise<string>;
99
107
  }
100
108
 
101
- export interface WriteCheckpoint {
102
- base: ActiveCheckpoint;
103
- writeCheckpoint: bigint | null;
104
- }
105
-
106
- export interface ActiveCheckpoint {
109
+ export interface Checkpoint {
107
110
  readonly checkpoint: util.OpId;
108
111
  readonly lsn: string | null;
112
+ }
109
113
 
114
+ export interface ActiveCheckpoint extends Checkpoint {
110
115
  hasSyncRules(): boolean;
111
116
 
112
117
  getBucketStorage(): Promise<SyncRulesBucketStorage | null>;
113
118
  }
114
119
 
120
+ export interface WriteCheckpoint {
121
+ base: ActiveCheckpoint;
122
+ writeCheckpoint: bigint | null;
123
+ }
124
+
115
125
  export interface StorageMetrics {
116
126
  /**
117
127
  * Size of operations (bucket_data)
@@ -194,7 +204,11 @@ export interface StartBatchOptions extends ParseSyncRulesOptions {
194
204
  zeroLSN: string;
195
205
  }
196
206
 
197
- export interface SyncRulesBucketStorage {
207
+ export interface SyncRulesBucketStorageListener extends DisposableListener {
208
+ batchStarted: (batch: BucketStorageBatch) => void;
209
+ }
210
+
211
+ export interface SyncRulesBucketStorage extends DisposableObserverClient<SyncRulesBucketStorageListener> {
198
212
  readonly group_id: number;
199
213
  readonly slot_name: string;
200
214
 
@@ -207,7 +221,7 @@ export interface SyncRulesBucketStorage {
207
221
  callback: (batch: BucketStorageBatch) => Promise<void>
208
222
  ): Promise<FlushedResult | null>;
209
223
 
210
- getCheckpoint(): Promise<{ checkpoint: util.OpId }>;
224
+ getCheckpoint(): Promise<Checkpoint>;
211
225
 
212
226
  getParsedSyncRules(options: ParseSyncRulesOptions): SqlSyncRules;
213
227
 
@@ -293,7 +307,11 @@ export interface FlushedResult {
293
307
  flushed_op: string;
294
308
  }
295
309
 
296
- export interface BucketStorageBatch {
310
+ export interface BucketBatchStorageListener extends DisposableListener {
311
+ replicationEvent: (payload: ReplicationEventPayload) => void;
312
+ }
313
+
314
+ export interface BucketStorageBatch extends DisposableObserverClient<BucketBatchStorageListener> {
297
315
  /**
298
316
  * Save an op, and potentially flush.
299
317
  *
@@ -345,6 +363,11 @@ export interface BucketStorageBatch {
345
363
  lastCheckpointLsn: string | null;
346
364
 
347
365
  markSnapshotDone(tables: SourceTable[], no_checkpoint_before_lsn: string): Promise<SourceTable[]>;
366
+
367
+ /**
368
+ * Queues the creation of a custom Write Checkpoint. This will be persisted after operations are flushed.
369
+ */
370
+ addCustomWriteCheckpoint(checkpoint: BatchedCustomWriteCheckpointOptions): void;
348
371
  }
349
372
 
350
373
  export interface SaveParameterData {
@@ -362,10 +385,18 @@ export interface SaveBucketData {
362
385
  evaluated: EvaluatedRow[];
363
386
  }
364
387
 
388
+ export type SaveOp = 'insert' | 'update' | 'delete';
389
+
365
390
  export type SaveOptions = SaveInsert | SaveUpdate | SaveDelete;
366
391
 
392
+ export enum SaveOperationTag {
393
+ INSERT = 'insert',
394
+ UPDATE = 'update',
395
+ DELETE = 'delete'
396
+ }
397
+
367
398
  export interface SaveInsert {
368
- tag: 'insert';
399
+ tag: SaveOperationTag.INSERT;
369
400
  sourceTable: SourceTable;
370
401
  before?: undefined;
371
402
  beforeReplicaId?: undefined;
@@ -374,7 +405,7 @@ export interface SaveInsert {
374
405
  }
375
406
 
376
407
  export interface SaveUpdate {
377
- tag: 'update';
408
+ tag: SaveOperationTag.UPDATE;
378
409
  sourceTable: SourceTable;
379
410
 
380
411
  /**
@@ -393,7 +424,7 @@ export interface SaveUpdate {
393
424
  }
394
425
 
395
426
  export interface SaveDelete {
396
- tag: 'delete';
427
+ tag: SaveOperationTag.DELETE;
397
428
  sourceTable: SourceTable;
398
429
  before?: SqliteRow;
399
430
  beforeReplicaId: ReplicaId;