@powersync/common 0.0.0-dev-20260311103504 → 0.0.0-dev-20260503073249

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 (72) hide show
  1. package/dist/bundle.cjs +791 -489
  2. package/dist/bundle.cjs.map +1 -1
  3. package/dist/bundle.mjs +785 -485
  4. package/dist/bundle.mjs.map +1 -1
  5. package/dist/bundle.node.cjs +789 -488
  6. package/dist/bundle.node.cjs.map +1 -1
  7. package/dist/bundle.node.mjs +783 -484
  8. package/dist/bundle.node.mjs.map +1 -1
  9. package/dist/index.d.cts +165 -103
  10. package/lib/attachments/AttachmentQueue.d.ts +10 -4
  11. package/lib/attachments/AttachmentQueue.js +10 -4
  12. package/lib/attachments/AttachmentQueue.js.map +1 -1
  13. package/lib/attachments/AttachmentService.js +2 -3
  14. package/lib/attachments/AttachmentService.js.map +1 -1
  15. package/lib/attachments/SyncingService.d.ts +2 -1
  16. package/lib/attachments/SyncingService.js +4 -5
  17. package/lib/attachments/SyncingService.js.map +1 -1
  18. package/lib/client/AbstractPowerSyncDatabase.d.ts +5 -1
  19. package/lib/client/AbstractPowerSyncDatabase.js +9 -5
  20. package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
  21. package/lib/client/sync/stream/AbstractRemote.d.ts +29 -8
  22. package/lib/client/sync/stream/AbstractRemote.js +154 -177
  23. package/lib/client/sync/stream/AbstractRemote.js.map +1 -1
  24. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +4 -0
  25. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +88 -88
  26. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
  27. package/lib/db/DBAdapter.d.ts +55 -9
  28. package/lib/db/DBAdapter.js +126 -0
  29. package/lib/db/DBAdapter.js.map +1 -1
  30. package/lib/db/crud/SyncStatus.d.ts +0 -4
  31. package/lib/db/crud/SyncStatus.js +0 -4
  32. package/lib/db/crud/SyncStatus.js.map +1 -1
  33. package/lib/db/schema/RawTable.d.ts +0 -5
  34. package/lib/db/schema/Schema.d.ts +0 -2
  35. package/lib/db/schema/Schema.js +0 -2
  36. package/lib/db/schema/Schema.js.map +1 -1
  37. package/lib/index.d.ts +1 -1
  38. package/lib/index.js +0 -1
  39. package/lib/index.js.map +1 -1
  40. package/lib/utils/async.d.ts +0 -9
  41. package/lib/utils/async.js +0 -9
  42. package/lib/utils/async.js.map +1 -1
  43. package/lib/utils/mutex.d.ts +47 -5
  44. package/lib/utils/mutex.js +146 -21
  45. package/lib/utils/mutex.js.map +1 -1
  46. package/lib/utils/queue.d.ts +16 -0
  47. package/lib/utils/queue.js +42 -0
  48. package/lib/utils/queue.js.map +1 -0
  49. package/lib/utils/stream_transform.d.ts +39 -0
  50. package/lib/utils/stream_transform.js +206 -0
  51. package/lib/utils/stream_transform.js.map +1 -0
  52. package/package.json +9 -8
  53. package/src/attachments/AttachmentQueue.ts +10 -4
  54. package/src/attachments/AttachmentService.ts +2 -3
  55. package/src/attachments/README.md +6 -4
  56. package/src/attachments/SyncingService.ts +4 -5
  57. package/src/client/AbstractPowerSyncDatabase.ts +9 -5
  58. package/src/client/sync/stream/AbstractRemote.ts +182 -206
  59. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +96 -83
  60. package/src/db/DBAdapter.ts +167 -9
  61. package/src/db/crud/SyncStatus.ts +0 -4
  62. package/src/db/schema/RawTable.ts +0 -5
  63. package/src/db/schema/Schema.ts +0 -2
  64. package/src/index.ts +1 -1
  65. package/src/utils/async.ts +0 -11
  66. package/src/utils/mutex.ts +184 -26
  67. package/src/utils/queue.ts +48 -0
  68. package/src/utils/stream_transform.ts +252 -0
  69. package/lib/utils/DataStream.d.ts +0 -62
  70. package/lib/utils/DataStream.js +0 -169
  71. package/lib/utils/DataStream.js.map +0 -1
  72. package/src/utils/DataStream.ts +0 -222
