@powersync/service-core 0.0.0-dev-20240718134716 → 0.0.0-dev-20240725112650

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 (97) hide show
  1. package/CHANGELOG.md +11 -6
  2. package/dist/entry/cli-entry.js +2 -1
  3. package/dist/entry/cli-entry.js.map +1 -1
  4. package/dist/entry/commands/compact-action.d.ts +2 -0
  5. package/dist/entry/commands/compact-action.js +48 -0
  6. package/dist/entry/commands/compact-action.js.map +1 -0
  7. package/dist/entry/entry-index.d.ts +1 -0
  8. package/dist/entry/entry-index.js +1 -0
  9. package/dist/entry/entry-index.js.map +1 -1
  10. package/dist/metrics/Metrics.d.ts +4 -3
  11. package/dist/metrics/Metrics.js +51 -0
  12. package/dist/metrics/Metrics.js.map +1 -1
  13. package/dist/replication/WalStream.js +6 -8
  14. package/dist/replication/WalStream.js.map +1 -1
  15. package/dist/routes/configure-fastify.d.ts +883 -0
  16. package/dist/routes/configure-fastify.js +58 -0
  17. package/dist/routes/configure-fastify.js.map +1 -0
  18. package/dist/routes/configure-rsocket.d.ts +13 -0
  19. package/dist/routes/configure-rsocket.js +46 -0
  20. package/dist/routes/configure-rsocket.js.map +1 -0
  21. package/dist/routes/endpoints/socket-route.js +6 -14
  22. package/dist/routes/endpoints/socket-route.js.map +1 -1
  23. package/dist/routes/endpoints/sync-stream.js +4 -5
  24. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  25. package/dist/routes/route-register.d.ts +1 -1
  26. package/dist/routes/route-register.js +1 -1
  27. package/dist/routes/route-register.js.map +1 -1
  28. package/dist/routes/router-socket.d.ts +4 -4
  29. package/dist/routes/router-socket.js.map +1 -1
  30. package/dist/routes/router.d.ts +1 -0
  31. package/dist/routes/router.js.map +1 -1
  32. package/dist/routes/routes-index.d.ts +2 -0
  33. package/dist/routes/routes-index.js +2 -0
  34. package/dist/routes/routes-index.js.map +1 -1
  35. package/dist/storage/BucketStorage.d.ts +31 -1
  36. package/dist/storage/BucketStorage.js.map +1 -1
  37. package/dist/storage/mongo/MongoCompactor.d.ts +40 -0
  38. package/dist/storage/mongo/MongoCompactor.js +292 -0
  39. package/dist/storage/mongo/MongoCompactor.js.map +1 -0
  40. package/dist/storage/mongo/MongoSyncBucketStorage.d.ts +3 -2
  41. package/dist/storage/mongo/MongoSyncBucketStorage.js +19 -13
  42. package/dist/storage/mongo/MongoSyncBucketStorage.js.map +1 -1
  43. package/dist/storage/mongo/models.d.ts +5 -4
  44. package/dist/storage/mongo/models.js.map +1 -1
  45. package/dist/storage/mongo/util.d.ts +3 -0
  46. package/dist/storage/mongo/util.js +22 -0
  47. package/dist/storage/mongo/util.js.map +1 -1
  48. package/dist/sync/RequestTracker.js +2 -3
  49. package/dist/sync/RequestTracker.js.map +1 -1
  50. package/dist/sync/sync-index.d.ts +1 -0
  51. package/dist/sync/sync-index.js +1 -0
  52. package/dist/sync/sync-index.js.map +1 -1
  53. package/dist/sync/sync.js +20 -7
  54. package/dist/sync/sync.js.map +1 -1
  55. package/dist/sync/util.js.map +1 -1
  56. package/dist/util/config/collectors/config-collector.d.ts +12 -0
  57. package/dist/util/config/collectors/config-collector.js +43 -0
  58. package/dist/util/config/collectors/config-collector.js.map +1 -1
  59. package/dist/util/config/compound-config-collector.d.ts +3 -29
  60. package/dist/util/config/compound-config-collector.js +22 -69
  61. package/dist/util/config/compound-config-collector.js.map +1 -1
  62. package/package.json +6 -4
  63. package/src/entry/cli-entry.ts +2 -1
  64. package/src/entry/commands/compact-action.ts +54 -0
  65. package/src/entry/entry-index.ts +1 -0
  66. package/src/metrics/Metrics.ts +67 -2
  67. package/src/replication/WalStream.ts +6 -10
  68. package/src/routes/configure-fastify.ts +102 -0
  69. package/src/routes/configure-rsocket.ts +59 -0
  70. package/src/routes/endpoints/socket-route.ts +6 -15
  71. package/src/routes/endpoints/sync-stream.ts +4 -5
  72. package/src/routes/route-register.ts +2 -2
  73. package/src/routes/router-socket.ts +5 -5
  74. package/src/routes/router.ts +2 -0
  75. package/src/routes/routes-index.ts +2 -0
  76. package/src/storage/BucketStorage.ts +36 -1
  77. package/src/storage/mongo/MongoCompactor.ts +371 -0
  78. package/src/storage/mongo/MongoSyncBucketStorage.ts +25 -14
  79. package/src/storage/mongo/models.ts +5 -4
  80. package/src/storage/mongo/util.ts +25 -0
  81. package/src/sync/RequestTracker.ts +3 -3
  82. package/src/sync/sync-index.ts +1 -0
  83. package/src/sync/sync.ts +21 -7
  84. package/src/sync/util.ts +1 -0
  85. package/src/util/config/collectors/config-collector.ts +48 -0
  86. package/src/util/config/compound-config-collector.ts +23 -87
  87. package/test/src/__snapshots__/sync.test.ts.snap +85 -0
  88. package/test/src/bucket_validation.test.ts +142 -0
  89. package/test/src/bucket_validation.ts +116 -0
  90. package/test/src/compacting.test.ts +207 -0
  91. package/test/src/data_storage.test.ts +19 -60
  92. package/test/src/slow_tests.test.ts +144 -102
  93. package/test/src/sync.test.ts +169 -29
  94. package/test/src/util.ts +71 -13
  95. package/test/src/wal_stream.test.ts +21 -16
  96. package/test/src/wal_stream_utils.ts +13 -4
  97. package/tsconfig.tsbuildinfo +1 -1
