@powersync/common 0.0.0-dev-20240516143814 → 0.0.0-dev-20240606141637

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.
@@ -50,7 +50,7 @@ export interface WatchHandler {
50
50
  onError?: (error: Error) => void;
51
51
  }
52
52
  export interface WatchOnChangeHandler {
53
- onChange: (event: WatchOnChangeEvent) => void;
53
+ onChange: (event: WatchOnChangeEvent) => Promise<void> | void;
54
54
  onError?: (error: Error) => void;
55
55
  }
56
56
  export interface PowerSyncDBListener extends StreamingSyncImplementationListener {
@@ -13,6 +13,7 @@ import { CrudBatch } from './sync/bucket/CrudBatch';
13
13
  import { CrudEntry } from './sync/bucket/CrudEntry';
14
14
  import { CrudTransaction } from './sync/bucket/CrudTransaction';
15
15
  import { DEFAULT_CRUD_UPLOAD_THROTTLE_MS } from './sync/stream/AbstractStreamingSyncImplementation';
16
+ import { ControlledExecutor } from '../utils/ControlledExecutor';
16
17
  const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
17
18
  const DEFAULT_DISCONNECT_CLEAR_OPTIONS = {
18
19
  clearLocal: true
@@ -565,10 +566,13 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
565
566
  const watchedTables = new Set(resolvedOptions.tables ?? []);
566
567
  const changedTables = new Set();
567
568
  const throttleMs = resolvedOptions.throttleMs ?? DEFAULT_WATCH_THROTTLE_MS;
569
+ const executor = new ControlledExecutor(async (e) => {
570
+ await onChange(e);
571
+ });
568
572
  const flushTableUpdates = throttle(() => this.handleTableChanges(changedTables, watchedTables, (intersection) => {
569
573
  if (resolvedOptions?.signal?.aborted)
570
574
  return;
571
- onChange({ changedTables: intersection });
575
+ executor.schedule({ changedTables: intersection });
572
576
  }), throttleMs, { leading: false, trailing: true });
573
577
  const dispose = this.database.registerListener({
574
578
  tablesUpdated: async (update) => {
@@ -583,6 +587,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
583
587
  }
584
588
  });
585
589
  resolvedOptions.signal?.addEventListener('abort', () => {
590
+ executor.dispose();
586
591
  dispose();
587
592
  });
588
593
  return () => dispose();
@@ -2,4 +2,5 @@ export interface PowerSyncCredentials {
2
2
  endpoint: string;
3
3
  token: string;
4
4
  expiresAt?: Date;
5
+ params?: Record<string, string>;
5
6
  }
@@ -16,6 +16,16 @@ export type SyncStreamOptions = {
16
16
  abortSignal?: AbortSignal;
17
17
  fetchOptions?: Request;
18
18
  };
19
+ export type FetchImplementation = typeof fetch;
20
+ /**
21
+ * Class wrapper for providing a fetch implementation.
22
+ * The class wrapper is used to distinguish the fetchImplementation
23
+ * option in [AbstractRemoteOptions] from the general fetch method
24
+ * which is typeof "function"
25
+ */
26
+ export declare class FetchImplementationProvider {
27
+ getFetch(): FetchImplementation;
28
+ }
19
29
  export type AbstractRemoteOptions = {
20
30
  /**
21
31
  * Transforms the PowerSync base URL which might contain
@@ -28,7 +38,7 @@ export type AbstractRemoteOptions = {
28
38
  * Note that this usually needs to be bound to the global scope.
29
39
  * Binding should be done before passing here.
30
40
  */
31
- fetchImplementation: typeof fetch;
41
+ fetchImplementation: FetchImplementation | FetchImplementationProvider;
32
42
  };
33
43
  export declare const DEFAULT_REMOTE_OPTIONS: AbstractRemoteOptions;
34
44
  export declare abstract class AbstractRemote {
@@ -37,7 +47,11 @@ export declare abstract class AbstractRemote {
37
47
  protected credentials: PowerSyncCredentials | null;
38
48
  protected options: AbstractRemoteOptions;
39
49
  constructor(connector: RemoteConnector, logger?: ILogger, options?: Partial<AbstractRemoteOptions>);
40
- get fetch(): typeof globalThis.fetch;
50
+ /**
51
+ * @returns a fetch implementation (function)
52
+ * which can be called to perform fetch requests
53
+ */
54
+ get fetch(): FetchImplementation;
41
55
  getCredentials(): Promise<PowerSyncCredentials | null>;
42
56
  protected buildRequest(path: string): Promise<{
43
57
  url: string;
@@ -15,11 +15,22 @@ const KEEP_ALIVE_MS = 20_000;
15
15
  // The ACK must be received in this period
16
16
  const KEEP_ALIVE_LIFETIME_MS = 30_000;
17
17
  export const DEFAULT_REMOTE_LOGGER = Logger.get('PowerSyncRemote');
18
+ /**
19
+ * Class wrapper for providing a fetch implementation.
20
+ * The class wrapper is used to distinguish the fetchImplementation
21
+ * option in [AbstractRemoteOptions] from the general fetch method
22
+ * which is typeof "function"
23
+ */
24
+ export class FetchImplementationProvider {
25
+ getFetch() {
26
+ return fetch.bind(globalThis);
27
+ }
28
+ }
18
29
  export const DEFAULT_REMOTE_OPTIONS = {
19
30
  socketUrlTransformer: (url) => url.replace(/^https?:\/\//, function (match) {
20
31
  return match === 'https://' ? 'wss://' : 'ws://';
21
32
  }),
22
- fetchImplementation: fetch.bind(globalThis)
33
+ fetchImplementation: new FetchImplementationProvider()
23
34
  };
24
35
  export class AbstractRemote {
25
36
  connector;
@@ -34,8 +45,15 @@ export class AbstractRemote {
34
45
  ...(options ?? {})
35
46
  };
36
47
  }
48
+ /**
49
+ * @returns a fetch implementation (function)
50
+ * which can be called to perform fetch requests
51
+ */
37
52
  get fetch() {
38
- return this.options.fetchImplementation;
53
+ const { fetchImplementation } = this.options;
54
+ return fetchImplementation instanceof FetchImplementationProvider
55
+ ? fetchImplementation.getFetch()
56
+ : fetchImplementation;
39
57
  }
40
58
  async getCredentials() {
41
59
  const { expiresAt } = this.credentials ?? {};
@@ -119,7 +137,7 @@ export class AbstractRemote {
119
137
  async socketStream(options) {
120
138
  const { path } = options;
121
139
  const request = await this.buildRequest(path);
122
- const BSON = await this.getBSON();
140
+ const bson = await this.getBSON();
123
141
  const connector = new RSocketConnector({
124
142
  transport: new WebsocketClientTransport({
125
143
  url: this.options.socketUrlTransformer(request.url)
@@ -131,7 +149,7 @@ export class AbstractRemote {
131
149
  metadataMimeType: 'application/bson',
132
150
  payload: {
133
151
  data: null,
134
- metadata: Buffer.from(BSON.serialize({
152
+ metadata: Buffer.from(bson.serialize({
135
153
  token: request.headers.Authorization
136
154
  }))
137
155
  }
@@ -167,8 +185,8 @@ export class AbstractRemote {
167
185
  const socket = await new Promise((resolve, reject) => {
168
186
  let connectionEstablished = false;
169
187
  const res = rsocket.requestStream({
170
- data: Buffer.from(BSON.serialize(options.data)),
171
- metadata: Buffer.from(BSON.serialize({
188
+ data: Buffer.from(bson.serialize(options.data)),
189
+ metadata: Buffer.from(bson.serialize({
172
190
  path
173
191
  }))
174
192
  }, SYNC_QUEUE_REQUEST_N, // The initial N amount
@@ -199,7 +217,7 @@ export class AbstractRemote {
199
217
  if (!data) {
200
218
  return;
201
219
  }
202
- const deserializedData = BSON.deserialize(data);
220
+ const deserializedData = bson.deserialize(data);
203
221
  stream.enqueueData(deserializedData);
204
222
  },
205
223
  onComplete: () => {
@@ -305,6 +305,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
305
305
  let validatedCheckpoint = null;
306
306
  let appliedCheckpoint = null;
307
307
  let bucketSet = new Set(initialBuckets.keys());
308
+ const { params = undefined } = (await this.options.remote.getCredentials()) ?? {};
308
309
  this.logger.debug('Requesting stream from server');
309
310
  const syncOptions = {
310
311
  path: '/sync/stream',
@@ -312,7 +313,8 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
312
313
  data: {
313
314
  buckets: req,
314
315
  include_checksum: true,
315
- raw_data: true
316
+ raw_data: true,
317
+ parameters: params
316
318
  }
317
319
  };
318
320
  const stream = resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP
@@ -59,6 +59,10 @@ export interface StreamingSyncRequest {
59
59
  * Changes the response to stringified data in each OplogEntry
60
60
  */
61
61
  raw_data: boolean;
62
+ /**
63
+ * Client parameters to be passed to the sync rules.
64
+ */
65
+ parameters?: Record<string, string>;
62
66
  }
63
67
  export interface StreamingSyncCheckpoint {
64
68
  checkpoint: Checkpoint;
@@ -0,0 +1,25 @@
1
+ export interface ControlledExecutorOptions {
2
+ /**
3
+ * If throttling is enabled, it ensures only one task runs at a time,
4
+ * and only one additional task can be scheduled to run after the current task completes. The pending task will be overwritten by the latest task.
5
+ * Enabled by default.
6
+ */
7
+ throttleEnabled?: boolean;
8
+ }
9
+ export declare class ControlledExecutor<T> {
10
+ private task;
11
+ /**
12
+ * Represents the currently running task, which could be a Promise or undefined if no task is running.
13
+ */
14
+ private runningTask;
15
+ private pendingTaskParam;
16
+ /**
17
+ * Flag to determine if throttling is enabled, which controls whether tasks are queued or run immediately.
18
+ */
19
+ private isThrottling;
20
+ private closed;
21
+ constructor(task: (param: T) => Promise<void> | void, options?: ControlledExecutorOptions);
22
+ schedule(param: T): void;
23
+ dispose(): void;
24
+ private execute;
25
+ }
@@ -0,0 +1,50 @@
1
+ export class ControlledExecutor {
2
+ task;
3
+ /**
4
+ * Represents the currently running task, which could be a Promise or undefined if no task is running.
5
+ */
6
+ runningTask;
7
+ pendingTaskParam;
8
+ /**
9
+ * Flag to determine if throttling is enabled, which controls whether tasks are queued or run immediately.
10
+ */
11
+ isThrottling;
12
+ closed;
13
+ constructor(task, options) {
14
+ this.task = task;
15
+ const { throttleEnabled = true } = options ?? {};
16
+ this.isThrottling = throttleEnabled;
17
+ this.closed = false;
18
+ }
19
+ schedule(param) {
20
+ if (this.closed) {
21
+ return;
22
+ }
23
+ if (!this.isThrottling) {
24
+ this.task(param);
25
+ return;
26
+ }
27
+ if (this.runningTask) {
28
+ // set or replace the pending task param with latest one
29
+ this.pendingTaskParam = param;
30
+ return;
31
+ }
32
+ this.execute(param);
33
+ }
34
+ dispose() {
35
+ this.closed = true;
36
+ if (this.runningTask) {
37
+ this.runningTask = undefined;
38
+ }
39
+ }
40
+ async execute(param) {
41
+ this.runningTask = this.task(param);
42
+ await this.runningTask;
43
+ this.runningTask = undefined;
44
+ if (this.pendingTaskParam) {
45
+ const pendingParam = this.pendingTaskParam;
46
+ this.pendingTaskParam = undefined;
47
+ this.execute(pendingParam);
48
+ }
49
+ }
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/common",
3
- "version": "0.0.0-dev-20240516143814",
3
+ "version": "0.0.0-dev-20240606141637",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"