@@ -4,7 +4,6 @@ import { InternalProgressInformation } from '../../../db/crud/SyncProgress.js';
4
4
  import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js';
5
5
  import { AbortOperation } from '../../../utils/AbortOperation.js';
6
6
  import { BaseListener, BaseObserver, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js';
7
- import { DataStream } from '../../../utils/DataStream.js';
8
7
  import { throttleLeadingTrailing } from '../../../utils/async.js';
9
8
  import {
10
9
  BucketChecksum,
@@ -19,7 +18,6 @@ import { AbstractRemote, FetchStrategy, SyncStreamOptions } from './AbstractRemo
19
18
  import { EstablishSyncStream, Instruction, coreStatusToJs } from './core-instruction.js';
20
19
  import {
21
20
  BucketRequest,
22
- CrudUploadNotification,
23
21
  StreamingSyncLine,
24
22
  StreamingSyncLineOrCrudUploadComplete,
25
23
  StreamingSyncRequestParameterType,
@@ -30,6 +28,8 @@ import {
30
28
  isStreamingSyncCheckpointPartiallyComplete,
31
29
  isStreamingSyncData
32
30
  } from './streaming-sync-types.js';
31
+ import { injectable, InjectableIterator, map, SimpleAsyncIterator } from '../../../utils/stream_transform.js';
32
+ import type { BSON } from 'bson';
33
33
 
34
34
  export enum LockType {
35
35
  CRUD = 'crud',
@@ -209,6 +209,7 @@ export interface StreamingSyncImplementation
209
209
  waitForStatus(status: SyncStatusOptions): Promise<void>;
210
210
  waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
211
211
  updateSubscriptions(subscriptions: SubscribedStream[]): void;
212
+ markConnectionMayHaveChanged(): void;
212
213
  }
213
214
 
214
215
  export const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
@@ -256,6 +257,7 @@ export abstract class AbstractStreamingSyncImplementation
256
257
  protected streamingSyncPromise?: Promise<void>;
257
258
  protected logger: ILogger;
258
259
  private activeStreams: SubscribedStream[];
260
+ private connectionMayHaveChanged = false;
259
261
 
260
262
  private isUploadingCrud: boolean = false;
261
263
  private notifyCompletedUploads?: () => void;
@@ -572,6 +574,10 @@ The next upload iteration will be delayed.`);
572
574
  this.logger.warn(ex);
573
575
  shouldDelayRetry = false;
574
576
  // A disconnect was requested, we should not delay since there is no explicit retry
577
+ } else if (this.connectionMayHaveChanged && (ex as Error).message?.indexOf('No iteration is active') >= 0) {
578
+ this.connectionMayHaveChanged = false;
579
+ this.logger.info('Sync error after changed connection, retrying immediately');
580
+ shouldDelayRetry = false;
575
581
  } else {
576
582
  this.logger.error(ex);
577
583
  }
@@ -607,6 +613,17 @@ The next upload iteration will be delayed.`);
607
613
  this.updateSyncStatus({ connected: false, connecting: false });
608
614
  }
609
615
 
616
+ markConnectionMayHaveChanged() {
617
+ // By setting this field, we'll immediately retry if the next sync event causes an error triggered by us not having
618
+ // an active sync iteration on the connection in use.
619
+ this.connectionMayHaveChanged = true;
620
+
621
+ // This triggers a `powersync_control` invocation if a sync iteration is currently active. This is a cheap call to
622
+ // make when no subscriptions have actually changed, we're mainly interested in this immediately throwing if no
623
+ // iteration is active. That allows us to reconnect ASAP, instead of having to wait for the next sync line.
624
+ this.handleActiveStreamsChange?.();
625
+ }
626
+
610
627
  private async collectLocalBucketState(): Promise<[BucketRequest[], Map<string, BucketDescription | null>]> {
611
628
  const bucketEntries = await this.options.adapter.getBucketStates();
612
629
  const req: BucketRequest[] = bucketEntries.map((entry) => ({
@@ -682,6 +699,27 @@ The next upload iteration will be delayed.`);
682
699
  });
683
700
  }
684
701
 
702
+ private async receiveSyncLines(data: {
703
+ options: SyncStreamOptions;
704
+ connection: RequiredPowerSyncConnectionOptions;
705
+ bson?: typeof BSON;
706
+ }): Promise<SimpleAsyncIterator<Uint8Array | string>> {
707
+ const { options, connection, bson } = data;
708
+ const remote = this.options.remote;
709
+
710
+ if (connection.connectionMethod == SyncStreamConnectionMethod.HTTP) {
711
+ return await remote.fetchStream(options);
712
+ } else {
713
+ return await this.options.remote.socketStreamRaw(
714
+ {
715
+ ...options,
716
+ ...{ fetchStrategy: connection.fetchStrategy }
717
+ },
718
+ bson
719
+ );
720
+ }
721
+ }
722
+
685
723
  private async legacyStreamingSyncIteration(signal: AbortSignal, resolvedOptions: RequiredPowerSyncConnectionOptions) {
686
724
  const rawTables = resolvedOptions.serializedSchema?.raw_tables;
687
725
  if (rawTables != null && rawTables.length) {
@@ -717,46 +755,31 @@ The next upload iteration will be delayed.`);
717
755
  }
718
756
  };
719
757
 
720
- let stream: DataStream<StreamingSyncLineOrCrudUploadComplete>;
721
- if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
722
- stream = await this.options.remote.postStreamRaw(syncOptions, (line: string | CrudUploadNotification) => {
758
+ const bson = await this.options.remote.getBSON();
759
+ const source = await this.receiveSyncLines({
760
+ options: syncOptions,
761
+ connection: resolvedOptions,
762
+ bson
763
+ });
764
+ const stream: InjectableIterator<StreamingSyncLineOrCrudUploadComplete> = injectable(
765
+ map(source, (line) => {
723
766
  if (typeof line == 'string') {
724
767
  return JSON.parse(line) as StreamingSyncLine;
725
768
  } else {
726
- // Directly enqueued by us
727
- return line;
769
+ return bson.deserialize(line) as StreamingSyncLine;
728
770
  }
729
- });
730
- } else {
731
- const bson = await this.options.remote.getBSON();
732
- stream = await this.options.remote.socketStreamRaw(
733
- {
734
- ...syncOptions,
735
- ...{ fetchStrategy: resolvedOptions.fetchStrategy }
736
- },
737
- (payload: Uint8Array | CrudUploadNotification) => {
738
- if (payload instanceof Uint8Array) {
739
- return bson.deserialize(payload) as StreamingSyncLine;
740
- } else {
741
- // Directly enqueued by us
742
- return payload;
743
- }
744
- },
745
- bson
746
- );
747
- }
771
+ })
772
+ );
748
773
 
749
774
  this.logger.debug('Stream established. Processing events');
750
775
 
751
776
  this.notifyCompletedUploads = () => {
752
- if (!stream.closed) {
753
- stream.enqueueData({ crud_upload_completed: null });
754
- }
777
+ stream.inject({ crud_upload_completed: null });
755
778
  };
756
779
 
757
- while (!stream.closed) {
758
- const line = await stream.read();
759
- if (!line) {
780
+ while (true) {
781
+ const { value: line, done } = await stream.next();
782
+ if (done) {
760
783
  // The stream has closed while waiting
761
784
  return;
762
785
  }
@@ -942,6 +965,11 @@ The next upload iteration will be delayed.`);
942
965
  const syncImplementation = this;
943
966
  const adapter = this.options.adapter;
944
967
  const remote = this.options.remote;
968
+ const controller = new AbortController();
969
+ const abort = () => {
970
+ return controller.abort(signal.reason);
971
+ };
972
+ signal.addEventListener('abort', abort);
945
973
  let receivingLines: Promise<void> | null = null;
946
974
  let hadSyncLine = false;
947
975
  let hideDisconnectOnRestart = false;
@@ -949,64 +977,53 @@ The next upload iteration will be delayed.`);
949
977
  if (signal.aborted) {
950
978
  throw new AbortOperation('Connection request has been aborted');
951
979
  }
952
- const abortController = new AbortController();
953
- signal.addEventListener('abort', () => abortController.abort());
954
980
 
955
981
  // Pending sync lines received from the service, as well as local events that trigger a powersync_control
956
982
  // invocation (local events include refreshed tokens and completed uploads).
957
983
  // This is a single data stream so that we can handle all control calls from a single place.
958
- let controlInvocations: DataStream<EnqueuedCommand, Uint8Array | EnqueuedCommand> | null = null;
984
+ let controlInvocations: InjectableIterator<EnqueuedCommand> | null = null;
959
985
 
960
986
  async function connect(instr: EstablishSyncStream) {
961
987
  const syncOptions: SyncStreamOptions = {
962
988
  path: '/sync/stream',
963
- abortSignal: abortController.signal,
989
+ abortSignal: controller.signal,
964
990
  data: instr.request
965
991
  };
966
992
 
967
- if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) {
968
- controlInvocations = await remote.postStreamRaw(syncOptions, (line: string | EnqueuedCommand) => {
969
- if (typeof line == 'string') {
970
- return {
971
- command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
972
- payload: line
973
- };
974
- } else {
975
- // Directly enqueued by us
976
- return line;
977
- }
978
- });
979
- } else {
980
- controlInvocations = await remote.socketStreamRaw(
981
- {
982
- ...syncOptions,
983
- fetchStrategy: resolvedOptions.fetchStrategy
984
- },
985
- (payload: Uint8Array | EnqueuedCommand) => {
986
- if (payload instanceof Uint8Array) {
993
+ controlInvocations = injectable(
994
+ map(
995
+ await syncImplementation.receiveSyncLines({
996
+ options: syncOptions,
997
+ connection: resolvedOptions
998
+ }),
999
+ (line) => {
1000
+ if (typeof line == 'string') {
987
1001
  return {
988
- command: PowerSyncControlCommand.PROCESS_BSON_LINE,
989
- payload: payload
1002
+ command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
1003
+ payload: line
990
1004
  };
991
1005
  } else {
992
- // Directly enqueued by us
993
- return payload;
1006
+ return {
1007
+ command: PowerSyncControlCommand.PROCESS_BSON_LINE,
1008
+ payload: line
1009
+ };
994
1010
  }
995
1011
  }
996
- );
997
- }
1012
+ )
1013
+ );
998
1014
 
999
1015
  // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
1000
1016
  // we're already connected here and can report that.
1001
1017
  syncImplementation.updateSyncStatus({ connected: true });
1002
1018
 
1003
1019
  try {
1004
- while (!controlInvocations.closed) {
1005
- const line = await controlInvocations.read();
1006
- if (line == null) {
1007
- return;
1020
+ while (true) {
1021
+ let event = await controlInvocations.next();
1022
+ if (event.done) {
1023
+ break;
1008
1024
  }
1009
1025
 
1026
+ const line = event.value;
1010
1027
  await control(line.command, line.payload);
1011
1028
 
1012
1029
  if (!hadSyncLine) {
@@ -1015,12 +1032,8 @@ The next upload iteration will be delayed.`);
1015
1032
  }
1016
1033
  }
1017
1034
  } finally {
1018
- const activeInstructions = controlInvocations;
1019
- // We concurrently add events to the active data stream when e.g. a CRUD upload is completed or a token is
1020
- // refreshed. That would throw after closing (and we can't handle those events either way), so set this back
1021
- // to null.
1022
- controlInvocations = null;
1023
- await activeInstructions.close();
1035
+ abort();
1036
+ signal.removeEventListener('abort', abort);
1024
1037
  }
1025
1038
  }
1026
1039
 
@@ -1038,6 +1051,10 @@ The next upload iteration will be delayed.`);
1038
1051
  rawResponse
1039
1052
  );
1040
1053
 
1054
+ if (op != PowerSyncControlCommand.STOP) {
1055
+ // Evidently we have a working connection here, otherwise powersync_control would have failed.
1056
+ syncImplementation.connectionMayHaveChanged = false;
1057
+ }
1041
1058
  await handleInstructions(JSON.parse(rawResponse));
1042
1059
  }
1043
1060
 
@@ -1072,7 +1089,7 @@ The next upload iteration will be delayed.`);
1072
1089
  // Restart iteration after the credentials have been refreshed.
1073
1090
  remote.fetchCredentials().then(
1074
1091
  (_) => {
1075
- controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
1092
+ controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
1076
1093
  },
1077
1094
  (err) => {
1078
1095
  syncImplementation.logger.warn('Could not prefetch credentials', err);
@@ -1080,7 +1097,7 @@ The next upload iteration will be delayed.`);
1080
1097
  );
1081
1098
  }
1082
1099
  } else if ('CloseSyncStream' in instruction) {
1083
- abortController.abort();
1100
+ controller.abort();
1084
1101
  hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
1085
1102
  } else if ('FlushFileSystem' in instruction) {
1086
1103
  // Not necessary on JS platforms.
@@ -1113,17 +1130,13 @@ The next upload iteration will be delayed.`);
1113
1130
  await control(PowerSyncControlCommand.START, JSON.stringify(options));
1114
1131
 
1115
1132
  this.notifyCompletedUploads = () => {
1116
- if (controlInvocations && !controlInvocations?.closed) {
1117
- controlInvocations.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
1118
- }
1133
+ controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
1119
1134
  };
1120
1135
  this.handleActiveStreamsChange = () => {
1121
- if (controlInvocations && !controlInvocations?.closed) {
1122
- controlInvocations.enqueueData({
1123
- command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
1124
- payload: JSON.stringify(this.activeStreams)
1125
- });
1126
- }
1136
+ controlInvocations?.inject({
1137
+ command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
1138
+ payload: JSON.stringify(this.activeStreams)
1139
+ });
1127
1140
  };
1128
1141
  await receivingLines;
1129
1142
  } finally {
@@ -16,7 +16,13 @@ import { BaseListener, BaseObserverInterface } from '../utils/BaseObserver.js';
16
16
  export type QueryResult = {
17
17
  /** Represents the auto-generated row id if applicable. */
18
18
  insertId?: number;
19
- /** Number of affected rows if result of a update query. */
19
+ /**
20
+ * Number of affected rows reported by SQLite for a write query.
21
+ *
22
+ * When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure),
23
+ * `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements.
24
+ * Use a `RETURNING` clause and inspect `rows` when you need to confirm which rows changed.
25
+ */
20
26
  rowsAffected: number;
21
27
  /** if status is undefined or 0 this object will contain the query results */
22
28
  rows?: {
@@ -41,7 +47,7 @@ export interface DBGetUtils {
41
47
  get<T>(sql: string, parameters?: any[]): Promise<T>;
42
48
  }
43
49
 
44
- export interface LockContext extends DBGetUtils {
50
+ export interface SqlExecutor {
45
51
  /** Execute a single write statement. */
46
52
  execute: (query: string, params?: any[] | undefined) => Promise<QueryResult>;
47
53
  /**
@@ -59,6 +65,61 @@ export interface LockContext extends DBGetUtils {
59
65
  * ```[ { id: '33', name: 'list 1', content: 'Post content', list_id: '1' } ]```
60
66
  */
61
67
  executeRaw: (query: string, params?: any[] | undefined) => Promise<any[][]>;
68
+
69
+ executeBatch: (query: string, params?: any[][]) => Promise<QueryResult>;
70
+ }
71
+
72
+ export interface LockContext extends SqlExecutor, DBGetUtils {}
73
+
74
+ /**
75
+ * Implements {@link DBGetUtils} on a {@link SqlRunner}.
76
+ */
77
+ export function DBGetUtilsDefaultMixin<TBase extends new (...args: any[]) => Omit<SqlExecutor, 'executeBatch'>>(
78
+ Base: TBase
79
+ ) {
80
+ return class extends Base implements DBGetUtils, SqlExecutor {
81
+ async getAll<T>(sql: string, parameters?: any[]): Promise<T[]> {
82
+ const res = await this.execute(sql, parameters);
83
+ return res.rows?._array ?? [];
84
+ }
85
+
86
+ async getOptional<T>(sql: string, parameters?: any[]): Promise<T | null> {
87
+ const res = await this.execute(sql, parameters);
88
+ return res.rows?.item(0) ?? null;
89
+ }
90
+
91
+ async get<T>(sql: string, parameters?: any[]): Promise<T> {
92
+ const res = await this.execute(sql, parameters);
93
+ const first = res.rows?.item(0);
94
+ if (!first) {
95
+ throw new Error('Result set is empty');
96
+ }
97
+ return first;
98
+ }
99
+
100
+ async executeBatch(query: string, params: any[][] = []): Promise<QueryResult> {
101
+ // If this context can run batch statements natively, use that.
102
+ // @ts-ignore
103
+ if (super.executeBatch) {
104
+ // @ts-ignore
105
+ return super.executeBatch(query, params);
106
+ }
107
+
108
+ // Emulate executeBatch by running statements individually.
109
+ let lastInsertId: number | undefined;
110
+ let rowsAffected = 0;
111
+ for (const set of params) {
112
+ const result = await this.execute(query, set);
113
+ lastInsertId = result.insertId;
114
+ rowsAffected += result.rowsAffected;
115
+ }
116
+
117
+ return {
118
+ rowsAffected,
119
+ insertId: lastInsertId
120
+ };
121
+ }
122
+ };
62
123
  }
63
124
 
64
125
  export interface Transaction extends LockContext {
@@ -107,22 +168,119 @@ export interface DBLockOptions {
107
168
  timeoutMs?: number;
108
169
  }
109
170
 
110
- export interface DBAdapter extends BaseObserverInterface<DBAdapterListener>, DBGetUtils {
111
- close: () => void | Promise<void>;
112
- execute: (query: string, params?: any[]) => Promise<QueryResult>;
113
- executeRaw: (query: string, params?: any[]) => Promise<any[][]>;
114
- executeBatch: (query: string, params?: any[][]) => Promise<QueryResult>;
171
+ export interface ConnectionPool extends BaseObserverInterface<DBAdapterListener> {
115
172
  name: string;
173
+ close: () => void | Promise<void>;
116
174
  readLock: <T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) => Promise<T>;
117
- readTransaction: <T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) => Promise<T>;
118
175
  writeLock: <T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) => Promise<T>;
119
- writeTransaction: <T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) => Promise<T>;
176
+
120
177
  /**
121
178
  * This method refreshes the schema information across all connections. This is for advanced use cases, and should generally not be needed.
122
179
  */
123
180
  refreshSchema: () => Promise<void>;
124
181
  }
125
182
 
183
+ export interface DBAdapter extends ConnectionPool, SqlExecutor, DBGetUtils {
184
+ readTransaction: <T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) => Promise<T>;
185
+ writeTransaction: <T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) => Promise<T>;
186
+ }
187
+
188
+ /**
189
+ * A mixin to implement {@link DBAdapter} by delegating to {@link ConnectionPool.readLock} and
190
+ * {@link ConnectionPool.writeLock}.
191
+ */
192
+ export function DBAdapterDefaultMixin<TBase extends new (...args: any[]) => ConnectionPool>(Base: TBase) {
193
+ return class extends Base implements DBAdapter {
194
+ readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions): Promise<T> {
195
+ return this.readLock((ctx) => TransactionImplementation.runWith(ctx, fn), options);
196
+ }
197
+
198
+ writeTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions): Promise<T> {
199
+ return this.writeLock((ctx) => TransactionImplementation.runWith(ctx, fn), options);
200
+ }
201
+
202
+ getAll<T>(sql: string, parameters?: any[]): Promise<T[]> {
203
+ return this.readLock((ctx) => ctx.getAll(sql, parameters));
204
+ }
205
+
206
+ getOptional<T>(sql: string, parameters?: any[]): Promise<T | null> {
207
+ return this.readLock((ctx) => ctx.getOptional(sql, parameters));
208
+ }
209
+
210
+ get<T>(sql: string, parameters?: any[]): Promise<T> {
211
+ return this.readLock((ctx) => ctx.get(sql, parameters));
212
+ }
213
+
214
+ execute(query: string, params?: any[]): Promise<QueryResult> {
215
+ return this.writeLock((ctx) => ctx.execute(query, params));
216
+ }
217
+
218
+ executeRaw(query: string, params?: any[]): Promise<any[][]> {
219
+ return this.writeLock((ctx) => ctx.executeRaw(query, params));
220
+ }
221
+
222
+ executeBatch(query: string, params?: any[][]): Promise<QueryResult> {
223
+ return this.writeTransaction((tx) => tx.executeBatch(query, params));
224
+ }
225
+ };
226
+ }
227
+
228
+ class BaseTransaction implements SqlExecutor {
229
+ private finalized = false;
230
+
231
+ constructor(private inner: SqlExecutor) {}
232
+
233
+ async commit(): Promise<QueryResult> {
234
+ if (this.finalized) {
235
+ return { rowsAffected: 0 };
236
+ }
237
+ this.finalized = true;
238
+ return this.inner.execute('COMMIT');
239
+ }
240
+
241
+ async rollback(): Promise<QueryResult> {
242
+ if (this.finalized) {
243
+ return { rowsAffected: 0 };
244
+ }
245
+ this.finalized = true;
246
+ return this.inner.execute('ROLLBACK');
247
+ }
248
+
249
+ execute(query: string, params?: any[] | undefined): Promise<QueryResult> {
250
+ return this.inner.execute(query, params);
251
+ }
252
+
253
+ executeRaw(query: string, params?: any[] | undefined): Promise<any[][]> {
254
+ return this.inner.executeRaw(query, params);
255
+ }
256
+
257
+ executeBatch(query: string, params?: any[][]): Promise<QueryResult> {
258
+ return this.inner.executeBatch(query, params);
259
+ }
260
+ }
261
+
262
+ class TransactionImplementation extends DBGetUtilsDefaultMixin(BaseTransaction) {
263
+ static async runWith<T>(ctx: LockContext, fn: (tx: Transaction) => Promise<T>): Promise<T> {
264
+ let tx = new TransactionImplementation(ctx);
265
+
266
+ try {
267
+ await ctx.execute('BEGIN IMMEDIATE');
268
+
269
+ const result = await fn(tx);
270
+ await tx.commit();
271
+ return result;
272
+ } catch (ex) {
273
+ try {
274
+ await tx.rollback();
275
+ } catch (ex2) {
276
+ // In rare cases, a rollback may fail.
277
+ // Safe to ignore.
278
+ }
279
+ throw ex;
280
+ }
281
+ }
282
+ }
283
+
126
284
  export function isBatchedUpdateNotification(
127
285
  update: BatchedUpdateNotification | UpdateNotification
128
286
  ): update is BatchedUpdateNotification {
@@ -122,8 +122,6 @@ export class SyncStatus {
122
122
  *
123
123
  * This returns null when the database is currently being opened and we don't have reliable information about all
124
124
  * included streams yet.
125
- *
126
- * @experimental Sync streams are currently in alpha.
127
125
  */
128
126
  get syncStreams(): SyncStreamStatus[] | undefined {
129
127
  return this.options.dataFlow?.internalStreamSubscriptions?.map((core) => new SyncStreamStatusView(this, core));
@@ -131,8 +129,6 @@ export class SyncStatus {
131
129
 
132
130
  /**
133
131
  * If the `stream` appears in {@link syncStreams}, returns the current status for that stream.
134
- *
135
- * @experimental Sync streams are currently in alpha.
136
132
  */
137
133
  forStream(stream: SyncStreamDescription): SyncStreamStatus | undefined {
138
134
  const asJson = JSON.stringify(stream.parameters);
@@ -9,11 +9,6 @@ import { TableOrRawTableOptions } from './Table.js';
9
9
  * To collect local writes to raw tables with PowerSync, custom triggers are required. See
10
10
  * {@link https://docs.powersync.com/usage/use-case-examples/raw-tables the documentation} for details and an example on
11
11
  * using raw tables.
12
- *
13
- * Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
14
- *
15
- * @experimental Please note that this feature is experimental at the moment, and not covered by PowerSync semver or
16
- * stability guarantees.
17
12
  */
18
13
  export type RawTableType = RawTableTypeWithStatements | InferredRawTableType;
19
14
 
@@ -51,10 +51,8 @@ export class Schema<S extends SchemaType = SchemaType> {
51
51
  * developer instead of automatically by PowerSync.
52
52
  * Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
53
53
  * using client-side table and column constraints.
54
- * Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
55
54
  *
56
55
  * @param tables An object of (table name, raw table definition) entries.
57
- * @experimental Note that the raw tables API is still experimental and may change in the future.
58
56
  */
59
57
  withRawTables(tables: Record<string, RawTableType>) {
60
58
  for (const [name, rawTableDefinition] of Object.entries(tables)) {
package/src/index.ts CHANGED
@@ -59,9 +59,9 @@ export * from './client/watched/WatchedQuery.js';
59
59
  export * from './utils/AbortOperation.js';
60
60
  export * from './utils/BaseObserver.js';
61
61
  export * from './utils/ControlledExecutor.js';
62
- export * from './utils/DataStream.js';
63
62
  export * from './utils/Logger.js';
64
63
  export * from './utils/mutex.js';
65
64
  export * from './utils/parseQuery.js';
65
+ export type { SimpleAsyncIterator } from './utils/stream_transform.js';
66
66
 
67
67
  export * from './types/types.js';
@@ -1,14 +1,3 @@
1
- /**
2
- * A ponyfill for `Symbol.asyncIterator` that is compatible with the
3
- * [recommended polyfill](https://github.com/Azure/azure-sdk-for-js/blob/%40azure/core-asynciterator-polyfill_1.0.2/sdk/core/core-asynciterator-polyfill/src/index.ts#L4-L6)
4
- * we recommend for React Native.
5
- *
6
- * As long as we use this symbol (instead of `for await` and `async *`) in this package, we can be compatible with async
7
- * iterators without requiring them.
8
- */
9
- export const symbolAsyncIterator: typeof Symbol.asyncIterator =
10
- Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
11
-
12
1
  /**
13
2
  * Throttle a function to be called at most once every "wait" milliseconds,
14
3
  * on the trailing edge.