@@ -406,7 +406,7 @@ WHERE oid = $1::regclass`,
406
406
  await batch.save({ tag: 'insert', sourceTable: table, before: undefined, after: record });
407
407
  }
408
408
  at += rows.length;
409
- container.getImplementation(Metrics).rows_replicated_total.add(rows.length);
409
+ Metrics.getInstance().rows_replicated_total.add(rows.length);
410
410
 
411
411
  await touch();
412
412
  }
@@ -492,21 +492,19 @@ WHERE oid = $1::regclass`,
492
492
  return null;
493
493
  }
494
494
 
495
- const metrics = container.getImplementation(Metrics);
496
-
497
495
  if (msg.tag == 'insert') {
498
- metrics.rows_replicated_total.add(1);
496
+ Metrics.getInstance().rows_replicated_total.add(1);
499
497
  const baseRecord = util.constructAfterRecord(msg);
500
498
  return await batch.save({ tag: 'insert', sourceTable: table, before: undefined, after: baseRecord });
501
499
  } else if (msg.tag == 'update') {
502
- metrics.rows_replicated_total.add(1);
500
+ Metrics.getInstance().rows_replicated_total.add(1);
503
501
  // "before" may be null if the replica id columns are unchanged
504
502
  // It's fine to treat that the same as an insert.
505
503
  const before = util.constructBeforeRecord(msg);
506
504
  const after = util.constructAfterRecord(msg);
507
505
  return await batch.save({ tag: 'update', sourceTable: table, before: before, after: after });
508
506
  } else if (msg.tag == 'delete') {
509
- metrics.rows_replicated_total.add(1);
507
+ Metrics.getInstance().rows_replicated_total.add(1);
510
508
  const before = util.constructBeforeRecord(msg)!;
511
509
 
512
510
  return await batch.save({ tag: 'delete', sourceTable: table, before: before, after: undefined });
@@ -557,8 +555,6 @@ WHERE oid = $1::regclass`,
557
555
  // Auto-activate as soon as initial replication is done
558
556
  await this.storage.autoActivate();
559
557
 
560
- const metrics = container.getImplementation(Metrics);
561
-
562
558
  await this.storage.startBatch({}, async (batch) => {
563
559
  // Replication never starts in the middle of a transaction
564
560
  let inTx = false;
@@ -581,7 +577,7 @@ WHERE oid = $1::regclass`,
581
577
  } else if (msg.tag == 'begin') {
582
578
  inTx = true;
583
579
  } else if (msg.tag == 'commit') {
584
- metrics.transactions_replicated_total.add(1);
580
+ Metrics.getInstance().transactions_replicated_total.add(1);
585
581
  inTx = false;
586
582
  await batch.commit(msg.lsn!);
587
583
  await this.ack(msg.lsn!, replicationStream);
@@ -606,7 +602,7 @@ WHERE oid = $1::regclass`,
606
602
  }
607
603
  }
608
604
 
609
- metrics.chunks_replicated_total.add(1);
605
+ Metrics.getInstance().chunks_replicated_total.add(1);
610
606
  }
611
607
  });
612
608
  }
@@ -0,0 +1,102 @@
1
+ import type fastify from 'fastify';
2
+ import { registerFastifyRoutes } from './route-register.js';
3
+
4
+ import * as system from '../system/system-index.js';
5
+
6
+ import { ADMIN_ROUTES } from './endpoints/admin.js';
7
+ import { CHECKPOINT_ROUTES } from './endpoints/checkpointing.js';
8
+ import { DEV_ROUTES } from './endpoints/dev.js';
9
+ import { SYNC_RULES_ROUTES } from './endpoints/sync-rules.js';
10
+ import { SYNC_STREAM_ROUTES } from './endpoints/sync-stream.js';
11
+ import { createRequestQueueHook, CreateRequestQueueParams } from './hooks.js';
12
+ import { RouteDefinition } from './router.js';
13
+
14
+ /**
15
+ * A list of route definitions to be registered as endpoints.
16
+ * Supplied concurrency limits will be applied to the grouped routes.
17
+ */
18
+ export type RouteRegistrationOptions = {
19
+ routes: RouteDefinition[];
20
+ queueOptions: CreateRequestQueueParams;
21
+ };
22
+
23
+ /**
24
+ * HTTP routes separated by API and Sync stream categories.
25
+ * This allows for separate concurrency limits.
26
+ */
27
+ export type RouteDefinitions = {
28
+ api?: Partial<RouteRegistrationOptions>;
29
+ syncStream?: Partial<RouteRegistrationOptions>;
30
+ };
31
+
32
+ export type FastifyServerConfig = {
33
+ system: system.CorePowerSyncSystem;
34
+ routes?: RouteDefinitions;
35
+ };
36
+
37
+ export const DEFAULT_ROUTE_OPTIONS = {
38
+ api: {
39
+ routes: [...ADMIN_ROUTES, ...CHECKPOINT_ROUTES, ...DEV_ROUTES, ...SYNC_RULES_ROUTES],
40
+ queueOptions: {
41
+ concurrency: 10,
42
+ max_queue_depth: 20
43
+ }
44
+ },
45
+ syncStream: {
46
+ routes: [...SYNC_STREAM_ROUTES],
47
+ queueOptions: {
48
+ concurrency: 200,
49
+ max_queue_depth: 0
50
+ }
51
+ }
52
+ };
53
+
54
+ /**
55
+ * Registers default routes on a Fastify server. Consumers can optionally configure
56
+ * concurrency queue limits or override routes.
57
+ */
58
+ export function configureFastifyServer(server: fastify.FastifyInstance, options: FastifyServerConfig) {
59
+ const { system, routes = DEFAULT_ROUTE_OPTIONS } = options;
60
+ /**
61
+ * Fastify creates an encapsulated context for each `.register` call.
62
+ * Creating a separate context here to separate the concurrency limits for Admin APIs
63
+ * and Sync Streaming routes.
64
+ * https://github.com/fastify/fastify/blob/main/docs/Reference/Encapsulation.md
65
+ */
66
+ server.register(async function (childContext) {
67
+ registerFastifyRoutes(
68
+ childContext,
69
+ async () => {
70
+ return {
71
+ user_id: undefined,
72
+ system: system
73
+ };
74
+ },
75
+ routes.api?.routes ?? DEFAULT_ROUTE_OPTIONS.api.routes
76
+ );
77
+ // Limit the active concurrent requests
78
+ childContext.addHook(
79
+ 'onRequest',
80
+ createRequestQueueHook(routes.api?.queueOptions ?? DEFAULT_ROUTE_OPTIONS.api.queueOptions)
81
+ );
82
+ });
83
+
84
+ // Create a separate context for concurrency queueing
85
+ server.register(async function (childContext) {
86
+ registerFastifyRoutes(
87
+ childContext,
88
+ async () => {
89
+ return {
90
+ user_id: undefined,
91
+ system: system
92
+ };
93
+ },
94
+ routes.syncStream?.routes ?? DEFAULT_ROUTE_OPTIONS.syncStream.routes
95
+ );
96
+ // Limit the active concurrent requests
97
+ childContext.addHook(
98
+ 'onRequest',
99
+ createRequestQueueHook(routes.syncStream?.queueOptions ?? DEFAULT_ROUTE_OPTIONS.syncStream.queueOptions)
100
+ );
101
+ });
102
+ }
@@ -0,0 +1,59 @@
1
+ import { deserialize } from 'bson';
2
+ import * as http from 'http';
3
+
4
+ import { errors, logger } from '@powersync/lib-services-framework';
5
+ import { ReactiveSocketRouter, RSocketRequestMeta } from '@powersync/service-rsocket-router';
6
+
7
+ import { CorePowerSyncSystem } from '../system/CorePowerSyncSystem.js';
8
+ import { generateContext, getTokenFromHeader } from './auth.js';
9
+ import { syncStreamReactive } from './endpoints/socket-route.js';
10
+ import { RSocketContextMeta, SocketRouteGenerator } from './router-socket.js';
11
+ import { Context } from './router.js';
12
+
13
+ export type RSockerRouterConfig = {
14
+ system: CorePowerSyncSystem;
15
+ server: http.Server;
16
+ routeGenerators?: SocketRouteGenerator[];
17
+ };
18
+
19
+ export const DEFAULT_SOCKET_ROUTES = [syncStreamReactive];
20
+
21
+ export function configureRSocket(router: ReactiveSocketRouter<Context>, options: RSockerRouterConfig) {
22
+ const { routeGenerators = DEFAULT_SOCKET_ROUTES, server, system } = options;
23
+
24
+ router.applyWebSocketEndpoints(server, {
25
+ contextProvider: async (data: Buffer) => {
26
+ const { token } = RSocketContextMeta.decode(deserialize(data) as any);
27
+
28
+ if (!token) {
29
+ throw new errors.AuthorizationError('No token provided');
30
+ }
31
+
32
+ try {
33
+ const extracted_token = getTokenFromHeader(token);
34
+ if (extracted_token != null) {
35
+ const { context, errors: token_errors } = await generateContext(system, extracted_token);
36
+ if (context?.token_payload == null) {
37
+ throw new errors.AuthorizationError(token_errors ?? 'Authentication required');
38
+ }
39
+ return {
40
+ token,
41
+ ...context,
42
+ token_errors: token_errors,
43
+ system
44
+ };
45
+ } else {
46
+ throw new errors.AuthorizationError('No token provided');
47
+ }
48
+ } catch (ex) {
49
+ logger.error(ex);
50
+ throw ex;
51
+ }
52
+ },
53
+ endpoints: routeGenerators.map((generator) => generator(router)),
54
+ metaDecoder: async (meta: Buffer) => {
55
+ return RSocketRequestMeta.decode(deserialize(meta) as any);
56
+ },
57
+ payloadDecoder: async (rawData?: Buffer) => rawData && deserialize(rawData)
58
+ });
59
+ }
@@ -1,22 +1,15 @@
1
- import { container, errors, logger, schema } from '@powersync/lib-services-framework';
1
+ import { errors, logger, schema } from '@powersync/lib-services-framework';
2
2
  import { RequestParameters } from '@powersync/service-sync-rules';
3
3
  import { serialize } from 'bson';
4
4
 
5
5
  import { Metrics } from '../../metrics/Metrics.js';
6
- import { streamResponse } from '../../sync/sync.js';
6
+ import * as sync from '../../sync/sync-index.js';
7
7
  import * as util from '../../util/util-index.js';
8
8
  import { SocketRouteGenerator } from '../router-socket.js';
9
9
  import { SyncRoutes } from './sync-stream.js';
10
- import { RequestTracker } from '../../sync/RequestTracker.js';
11
10
 
12
11
  export const syncStreamReactive: SocketRouteGenerator = (router) =>
13
12
  router.reactiveStream<util.StreamingSyncRequest, any>(SyncRoutes.STREAM, {
14
- authorize: ({ context }) => {
15
- return {
16
- authorized: !!context.token_payload,
17
- errors: ['Authentication required'].concat(context.token_errors ?? [])
18
- };
19
- },
20
13
  validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
21
14
  handler: async ({ context, params, responder, observer, initialN }) => {
22
15
  const { system } = context;
@@ -66,12 +59,10 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
66
59
  observer.triggerCancel();
67
60
  });
68
61
 
69
- const metrics = container.getImplementation(Metrics);
70
-
71
- metrics.concurrent_connections.add(1);
72
- const tracker = new RequestTracker();
62
+ Metrics.getInstance().concurrent_connections.add(1);
63
+ const tracker = new sync.RequestTracker();
73
64
  try {
74
- for await (const data of streamResponse({
65
+ for await (const data of sync.streamResponse({
75
66
  storage,
76
67
  params: {
77
68
  ...params,
@@ -136,7 +127,7 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
136
127
  operations_synced: tracker.operationsSynced,
137
128
  data_synced_bytes: tracker.dataSyncedBytes
138
129
  });
139
- metrics.concurrent_connections.add(-1);
130
+ Metrics.getInstance().concurrent_connections.add(-1);
140
131
  }
141
132
  }
142
133
  });
@@ -1,4 +1,4 @@
1
- import { container, errors, logger, router, schema } from '@powersync/lib-services-framework';
1
+ import { errors, logger, router, schema } from '@powersync/lib-services-framework';
2
2
  import { RequestParameters } from '@powersync/service-sync-rules';
3
3
  import { Readable } from 'stream';
4
4
 
@@ -43,11 +43,10 @@ export const syncStreamed = routeDefinition({
43
43
  description: 'No sync rules available'
44
44
  });
45
45
  }
46
- const metrics = container.getImplementation(Metrics);
47
46
  const controller = new AbortController();
48
47
  const tracker = new RequestTracker();
49
48
  try {
50
- metrics.concurrent_connections.add(1);
49
+ Metrics.getInstance().concurrent_connections.add(1);
51
50
  const stream = Readable.from(
52
51
  sync.transformToBytesTracked(
53
52
  sync.ndjson(
@@ -90,7 +89,7 @@ export const syncStreamed = routeDefinition({
90
89
  data: stream,
91
90
  afterSend: async () => {
92
91
  controller.abort();
93
- metrics.concurrent_connections.add(-1);
92
+ Metrics.getInstance().concurrent_connections.add(-1);
94
93
  logger.info(`Sync stream complete`, {
95
94
  user_id: syncParams.user_id,
96
95
  operations_synced: tracker.operationsSynced,
@@ -100,7 +99,7 @@ export const syncStreamed = routeDefinition({
100
99
  });
101
100
  } catch (ex) {
102
101
  controller.abort();
103
- metrics.concurrent_connections.add(-1);
102
+ Metrics.getInstance().concurrent_connections.add(-1);
104
103
  }
105
104
  }
106
105
  });
@@ -1,6 +1,6 @@
1
- import fastify from 'fastify';
1
+ import type fastify from 'fastify';
2
2
 
3
- import { errors, router, HTTPMethod, logger } from '@powersync/lib-services-framework';
3
+ import { errors, HTTPMethod, logger, router } from '@powersync/lib-services-framework';
4
4
  import { Context, ContextProvider, RequestEndpoint, RequestEndpointHandlerPayload } from './router.js';
5
5
 
6
6
  export type FastifyEndpoint<I, O, C> = RequestEndpoint<I, O, C> & {
@@ -1,13 +1,13 @@
1
+ import { IReactiveStream, ReactiveSocketRouter } from '@powersync/service-rsocket-router';
1
2
  import * as t from 'ts-codec';
2
- import { ReactiveSocketRouter, IReactiveStream } from '@powersync/service-rsocket-router';
3
3
 
4
4
  import { Context } from './router.js';
5
5
 
6
- export const RSocketContextMeta = t.object({
7
- token: t.string
8
- });
9
-
10
6
  /**
11
7
  * Creates a socket route handler given a router instance
12
8
  */
13
9
  export type SocketRouteGenerator = (router: ReactiveSocketRouter<Context>) => IReactiveStream;
10
+
11
+ export const RSocketContextMeta = t.object({
12
+ token: t.string
13
+ });
@@ -36,6 +36,8 @@ export type RequestEndpointHandlerPayload<
36
36
  request: Request;
37
37
  };
38
38
 
39
+ export type RouteDefinition<I = any, O = any> = RequestEndpoint<I, O>;
40
+
39
41
  /**
40
42
  * Helper function for making generics work well when defining routes
41
43
  */
@@ -1,4 +1,6 @@
1
1
  export * as auth from './auth.js';
2
+ export * from './configure-fastify.js';
3
+ export * from './configure-rsocket.js';
2
4
  export * as endpoints from './endpoints/route-endpoints-index.js';
3
5
  export * as hooks from './hooks.js';
4
6
  export * from './route-register.js';
@@ -228,7 +228,7 @@ export interface SyncRulesBucketStorage {
228
228
  checkpoint: util.OpId,
229
229
  dataBuckets: Map<string, string>,
230
230
  options?: BucketDataBatchOptions
231
- ): AsyncIterable<util.SyncBucketData>;
231
+ ): AsyncIterable<SyncBucketDataBatch>;
232
232
 
233
233
  /**
234
234
  * Compute checksums for a given list of buckets.
@@ -266,6 +266,8 @@ export interface SyncRulesBucketStorage {
266
266
  * Errors are cleared on commit.
267
267
  */
268
268
  reportError(e: any): Promise<void>;
269
+
270
+ compact(options?: CompactOptions): Promise<void>;
269
271
  }
270
272
 
271
273
  export interface SyncRuleStatus {
@@ -388,6 +390,11 @@ export interface SaveDelete {
388
390
  after?: undefined;
389
391
  }
390
392
 
393
+ export interface SyncBucketDataBatch {
394
+ batch: util.SyncBucketData;
395
+ targetOp: bigint | null;
396
+ }
397
+
391
398
  export function mergeToast(record: ToastableSqliteRow, persisted: ToastableSqliteRow): ToastableSqliteRow {
392
399
  const newRecord: ToastableSqliteRow = {};
393
400
  for (let key in record) {
@@ -399,3 +406,31 @@ export function mergeToast(record: ToastableSqliteRow, persisted: ToastableSqlit
399
406
  }
400
407
  return newRecord;
401
408
  }
409
+
410
+ export interface CompactOptions {
411
+ /**
412
+ * Heap memory limit for the compact process.
413
+ *
414
+ * Add around 64MB to this to determine the "--max-old-space-size" argument.
415
+ * Add another 80MB to get RSS usage / memory limits.
416
+ */
417
+ memoryLimitMB?: number;
418
+
419
+ /**
420
+ * If specified, ignore any operations newer than this when compacting.
421
+ *
422
+ * This is primarily for tests, where we want to test compacting at a specific
423
+ * point.
424
+ *
425
+ * This can also be used to create a "safe buffer" of recent operations that should
426
+ * not be compacted, to avoid invalidating checkpoints in use.
427
+ */
428
+ maxOpId?: bigint;
429
+
430
+ /**
431
+ * If specified, compact only the specific buckets.
432
+ *
433
+ * If not specified, compacts all buckets.
434
+ */
435
+ compactBuckets?: string[];
436
+ }