@powersync/common 0.0.0-dev-20250625140957 → 0.0.0-dev-20250701144132

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.
@@ -90,5 +90,5 @@ export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener
90
90
  /**
91
91
  * Invokes the `powersync_control` function for the sync client.
92
92
  */
93
- control(op: PowerSyncControlCommand, payload: string | ArrayBuffer | null): Promise<string>;
93
+ control(op: PowerSyncControlCommand, payload: string | Uint8Array | null): Promise<string>;
94
94
  }
@@ -56,7 +56,7 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
56
56
  * Set a target checkpoint.
57
57
  */
58
58
  setTargetCheckpoint(checkpoint: Checkpoint): Promise<void>;
59
- control(op: PowerSyncControlCommand, payload: string | ArrayBuffer | null): Promise<string>;
59
+ control(op: PowerSyncControlCommand, payload: string | Uint8Array | ArrayBuffer | null): Promise<string>;
60
60
  hasMigratedSubkeys(): Promise<boolean>;
61
61
  migrateToFixedSubkeys(): Promise<void>;
62
62
  static _subkeyMigrationKey: string;
@@ -1,5 +1,4 @@
1
1
  import type { BSON } from 'bson';
2
- import { Buffer } from 'buffer';
3
2
  import { type fetch } from 'cross-fetch';
4
3
  import Logger, { ILogger } from 'js-logger';
5
4
  import { DataStream } from '../../../utils/DataStream.js';
