@powersync/common 1.49.0 → 1.51.0
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.
- package/dist/bundle.cjs +214 -39
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.mjs +210 -37
- package/dist/bundle.mjs.map +1 -1
- package/dist/bundle.node.cjs +214 -38
- package/dist/bundle.node.cjs.map +1 -1
- package/dist/bundle.node.mjs +210 -36
- package/dist/bundle.node.mjs.map +1 -1
- package/dist/index.d.cts +75 -16
- package/lib/attachments/AttachmentQueue.d.ts +10 -4
- package/lib/attachments/AttachmentQueue.js +10 -4
- package/lib/attachments/AttachmentQueue.js.map +1 -1
- package/lib/attachments/AttachmentService.js +2 -3
- package/lib/attachments/AttachmentService.js.map +1 -1
- package/lib/attachments/SyncingService.d.ts +2 -1
- package/lib/attachments/SyncingService.js +4 -5
- package/lib/attachments/SyncingService.js.map +1 -1
- package/lib/client/AbstractPowerSyncDatabase.d.ts +5 -1
- package/lib/client/AbstractPowerSyncDatabase.js +6 -2
- package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
- package/lib/db/DBAdapter.d.ts +7 -1
- package/lib/db/DBAdapter.js.map +1 -1
- package/lib/utils/mutex.d.ts +47 -5
- package/lib/utils/mutex.js +146 -21
- package/lib/utils/mutex.js.map +1 -1
- package/lib/utils/queue.d.ts +16 -0
- package/lib/utils/queue.js +42 -0
- package/lib/utils/queue.js.map +1 -0
- package/package.json +1 -2
- package/src/attachments/AttachmentQueue.ts +10 -4
- package/src/attachments/AttachmentService.ts +2 -3
- package/src/attachments/README.md +6 -4
- package/src/attachments/SyncingService.ts +4 -5
- package/src/client/AbstractPowerSyncDatabase.ts +6 -2
- package/src/db/DBAdapter.ts +7 -1
- package/src/utils/mutex.ts +184 -26
- package/src/utils/queue.ts +48 -0
package/dist/bundle.node.cjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var asyncMutex = require('async-mutex');
|
|
4
3
|
var eventIterator = require('event-iterator');
|
|
5
4
|
var node_buffer = require('node:buffer');
|
|
6
5
|
|
|
@@ -661,7 +660,7 @@ class SyncingService {
|
|
|
661
660
|
updatedAttachments.push(downloaded);
|
|
662
661
|
break;
|
|
663
662
|
case exports.AttachmentState.QUEUED_DELETE:
|
|
664
|
-
const deleted = await this.deleteAttachment(attachment);
|
|
663
|
+
const deleted = await this.deleteAttachment(attachment, context);
|
|
665
664
|
updatedAttachments.push(deleted);
|
|
666
665
|
break;
|
|
667
666
|
}
|
|
@@ -739,17 +738,16 @@ class SyncingService {
|
|
|
739
738
|
* On failure, defers to error handler or archives.
|
|
740
739
|
*
|
|
741
740
|
* @param attachment - The attachment record to delete
|
|
741
|
+
* @param context - Attachment context for database operations
|
|
742
742
|
* @returns Updated attachment record
|
|
743
743
|
*/
|
|
744
|
-
async deleteAttachment(attachment) {
|
|
744
|
+
async deleteAttachment(attachment, context) {
|
|
745
745
|
try {
|
|
746
746
|
await this.remoteStorage.deleteFile(attachment);
|
|
747
747
|
if (attachment.localUri) {
|
|
748
748
|
await this.localStorage.deleteFile(attachment.localUri);
|
|
749
749
|
}
|
|
750
|
-
await
|
|
751
|
-
await ctx.deleteAttachment(attachment.id);
|
|
752
|
-
});
|
|
750
|
+
await context.deleteAttachment(attachment.id);
|
|
753
751
|
return {
|
|
754
752
|
...attachment,
|
|
755
753
|
state: exports.AttachmentState.ARCHIVED
|
|
@@ -787,32 +785,198 @@ class SyncingService {
|
|
|
787
785
|
}
|
|
788
786
|
|
|
789
787
|
/**
|
|
790
|
-
*
|
|
788
|
+
* A simple fixed-capacity queue implementation.
|
|
789
|
+
*
|
|
790
|
+
* Unlike a naive queue implemented by `array.push()` and `array.shift()`, this avoids moving array elements around
|
|
791
|
+
* and is `O(1)` for {@link addLast} and {@link removeFirst}.
|
|
791
792
|
*/
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
793
|
+
class Queue {
|
|
794
|
+
table;
|
|
795
|
+
// Index of the first element in the table.
|
|
796
|
+
head;
|
|
797
|
+
// Amount of items currently in the queue.
|
|
798
|
+
_length;
|
|
799
|
+
constructor(initialItems) {
|
|
800
|
+
this.table = [...initialItems];
|
|
801
|
+
this.head = 0;
|
|
802
|
+
this._length = this.table.length;
|
|
803
|
+
}
|
|
804
|
+
get isEmpty() {
|
|
805
|
+
return this.length == 0;
|
|
806
|
+
}
|
|
807
|
+
get length() {
|
|
808
|
+
return this._length;
|
|
809
|
+
}
|
|
810
|
+
removeFirst() {
|
|
811
|
+
if (this.isEmpty) {
|
|
812
|
+
throw new Error('Queue is empty');
|
|
813
|
+
}
|
|
814
|
+
const result = this.table[this.head];
|
|
815
|
+
this._length--;
|
|
816
|
+
this.table[this.head] = undefined;
|
|
817
|
+
this.head = (this.head + 1) % this.table.length;
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
820
|
+
addLast(element) {
|
|
821
|
+
if (this.length == this.table.length) {
|
|
822
|
+
throw new Error('Queue is full');
|
|
823
|
+
}
|
|
824
|
+
this.table[(this.head + this._length) % this.table.length] = element;
|
|
825
|
+
this._length++;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* An asynchronous semaphore implementation with associated items per lease.
|
|
831
|
+
*
|
|
832
|
+
* @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
|
|
833
|
+
*/
|
|
834
|
+
class Semaphore {
|
|
835
|
+
// Available items that are not currently assigned to a waiter.
|
|
836
|
+
available;
|
|
837
|
+
size;
|
|
838
|
+
// Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
|
|
839
|
+
// aborted waiters from the middle of the list efficiently.
|
|
840
|
+
firstWaiter;
|
|
841
|
+
lastWaiter;
|
|
842
|
+
constructor(elements) {
|
|
843
|
+
this.available = new Queue(elements);
|
|
844
|
+
this.size = this.available.length;
|
|
845
|
+
}
|
|
846
|
+
addWaiter(requestedItems, onAcquire) {
|
|
847
|
+
const node = {
|
|
848
|
+
isActive: true,
|
|
849
|
+
acquiredItems: [],
|
|
850
|
+
remainingItems: requestedItems,
|
|
851
|
+
onAcquire,
|
|
852
|
+
prev: this.lastWaiter
|
|
853
|
+
};
|
|
854
|
+
if (this.lastWaiter) {
|
|
855
|
+
this.lastWaiter.next = node;
|
|
856
|
+
this.lastWaiter = node;
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
// First waiter
|
|
860
|
+
this.lastWaiter = this.firstWaiter = node;
|
|
861
|
+
}
|
|
862
|
+
return node;
|
|
863
|
+
}
|
|
864
|
+
deactivateWaiter(waiter) {
|
|
865
|
+
const { prev, next } = waiter;
|
|
866
|
+
waiter.isActive = false;
|
|
867
|
+
if (prev)
|
|
868
|
+
prev.next = next;
|
|
869
|
+
if (next)
|
|
870
|
+
next.prev = prev;
|
|
871
|
+
if (waiter == this.firstWaiter)
|
|
872
|
+
this.firstWaiter = next;
|
|
873
|
+
if (waiter == this.lastWaiter)
|
|
874
|
+
this.lastWaiter = prev;
|
|
875
|
+
}
|
|
876
|
+
requestPermits(amount, abort) {
|
|
877
|
+
if (amount <= 0 || amount > this.size) {
|
|
878
|
+
throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`);
|
|
879
|
+
}
|
|
880
|
+
return new Promise((resolve, reject) => {
|
|
881
|
+
function rejectAborted() {
|
|
882
|
+
reject(abort?.reason ?? new Error('Semaphore acquire aborted'));
|
|
883
|
+
}
|
|
884
|
+
if (abort?.aborted) {
|
|
885
|
+
return rejectAborted();
|
|
886
|
+
}
|
|
887
|
+
let waiter;
|
|
888
|
+
const markCompleted = () => {
|
|
889
|
+
const items = waiter.acquiredItems;
|
|
890
|
+
waiter.acquiredItems = []; // Avoid releasing items twice.
|
|
891
|
+
for (const element of items) {
|
|
892
|
+
// Give to next waiter, if possible.
|
|
893
|
+
const nextWaiter = this.firstWaiter;
|
|
894
|
+
if (nextWaiter) {
|
|
895
|
+
nextWaiter.acquiredItems.push(element);
|
|
896
|
+
nextWaiter.remainingItems--;
|
|
897
|
+
if (nextWaiter.remainingItems == 0) {
|
|
898
|
+
nextWaiter.onAcquire();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
// No pending waiter, return lease into pool.
|
|
903
|
+
this.available.addLast(element);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
const onAbort = () => {
|
|
908
|
+
abort?.removeEventListener('abort', onAbort);
|
|
909
|
+
if (waiter.isActive) {
|
|
910
|
+
this.deactivateWaiter(waiter);
|
|
911
|
+
rejectAborted();
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
const resolvePromise = () => {
|
|
915
|
+
this.deactivateWaiter(waiter);
|
|
916
|
+
abort?.removeEventListener('abort', onAbort);
|
|
917
|
+
const items = waiter.acquiredItems;
|
|
918
|
+
resolve({ items, release: markCompleted });
|
|
919
|
+
};
|
|
920
|
+
waiter = this.addWaiter(amount, resolvePromise);
|
|
921
|
+
// If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is
|
|
922
|
+
// only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter).
|
|
923
|
+
while (!this.available.isEmpty && waiter.remainingItems > 0) {
|
|
924
|
+
waiter.acquiredItems.push(this.available.removeFirst());
|
|
925
|
+
waiter.remainingItems--;
|
|
810
926
|
}
|
|
811
|
-
|
|
812
|
-
|
|
927
|
+
if (waiter.remainingItems == 0) {
|
|
928
|
+
return resolvePromise();
|
|
813
929
|
}
|
|
930
|
+
abort?.addEventListener('abort', onAbort);
|
|
814
931
|
});
|
|
815
|
-
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Requests a single item from the pool.
|
|
935
|
+
*
|
|
936
|
+
* The returned `release` callback must be invoked to return the item into the pool.
|
|
937
|
+
*/
|
|
938
|
+
async requestOne(abort) {
|
|
939
|
+
const { items, release } = await this.requestPermits(1, abort);
|
|
940
|
+
return { release, item: items[0] };
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Requests access to all items from the pool.
|
|
944
|
+
*
|
|
945
|
+
* The returned `release` callback must be invoked to return items into the pool.
|
|
946
|
+
*/
|
|
947
|
+
requestAll(abort) {
|
|
948
|
+
return this.requestPermits(this.size, abort);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* An asynchronous mutex implementation.
|
|
953
|
+
*
|
|
954
|
+
* @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
|
|
955
|
+
*/
|
|
956
|
+
class Mutex {
|
|
957
|
+
inner = new Semaphore([null]);
|
|
958
|
+
async acquire(abort) {
|
|
959
|
+
const { release } = await this.inner.requestOne(abort);
|
|
960
|
+
return release;
|
|
961
|
+
}
|
|
962
|
+
async runExclusive(fn, abort) {
|
|
963
|
+
const returnMutex = await this.acquire(abort);
|
|
964
|
+
try {
|
|
965
|
+
return await fn();
|
|
966
|
+
}
|
|
967
|
+
finally {
|
|
968
|
+
returnMutex();
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
function timeoutSignal(timeout) {
|
|
973
|
+
if (timeout == null)
|
|
974
|
+
return;
|
|
975
|
+
if ('timeout' in AbortSignal)
|
|
976
|
+
return AbortSignal.timeout(timeout);
|
|
977
|
+
const controller = new AbortController();
|
|
978
|
+
setTimeout(() => controller.abort(new Error('Timeout waiting for lock')), timeout);
|
|
979
|
+
return controller.signal;
|
|
816
980
|
}
|
|
817
981
|
|
|
818
982
|
/**
|
|
@@ -824,7 +988,7 @@ class AttachmentService {
|
|
|
824
988
|
db;
|
|
825
989
|
logger;
|
|
826
990
|
tableName;
|
|
827
|
-
mutex = new
|
|
991
|
+
mutex = new Mutex();
|
|
828
992
|
context;
|
|
829
993
|
constructor(db, logger, tableName = 'attachments', archivedCacheLimit = 100) {
|
|
830
994
|
this.db = db;
|
|
@@ -861,7 +1025,7 @@ class AttachmentService {
|
|
|
861
1025
|
* Executes a callback with exclusive access to the attachment context.
|
|
862
1026
|
*/
|
|
863
1027
|
async withContext(callback) {
|
|
864
|
-
return
|
|
1028
|
+
return this.mutex.runExclusive(async () => {
|
|
865
1029
|
return callback(this.context);
|
|
866
1030
|
});
|
|
867
1031
|
}
|
|
@@ -897,9 +1061,15 @@ class AttachmentQueue {
|
|
|
897
1061
|
tableName;
|
|
898
1062
|
/** Logger instance for diagnostic information */
|
|
899
1063
|
logger;
|
|
900
|
-
/** Interval in milliseconds between periodic sync operations.
|
|
1064
|
+
/** Interval in milliseconds between periodic sync operations. Acts as a polling timer to retry
|
|
1065
|
+
* failed uploads/downloads, especially after the app goes offline. Default: 30000 (30 seconds) */
|
|
901
1066
|
syncIntervalMs = 30 * 1000;
|
|
902
|
-
/**
|
|
1067
|
+
/** Throttle duration in milliseconds for the reactive watch query on the attachments table.
|
|
1068
|
+
* When attachment records change, a watch query detects the change and triggers a sync.
|
|
1069
|
+
* This throttle prevents the sync from firing too rapidly when many changes happen in
|
|
1070
|
+
* quick succession (e.g., bulk inserts). This is distinct from syncIntervalMs — it controls
|
|
1071
|
+
* how quickly the queue reacts to changes, while syncIntervalMs controls how often it polls
|
|
1072
|
+
* for retries. Default: 30 (from DEFAULT_WATCH_THROTTLE_MS) */
|
|
903
1073
|
syncThrottleDuration;
|
|
904
1074
|
/** Whether to automatically download remote attachments. Default: true */
|
|
905
1075
|
downloadAttachments = true;
|
|
@@ -923,8 +1093,8 @@ class AttachmentQueue {
|
|
|
923
1093
|
* @param options.watchAttachments - Callback for monitoring attachment changes in your data model
|
|
924
1094
|
* @param options.tableName - Name of the table to store attachment records. Default: 'ps_attachment_queue'
|
|
925
1095
|
* @param options.logger - Logger instance. Defaults to db.logger
|
|
926
|
-
* @param options.syncIntervalMs -
|
|
927
|
-
* @param options.syncThrottleDuration - Throttle duration for
|
|
1096
|
+
* @param options.syncIntervalMs - Periodic polling interval in milliseconds for retrying failed uploads/downloads. Default: 30000
|
|
1097
|
+
* @param options.syncThrottleDuration - Throttle duration in milliseconds for the reactive watch query that detects attachment changes. Prevents rapid-fire syncs during bulk changes. Default: 30
|
|
928
1098
|
* @param options.downloadAttachments - Whether to automatically download remote attachments. Default: true
|
|
929
1099
|
* @param options.archivedCacheLimit - Maximum archived attachments before cleanup. Default: 100
|
|
930
1100
|
*/
|
|
@@ -8061,7 +8231,7 @@ function requireDist () {
|
|
|
8061
8231
|
|
|
8062
8232
|
var distExports = requireDist();
|
|
8063
8233
|
|
|
8064
|
-
var version = "1.
|
|
8234
|
+
var version = "1.51.0";
|
|
8065
8235
|
var PACKAGE = {
|
|
8066
8236
|
version: version};
|
|
8067
8237
|
|
|
@@ -10453,7 +10623,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
10453
10623
|
this._schema = schema;
|
|
10454
10624
|
this.ready = false;
|
|
10455
10625
|
this.sdkVersion = '';
|
|
10456
|
-
this.runExclusiveMutex = new
|
|
10626
|
+
this.runExclusiveMutex = new Mutex();
|
|
10457
10627
|
// Start async init
|
|
10458
10628
|
this.subscriptions = {
|
|
10459
10629
|
firstStatusMatching: (predicate, abort) => this.waitForStatus(predicate, abort),
|
|
@@ -10918,6 +11088,10 @@ SELECT * FROM crud_entries;
|
|
|
10918
11088
|
* Execute a SQL write (INSERT/UPDATE/DELETE) query
|
|
10919
11089
|
* and optionally return results.
|
|
10920
11090
|
*
|
|
11091
|
+
* When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure),
|
|
11092
|
+
* the returned result's `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements.
|
|
11093
|
+
* Use a `RETURNING` clause and inspect `result.rows` when you need to confirm which rows changed.
|
|
11094
|
+
*
|
|
10921
11095
|
* @param sql The SQL query to execute
|
|
10922
11096
|
* @param parameters Optional array of parameters to bind to the query
|
|
10923
11097
|
* @returns The query result as an object with structured key-value pairs
|
|
@@ -11014,7 +11188,7 @@ SELECT * FROM crud_entries;
|
|
|
11014
11188
|
async readTransaction(callback, lockTimeout = DEFAULT_LOCK_TIMEOUT_MS) {
|
|
11015
11189
|
await this.waitForReady();
|
|
11016
11190
|
return this.database.readTransaction(async (tx) => {
|
|
11017
|
-
const res = await callback(
|
|
11191
|
+
const res = await callback(tx);
|
|
11018
11192
|
await tx.rollback();
|
|
11019
11193
|
return res;
|
|
11020
11194
|
}, { timeoutMs: lockTimeout });
|
|
@@ -12044,10 +12218,12 @@ exports.LogLevel = LogLevel;
|
|
|
12044
12218
|
exports.MAX_AMOUNT_OF_COLUMNS = MAX_AMOUNT_OF_COLUMNS;
|
|
12045
12219
|
exports.MAX_OP_ID = MAX_OP_ID;
|
|
12046
12220
|
exports.MEMORY_TRIGGER_CLAIM_MANAGER = MEMORY_TRIGGER_CLAIM_MANAGER;
|
|
12221
|
+
exports.Mutex = Mutex;
|
|
12047
12222
|
exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
|
|
12048
12223
|
exports.OpType = OpType;
|
|
12049
12224
|
exports.OplogEntry = OplogEntry;
|
|
12050
12225
|
exports.Schema = Schema;
|
|
12226
|
+
exports.Semaphore = Semaphore;
|
|
12051
12227
|
exports.SqliteBucketStorage = SqliteBucketStorage;
|
|
12052
12228
|
exports.SyncDataBatch = SyncDataBatch;
|
|
12053
12229
|
exports.SyncDataBucket = SyncDataBucket;
|
|
@@ -12077,9 +12253,9 @@ exports.isStreamingSyncCheckpointDiff = isStreamingSyncCheckpointDiff;
|
|
|
12077
12253
|
exports.isStreamingSyncCheckpointPartiallyComplete = isStreamingSyncCheckpointPartiallyComplete;
|
|
12078
12254
|
exports.isStreamingSyncData = isStreamingSyncData;
|
|
12079
12255
|
exports.isSyncNewCheckpointRequest = isSyncNewCheckpointRequest;
|
|
12080
|
-
exports.mutexRunExclusive = mutexRunExclusive;
|
|
12081
12256
|
exports.parseQuery = parseQuery;
|
|
12082
12257
|
exports.runOnSchemaChange = runOnSchemaChange;
|
|
12083
12258
|
exports.sanitizeSQL = sanitizeSQL;
|
|
12084
12259
|
exports.sanitizeUUID = sanitizeUUID;
|
|
12260
|
+
exports.timeoutSignal = timeoutSignal;
|
|
12085
12261
|
//# sourceMappingURL=bundle.node.cjs.map
|