@powersync/service-core 0.0.0-dev-20251111070830 → 0.0.0-dev-20251120150014

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.
@@ -5,10 +5,11 @@ import { Readable } from 'stream';
5
5
  import * as sync from '../../sync/sync-index.js';
6
6
  import * as util from '../../util/util-index.js';
7
7
 
8
+ import { APIMetric, event_types } from '@powersync/service-types';
8
9
  import { authUser } from '../auth.js';
9
10
  import { routeDefinition } from '../router.js';
10
- import { APIMetric, event_types } from '@powersync/service-types';
11
11
 
12
+ import { formatParamsForLogging } from '../../util/param-logging.js';
12
13
  import { maybeCompressResponseStream } from '../compression.js';
13
14
 
14
15
  export enum SyncRoutes {
@@ -42,7 +43,10 @@ export const syncStreamed = routeDefinition({
42
43
  user_agent: userAgent,
43
44
  client_id: clientId,
44
45
  user_id: payload.context.user_id,
45
- bson: useBson
46
+ bson: useBson,
47
+ app_metadata: payload.params.applicationMetadata
48
+ ? formatParamsForLogging(payload.params.applicationMetadata)
49
+ : undefined
46
50
  };
47
51
  const sdkData: event_types.ConnectedUserData & event_types.ClientConnectionEventData = {
48
52
  client_id: clientId ?? '',
@@ -75,6 +79,7 @@ export const syncStreamed = routeDefinition({
75
79
 
76
80
  const controller = new AbortController();
77
81
  const tracker = new sync.RequestTracker(metricsEngine);
82
+
78
83
  try {
79
84
  metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(1);
80
85
  service_context.eventsEngine.emit(event_types.EventsEngineEventType.SDK_CONNECT_EVENT, sdkData);
@@ -133,7 +138,7 @@ export const syncStreamed = routeDefinition({
133
138
  return new router.RouterResponse({
134
139
  status: 200,
135
140
  headers: {
136
- 'Content-Type': useBson ? concatenatedBsonContentType : ndJsonContentType,
141
+ 'Content-Type': 'text/event-stream', // useBson ? concatenatedBsonContentType : ndJsonContentType,
137
142
  ...encodingHeaders
138
143
  },
139
144
  data: stream,
@@ -217,6 +217,11 @@ export interface CompactOptions {
217
217
  /** Minimum of 1 */
218
218
  moveBatchQueryLimit?: number;
219
219
 
220
+ /**
221
+ * Minimum of 1, default of 10.
222
+ */
223
+ minBucketChanges?: number;
224
+
220
225
  /**
221
226
  * Internal/testing use: Cache size for compacting parameters.
222
227
  */
package/src/sync/sync.ts CHANGED
@@ -1,11 +1,5 @@
1
1
  import { JSONBig, JsonContainer } from '@powersync/service-jsonbig';
2
- import {
3
- BucketDescription,
4
- BucketPriority,
5
- RequestJwtPayload,
6
- RequestParameters,
7
- SqlSyncRules
8
- } from '@powersync/service-sync-rules';
2
+ import { BucketDescription, BucketPriority, RequestJwtPayload } from '@powersync/service-sync-rules';
9
3
 
10
4
  import { AbortError } from 'ix/aborterror.js';
11
5
 
@@ -14,11 +8,12 @@ import * as storage from '../storage/storage-index.js';
14
8
  import * as util from '../util/util-index.js';
15
9
 
16
10
  import { Logger, logger as defaultLogger } from '@powersync/lib-services-framework';
17
- import { BucketChecksumState, CheckpointLine, VersionedSyncRules } from './BucketChecksumState.js';
18
11
  import { mergeAsyncIterables } from '../streams/streams-index.js';
19
- import { acquireSemaphoreAbortable, settledPromise, tokenStream, TokenStreamOptions } from './util.js';
20
- import { SyncContext } from './SyncContext.js';
12
+ import { formatParamsForLogging } from '../util/param-logging.js';
13
+ import { BucketChecksumState, CheckpointLine, VersionedSyncRules } from './BucketChecksumState.js';
21
14
  import { OperationsSentStats, RequestTracker, statsForBatch } from './RequestTracker.js';
15
+ import { SyncContext } from './SyncContext.js';
16
+ import { TokenStreamOptions, acquireSemaphoreAbortable, settledPromise, tokenStream } from './util.js';
22
17
 
23
18
  export interface SyncStreamParameters {
24
19
  syncContext: SyncContext;
@@ -53,6 +48,11 @@ export async function* streamResponse(
53
48
  } = options;
54
49
  const logger = options.logger ?? defaultLogger;
55
50
 
51
+ logger.info('Sync stream started', {
52
+ client_params: params.parameters ? formatParamsForLogging(params.parameters) : undefined,
53
+ streams: params.streams?.subscriptions.map((subscription) => subscription.stream)
54
+ });
55
+
56
56
  // We also need to be able to abort, so we create our own controller.
57
57
  const controller = new AbortController();
58
58
  if (signal) {
@@ -0,0 +1,40 @@
1
+ export type ParamLoggingFormatOptions = {
2
+ maxKeyCount: number;
3
+ maxStringLength: number;
4
+ };
5
+
6
+ export const DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS: ParamLoggingFormatOptions = {
7
+ maxKeyCount: 10,
8
+ maxStringLength: 20
9
+ };
10
+
11
+ export function formatParamsForLogging(params: Record<string, any>, options: Partial<ParamLoggingFormatOptions> = {}) {
12
+ const {
13
+ maxStringLength = DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS.maxStringLength,
14
+ maxKeyCount = DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS.maxKeyCount
15
+ } = options;
16
+
17
+ function trimString(value: string): string {
18
+ if (value.length > maxStringLength) {
19
+ return value.slice(0, maxStringLength - 3) + '...';
20
+ }
21
+ return value;
22
+ }
23
+
24
+ return Object.fromEntries(
25
+ Object.entries(params).map(([key, value], index) => {
26
+ if (index == maxKeyCount) {
27
+ return ['[...]', '[...]'];
28
+ }
29
+
30
+ if (index > maxKeyCount) {
31
+ return [];
32
+ }
33
+
34
+ if (typeof value === 'string') {
35
+ return [key, trimString(value)];
36
+ }
37
+ return [key, trimString(JSON.stringify(value))];
38
+ })
39
+ );
40
+ }
@@ -1,6 +1,6 @@
1
- import * as t from 'ts-codec';
2
- import { BucketPriority, SqliteJsonRow } from '@powersync/service-sync-rules';
3
1
  import { JsonContainer } from '@powersync/service-jsonbig';
2
+ import { BucketPriority, SqliteJsonRow } from '@powersync/service-sync-rules';
3
+ import * as t from 'ts-codec';
4
4
 
5
5
  export const BucketRequest = t.object({
6
6
  name: t.string,
@@ -81,6 +81,11 @@ export const StreamingSyncRequest = t.object({
81
81
  */
82
82
  parameters: t.record(t.any).optional(),
83
83
 
84
+ /**
85
+ * Application metadata to be used in logging.
86
+ */
87
+ applicationMetadata: t.record(t.string).optional(),
88
+
84
89
  /**
85
90
  * Unique client id.
86
91
  */
@@ -1,10 +1,12 @@
1
1
  import { BasicRouterRequest, Context, SyncRulesBucketStorage } from '@/index.js';
2
- import { logger, RouterResponse, ServiceError } from '@powersync/lib-services-framework';
2
+ import { RouterResponse, ServiceError, logger } from '@powersync/lib-services-framework';
3
3
  import { SqlSyncRules } from '@powersync/service-sync-rules';
4
4
  import { Readable, Writable } from 'stream';
5
5
  import { pipeline } from 'stream/promises';
6
6
  import { describe, expect, it } from 'vitest';
7
+ import winston from 'winston';
7
8
  import { syncStreamed } from '../../../src/routes/endpoints/sync-stream.js';
9
+ import { DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS, formatParamsForLogging } from '../../../src/util/param-logging.js';
8
10
  import { mockServiceContext } from './mocks.js';
9
11
 
10
12
  describe('Stream Route', () => {
@@ -77,6 +79,88 @@ describe('Stream Route', () => {
77
79
  const r = await drainWithTimeout(stream).catch((error) => error);
78
80
  expect(r.message).toContain('Simulated storage error');
79
81
  });
82
+
83
+ it('logs the application metadata', async () => {
84
+ const storage = {
85
+ getParsedSyncRules() {
86
+ return new SqlSyncRules('bucket_definitions: {}');
87
+ },
88
+ watchCheckpointChanges: async function* (options) {
89
+ throw new Error('Simulated storage error');
90
+ }
91
+ } as Partial<SyncRulesBucketStorage>;
92
+ const serviceContext = mockServiceContext(storage);
93
+
94
+ // Create a custom format to capture log info objects (which include defaultMeta)
95
+ // Winston merges defaultMeta into the info object during formatting
96
+ const capturedLogs: any[] = [];
97
+ const captureFormat = winston.format((info) => {
98
+ // Capture the info object which includes defaultMeta merged in
99
+ capturedLogs.push({ ...info });
100
+ return info;
101
+ });
102
+
103
+ // Create a test logger with the capture format
104
+ const testLogger = winston.createLogger({
105
+ format: winston.format.combine(captureFormat(), winston.format.json()),
106
+ transports: [new winston.transports.Console()]
107
+ });
108
+
109
+ const context: Context = {
110
+ logger: testLogger,
111
+ service_context: serviceContext,
112
+ token_payload: {
113
+ exp: new Date().getTime() / 1000 + 10000,
114
+ iat: new Date().getTime() / 1000 - 10000,
115
+ sub: 'test-user'
116
+ }
117
+ };
118
+
119
+ const request: BasicRouterRequest = {
120
+ headers: {
121
+ 'accept-encoding': 'gzip'
122
+ },
123
+ hostname: '',
124
+ protocol: 'http'
125
+ };
126
+
127
+ const inputMeta = {
128
+ test: 'test',
129
+ long_meta: 'a'.repeat(1000)
130
+ };
131
+
132
+ const response = await (syncStreamed.handler({
133
+ context,
134
+ params: {
135
+ applicationMetadata: inputMeta,
136
+ parameters: {
137
+ user_name: 'bob'
138
+ }
139
+ },
140
+ request
141
+ }) as Promise<RouterResponse>);
142
+ expect(response.status).toEqual(200);
143
+ const stream = response.data as Readable;
144
+ const r = await drainWithTimeout(stream).catch((error) => error);
145
+ expect(r.message).toContain('Simulated storage error');
146
+
147
+ // Find the "Sync stream started" log entry
148
+ const syncStartedLog = capturedLogs.find((log) => log.message === 'Sync stream started');
149
+ expect(syncStartedLog).toBeDefined();
150
+
151
+ // Verify that app_metadata from defaultMeta is present in the log
152
+ expect(syncStartedLog?.app_metadata).toBeDefined();
153
+ expect(syncStartedLog?.app_metadata).toEqual(formatParamsForLogging(inputMeta));
154
+ // Should trim long metadata
155
+ expect(syncStartedLog?.app_metadata.long_meta.length).toEqual(
156
+ DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS.maxStringLength
157
+ );
158
+
159
+ // Verify the explicit log parameters
160
+ expect(syncStartedLog?.client_params).toEqual({
161
+ user_name: 'bob'
162
+ });
163
+ });
80
164
  });
81
165
  });
82
166