@@ -133,7 +132,7 @@ export declare abstract class AbstractRemote {
133
132
  * @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
134
133
  * (required for compatibility with older sync services).
135
134
  */
136
- socketStreamRaw<T>(options: SocketSyncStreamOptions, map: (buffer: Buffer) => T, bson?: typeof BSON): Promise<DataStream>;
135
+ socketStreamRaw<T>(options: SocketSyncStreamOptions, map: (buffer: Uint8Array) => T, bson?: typeof BSON): Promise<DataStream<T>>;
137
136
  /**
138
137
  * Connects to the sync/stream http endpoint, parsing lines as JSON.
139
138
  */
@@ -10,8 +10,12 @@ const POWERSYNC_JS_VERSION = PACKAGE.version;
10
10
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
11
11
  // Keep alive message is sent every period
12
12
  const KEEP_ALIVE_MS = 20_000;
13
- // The ACK must be received in this period
14
- const KEEP_ALIVE_LIFETIME_MS = 30_000;
13
+ // One message of any type must be received in this period.
14
+ const SOCKET_TIMEOUT_MS = 30_000;
15
+ // One keepalive message must be received in this period.
16
+ // If there is a backlog of messages (for example on slow connections), keepalive messages could be delayed
17
+ // significantly. Therefore this is longer than the socket timeout.
18
+ const KEEP_ALIVE_LIFETIME_MS = 90_000;
15
19
  export const DEFAULT_REMOTE_LOGGER = Logger.get('PowerSyncRemote');
16
20
  export var FetchStrategy;
17
21
  (function (FetchStrategy) {
@@ -208,12 +212,25 @@ export class AbstractRemote {
208
212
  // headers with websockets on web. The browser userAgent is however added
209
213
  // automatically as a header.
210
214
  const userAgent = this.getUserAgent();
215
+ let keepAliveTimeout;
216
+ const resetTimeout = () => {
217
+ clearTimeout(keepAliveTimeout);
218
+ keepAliveTimeout = setTimeout(() => {
219
+ this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
220
+ stream.close();
221
+ }, SOCKET_TIMEOUT_MS);
222
+ };
223
+ resetTimeout();
211
224
  const url = this.options.socketUrlTransformer(request.url);
212
225
  const connector = new RSocketConnector({
213
226
  transport: new WebsocketClientTransport({
214
227
  url,
215
228
  wsCreator: (url) => {
216
- return this.createSocket(url);
229
+ const socket = this.createSocket(url);
230
+ socket.addEventListener('message', (event) => {
231
+ resetTimeout();
232
+ });
233
+ return socket;
217
234
  }
218
235
  }),
219
236
  setup: {
@@ -236,16 +253,20 @@ export class AbstractRemote {
236
253
  }
237
254
  catch (ex) {
238
255
  this.logger.error(`Failed to connect WebSocket`, ex);
256
+ clearTimeout(keepAliveTimeout);
239
257
  throw ex;
240
258
  }
259
+ resetTimeout();
241
260
  const stream = new DataStream({
242
261
  logger: this.logger,
243
262
  pressure: {
244
263
  lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
245
- }
264
+ },
265
+ mapLine: map
246
266
  });
247
267
  let socketIsClosed = false;
248
268
  const closeSocket = () => {
269
+ clearTimeout(keepAliveTimeout);
249
270
  if (socketIsClosed) {
250
271
  return;
251
272
  }
@@ -307,7 +328,7 @@ export class AbstractRemote {
307
328
  if (!data) {
308
329
  return;
309
330
  }
310
- stream.enqueueData(map(data));
331
+ stream.enqueueData(data);
311
332
  },
312
333
  onComplete: () => {
313
334
  stream.close();
@@ -418,7 +439,8 @@ export class AbstractRemote {
418
439
  const decoder = new TextDecoder();
419
440
  let buffer = '';
420
441
  const stream = new DataStream({
421
- logger: this.logger
442
+ logger: this.logger,
443
+ mapLine: mapLine
422
444
  });
423
445
  const l = stream.registerListener({
424
446
  lowWater: async () => {
@@ -429,7 +451,7 @@ export class AbstractRemote {
429
451
  if (done) {
430
452
  const remaining = buffer.trim();
431
453
  if (remaining.length != 0) {
432
- stream.enqueueData(mapLine(remaining));
454
+ stream.enqueueData(remaining);
433
455
  }
434
456
  stream.close();
435
457
  await closeReader();
@@ -441,7 +463,7 @@ export class AbstractRemote {
441
463
  for (var i = 0; i < lines.length - 1; i++) {
442
464
  var l = lines[i].trim();
443
465
  if (l.length > 0) {
444
- stream.enqueueData(mapLine(l));
466
+ stream.enqueueData(l);
445
467
  didCompleteLine = true;
446
468
  }
447
469
  }
@@ -668,19 +668,35 @@ The next upload iteration will be delayed.`);
668
668
  data: instr.request
669
669
  };
670
670
  if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) {
671
- controlInvocations = await remote.postStreamRaw(syncOptions, (line) => ({
672
- command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
673
- payload: line
674
- }));
671
+ controlInvocations = await remote.postStreamRaw(syncOptions, (line) => {
672
+ if (typeof line == 'string') {
673
+ return {
674
+ command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
675
+ payload: line
676
+ };
677
+ }
678
+ else {
679
+ // Directly enqueued by us
680
+ return line;
681
+ }
682
+ });
675
683
  }
676
684
  else {
677
685
  controlInvocations = await remote.socketStreamRaw({
678
686
  ...syncOptions,
679
687
  fetchStrategy: resolvedOptions.fetchStrategy
680
- }, (buffer) => ({
681
- command: PowerSyncControlCommand.PROCESS_BSON_LINE,
682
- payload: buffer
683
- }));
688
+ }, (payload) => {
689
+ if (payload instanceof Uint8Array) {
690
+ return {
691
+ command: PowerSyncControlCommand.PROCESS_BSON_LINE,
692
+ payload: payload
693
+ };
694
+ }
695
+ else {
696
+ // Directly enqueued by us
697
+ return payload;
698
+ }
699
+ });
684
700
  }
685
701
  try {
686
702
  while (!controlInvocations.closed) {
@@ -1,6 +1,7 @@
1
1
  import { ILogger } from 'js-logger';
2
2
  import { BaseListener, BaseObserver } from './BaseObserver.js';
3
- export type DataStreamOptions = {
3
+ export type DataStreamOptions<ParsedData, SourceData> = {
4
+ mapLine?: (line: SourceData) => ParsedData;
4
5
  /**
5
6
  * Close the stream if any consumer throws an error
6
7
  */
@@ -28,13 +29,14 @@ export declare const DEFAULT_PRESSURE_LIMITS: {
28
29
  * native JS streams or async iterators.
29
30
  * This is handy for environments such as React Native which need polyfills for the above.
30
31
  */
31
- export declare class DataStream<Data extends any = any> extends BaseObserver<DataStreamListener<Data>> {
32
- protected options?: DataStreamOptions | undefined;
33
- dataQueue: Data[];
32
+ export declare class DataStream<ParsedData, SourceData = any> extends BaseObserver<DataStreamListener<ParsedData>> {
33
+ protected options?: DataStreamOptions<ParsedData, SourceData> | undefined;
34
+ dataQueue: SourceData[];
34
35
  protected isClosed: boolean;
35
36
  protected processingPromise: Promise<void> | null;
36
37
  protected logger: ILogger;
37
- constructor(options?: DataStreamOptions | undefined);
38
+ protected mapLine: (line: SourceData) => ParsedData;
39
+ constructor(options?: DataStreamOptions<ParsedData, SourceData> | undefined);
38
40
  get highWatermark(): number;
39
41
  get lowWatermark(): number;
40
42
  get closed(): boolean;
@@ -42,22 +44,18 @@ export declare class DataStream<Data extends any = any> extends BaseObserver<Dat
42
44
  /**
43
45
  * Enqueues data for the consumers to read
44
46
  */
45
- enqueueData(data: Data): void;
47
+ enqueueData(data: SourceData): void;
46
48
  /**
47
49
  * Reads data once from the data stream
48
50
  * @returns a Data payload or Null if the stream closed.
49
51
  */
50
- read(): Promise<Data | null>;
52
+ read(): Promise<ParsedData | null>;
51
53
  /**
52
54
  * Executes a callback for each data item in the stream
53
55
  */
54
- forEach(callback: DataStreamCallback<Data>): () => void;
56
+ forEach(callback: DataStreamCallback<ParsedData>): () => void;
55
57
  protected processQueue(): Promise<void>;
56
- /**
57
- * Creates a new data stream which is a map of the original
58
- */
59
- map<ReturnData>(callback: (data: Data) => ReturnData): DataStream<ReturnData>;
60
58
  protected hasDataReader(): boolean;
61
59
  protected _processQueue(): Promise<void>;
62
- protected iterateAsyncErrored(cb: (l: BaseListener) => Promise<void>): Promise<void>;
60
+ protected iterateAsyncErrored(cb: (l: Partial<DataStreamListener<ParsedData>>) => Promise<void>): Promise<void>;
63
61
  }
@@ -15,12 +15,14 @@ export class DataStream extends BaseObserver {
15
15
  isClosed;
16
16
  processingPromise;
17
17
  logger;
18
+ mapLine;
18
19
  constructor(options) {
19
20
  super();
20
21
  this.options = options;
21
22
  this.processingPromise = null;
22
23
  this.isClosed = false;
23
24
  this.dataQueue = [];
25
+ this.mapLine = options?.mapLine ?? ((line) => line);
24
26
  this.logger = options?.logger ?? Logger.get('DataStream');
25
27
  if (options?.closeOnError) {
26
28
  const l = this.registerListener({
@@ -110,22 +112,6 @@ export class DataStream extends BaseObserver {
110
112
  }
111
113
  return (this.processingPromise = this._processQueue());
112
114
  }
113
- /**
114
- * Creates a new data stream which is a map of the original
115
- */
116
- map(callback) {
117
- const stream = new DataStream(this.options);
118
- const l = this.registerListener({
119
- data: async (data) => {
120
- stream.enqueueData(callback(data));
121
- },
122
- closed: () => {
123
- stream.close();
124
- l?.();
125
- }
126
- });
127
- return stream;
128
- }
129
115
  hasDataReader() {
130
116
  return Array.from(this.listeners.values()).some((l) => !!l.data);
131
117
  }
@@ -136,7 +122,8 @@ export class DataStream extends BaseObserver {
136
122
  }
137
123
  if (this.dataQueue.length) {
138
124
  const data = this.dataQueue.shift();
139
- await this.iterateAsyncErrored(async (l) => l.data?.(data));
125
+ const mapped = this.mapLine(data);
126
+ await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
140
127
  }
141
128
  if (this.dataQueue.length <= this.lowWatermark) {
142
129
  await this.iterateAsyncErrored(async (l) => l.lowWater?.());
@@ -148,7 +135,7 @@ export class DataStream extends BaseObserver {
148
135
  }
149
136
  }
150
137
  async iterateAsyncErrored(cb) {
151
- for (let i of Array.from(this.listeners.values())) {
138
+ for (let i of this.listeners.values()) {
152
139
  try {
153
140
  await cb(i);
154
141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/common",
3
- "version": "0.0.0-dev-20250625140957",
3
+ "version": "0.0.0-dev-20250701144132",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -13,9 +13,8 @@
13
13
  "exports": {
14
14
  ".": {
15
15
  "import": "./dist/bundle.mjs",
16
- "require": "./dist/bundle.cjs",
17
- "types": "./lib/index.d.ts",
18
- "default": "./dist/bundle.mjs"
16
+ "default": "./dist/bundle.mjs",
17
+ "types": "./lib/index.d.ts"
19
18
  }
20
19
  },
21
20
  "author": "JOURNEYAPPS",