@loro-dev/flock-sqlite 0.4.0 → 0.6.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/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +249 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loro-dev/flock-sqlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "SQLite-backed Flock CRDT replica for Node, browsers, and Cloudflare Workers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"tsdown": "^0.15.4",
|
|
63
63
|
"typescript": "^5.9.2",
|
|
64
64
|
"vitest": "^3.2.4",
|
|
65
|
-
"@loro-dev/flock": "4.
|
|
65
|
+
"@loro-dev/flock": "4.4.0"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsdown",
|
package/src/index.ts
CHANGED
|
@@ -627,6 +627,24 @@ export class FlockSQLite {
|
|
|
627
627
|
private maxHlc: { physicalTime: number; logicalCounter: number };
|
|
628
628
|
private listeners: Set<EventListener>;
|
|
629
629
|
private tables: TableNames;
|
|
630
|
+
/** Transaction state: undefined when not in transaction, array when accumulating */
|
|
631
|
+
private txnEventSink:
|
|
632
|
+
| Array<{ key: KeyPart[]; payload: ExportPayload; source: string }>
|
|
633
|
+
| undefined;
|
|
634
|
+
/** Debounce state for autoDebounceCommit */
|
|
635
|
+
private debounceState:
|
|
636
|
+
| {
|
|
637
|
+
timeout: number;
|
|
638
|
+
maxDebounceTime: number;
|
|
639
|
+
timerId: ReturnType<typeof setTimeout> | undefined;
|
|
640
|
+
maxTimerId: ReturnType<typeof setTimeout> | undefined;
|
|
641
|
+
pendingEvents: Array<{
|
|
642
|
+
key: KeyPart[];
|
|
643
|
+
payload: ExportPayload;
|
|
644
|
+
source: string;
|
|
645
|
+
}>;
|
|
646
|
+
}
|
|
647
|
+
| undefined;
|
|
630
648
|
|
|
631
649
|
private constructor(
|
|
632
650
|
db: UniStoreConnection,
|
|
@@ -641,6 +659,8 @@ export class FlockSQLite {
|
|
|
641
659
|
this.maxHlc = maxHlc;
|
|
642
660
|
this.listeners = new Set();
|
|
643
661
|
this.tables = tables;
|
|
662
|
+
this.txnEventSink = undefined;
|
|
663
|
+
this.debounceState = undefined;
|
|
644
664
|
}
|
|
645
665
|
|
|
646
666
|
static async open(options: FlockSQLiteOptions): Promise<FlockSQLite> {
|
|
@@ -662,6 +682,20 @@ export class FlockSQLite {
|
|
|
662
682
|
}
|
|
663
683
|
|
|
664
684
|
async close(): Promise<void> {
|
|
685
|
+
// Commit any pending debounced events
|
|
686
|
+
if (this.debounceState !== undefined) {
|
|
687
|
+
this.disableAutoDebounceCommit();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Commit any transaction events (edge case: close during txn)
|
|
691
|
+
if (this.txnEventSink !== undefined) {
|
|
692
|
+
const pending = this.txnEventSink;
|
|
693
|
+
this.txnEventSink = undefined;
|
|
694
|
+
if (pending.length > 0) {
|
|
695
|
+
this.emitEvents("local", pending);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
665
699
|
await this.db.close();
|
|
666
700
|
}
|
|
667
701
|
|
|
@@ -908,14 +942,48 @@ export class FlockSQLite {
|
|
|
908
942
|
source: operation.source,
|
|
909
943
|
};
|
|
910
944
|
if (operation.eventSink) {
|
|
945
|
+
// Explicit event sink provided (e.g., import)
|
|
911
946
|
operation.eventSink.push(eventPayload);
|
|
947
|
+
} else if (this.txnEventSink) {
|
|
948
|
+
// In transaction: accumulate events
|
|
949
|
+
this.txnEventSink.push(eventPayload);
|
|
950
|
+
} else if (this.debounceState) {
|
|
951
|
+
// Debounce active: accumulate and reset timer
|
|
952
|
+
this.debounceState.pendingEvents.push(eventPayload);
|
|
953
|
+
this.resetDebounceTimer();
|
|
912
954
|
} else {
|
|
955
|
+
// Normal: emit immediately
|
|
913
956
|
this.emitEvents(operation.source, [eventPayload]);
|
|
914
957
|
}
|
|
915
958
|
}
|
|
916
959
|
return applied;
|
|
917
960
|
}
|
|
918
961
|
|
|
962
|
+
private resetDebounceTimer(): void {
|
|
963
|
+
if (this.debounceState === undefined) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (this.debounceState.timerId !== undefined) {
|
|
968
|
+
clearTimeout(this.debounceState.timerId);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
this.debounceState.timerId = setTimeout(() => {
|
|
972
|
+
this.commit();
|
|
973
|
+
}, this.debounceState.timeout);
|
|
974
|
+
|
|
975
|
+
// Start max debounce timer on first pending event
|
|
976
|
+
// Note: this is called after the event is pushed, so length === 1 means first event
|
|
977
|
+
if (
|
|
978
|
+
this.debounceState.maxTimerId === undefined &&
|
|
979
|
+
this.debounceState.pendingEvents.length === 1
|
|
980
|
+
) {
|
|
981
|
+
this.debounceState.maxTimerId = setTimeout(() => {
|
|
982
|
+
this.commit();
|
|
983
|
+
}, this.debounceState.maxDebounceTime);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
919
987
|
private emitEvents(
|
|
920
988
|
source: string,
|
|
921
989
|
events: Array<{ key: KeyPart[]; payload: ExportPayload }>,
|
|
@@ -1473,6 +1541,23 @@ export class FlockSQLite {
|
|
|
1473
1541
|
}
|
|
1474
1542
|
|
|
1475
1543
|
private async importInternal(bundle: ExportBundle): Promise<ImportReport> {
|
|
1544
|
+
// Force commit if in transaction - this is an error condition
|
|
1545
|
+
if (this.txnEventSink !== undefined) {
|
|
1546
|
+
const pending = this.txnEventSink;
|
|
1547
|
+
this.txnEventSink = undefined;
|
|
1548
|
+
if (pending.length > 0) {
|
|
1549
|
+
this.emitEvents("local", pending);
|
|
1550
|
+
}
|
|
1551
|
+
throw new Error(
|
|
1552
|
+
"import called during transaction - transaction was auto-committed",
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// Force commit if in debounce mode
|
|
1557
|
+
if (this.debounceState !== undefined) {
|
|
1558
|
+
this.commit();
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1476
1561
|
if (bundle.version !== 0) {
|
|
1477
1562
|
throw new TypeError("Unsupported bundle version");
|
|
1478
1563
|
}
|
|
@@ -1597,6 +1682,170 @@ export class FlockSQLite {
|
|
|
1597
1682
|
this.listeners.delete(listener);
|
|
1598
1683
|
};
|
|
1599
1684
|
}
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* Execute operations within a transaction. All put/delete operations inside
|
|
1688
|
+
* the callback will be batched and emitted as a single EventBatch when the
|
|
1689
|
+
* transaction commits successfully.
|
|
1690
|
+
*
|
|
1691
|
+
* If the callback throws or rejects, the transaction is rolled back and no
|
|
1692
|
+
* events are emitted. Note: Database operations are NOT rolled back - only
|
|
1693
|
+
* event emission is affected.
|
|
1694
|
+
*
|
|
1695
|
+
* @param callback - Async function containing put/delete operations
|
|
1696
|
+
* @returns The return value of the callback
|
|
1697
|
+
* @throws Error if nested transaction attempted
|
|
1698
|
+
* @throws Error if called while autoDebounceCommit is active
|
|
1699
|
+
*
|
|
1700
|
+
* @example
|
|
1701
|
+
* ```ts
|
|
1702
|
+
* await flock.txn(async () => {
|
|
1703
|
+
* await flock.put(["a"], 1);
|
|
1704
|
+
* await flock.put(["b"], 2);
|
|
1705
|
+
* await flock.put(["c"], 3);
|
|
1706
|
+
* });
|
|
1707
|
+
* // Subscribers receive a single EventBatch with 3 events
|
|
1708
|
+
* ```
|
|
1709
|
+
*/
|
|
1710
|
+
async txn<T>(callback: () => Promise<T>): Promise<T> {
|
|
1711
|
+
if (this.txnEventSink !== undefined) {
|
|
1712
|
+
throw new Error("Nested transactions are not supported");
|
|
1713
|
+
}
|
|
1714
|
+
if (this.debounceState !== undefined) {
|
|
1715
|
+
throw new Error(
|
|
1716
|
+
"Cannot start transaction while autoDebounceCommit is active",
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const eventSink: Array<{
|
|
1721
|
+
key: KeyPart[];
|
|
1722
|
+
payload: ExportPayload;
|
|
1723
|
+
source: string;
|
|
1724
|
+
}> = [];
|
|
1725
|
+
this.txnEventSink = eventSink;
|
|
1726
|
+
|
|
1727
|
+
try {
|
|
1728
|
+
const result = await callback();
|
|
1729
|
+
// Commit: emit all accumulated events as single batch
|
|
1730
|
+
if (eventSink.length > 0) {
|
|
1731
|
+
this.emitEvents("local", eventSink);
|
|
1732
|
+
}
|
|
1733
|
+
return result;
|
|
1734
|
+
} finally {
|
|
1735
|
+
this.txnEventSink = undefined;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/**
|
|
1740
|
+
* Check if a transaction is currently active.
|
|
1741
|
+
*/
|
|
1742
|
+
isInTxn(): boolean {
|
|
1743
|
+
return this.txnEventSink !== undefined;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* Enable auto-debounce mode. Events will be accumulated and emitted after
|
|
1748
|
+
* the specified timeout of inactivity. Each new operation resets the timer.
|
|
1749
|
+
*
|
|
1750
|
+
* Use `commit()` to force immediate emission of pending events.
|
|
1751
|
+
* Use `disableAutoDebounceCommit()` to disable and emit pending events.
|
|
1752
|
+
*
|
|
1753
|
+
* Import operations will automatically call `commit()` before proceeding.
|
|
1754
|
+
*
|
|
1755
|
+
* @param timeout - Debounce timeout in milliseconds
|
|
1756
|
+
* @param options - Optional configuration object with maxDebounceTime (default: 10000ms)
|
|
1757
|
+
* @throws Error if called while a transaction is active
|
|
1758
|
+
* @throws Error if autoDebounceCommit is already active
|
|
1759
|
+
*
|
|
1760
|
+
* @example
|
|
1761
|
+
* ```ts
|
|
1762
|
+
* flock.autoDebounceCommit(100);
|
|
1763
|
+
* await flock.put(["a"], 1);
|
|
1764
|
+
* await flock.put(["b"], 2);
|
|
1765
|
+
* // No events emitted yet...
|
|
1766
|
+
* // After 100ms of inactivity, subscribers receive single EventBatch
|
|
1767
|
+
* // If operations keep coming, commit happens after maxDebounceTime (10s default)
|
|
1768
|
+
* ```
|
|
1769
|
+
*/
|
|
1770
|
+
autoDebounceCommit(
|
|
1771
|
+
timeout: number,
|
|
1772
|
+
options?: { maxDebounceTime?: number },
|
|
1773
|
+
): void {
|
|
1774
|
+
if (this.txnEventSink !== undefined) {
|
|
1775
|
+
throw new Error(
|
|
1776
|
+
"Cannot enable autoDebounceCommit while transaction is active",
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
if (this.debounceState !== undefined) {
|
|
1780
|
+
throw new Error("autoDebounceCommit is already active");
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
const maxDebounceTime = options?.maxDebounceTime ?? 10000;
|
|
1784
|
+
|
|
1785
|
+
this.debounceState = {
|
|
1786
|
+
timeout,
|
|
1787
|
+
maxDebounceTime,
|
|
1788
|
+
timerId: undefined,
|
|
1789
|
+
maxTimerId: undefined,
|
|
1790
|
+
pendingEvents: [],
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/**
|
|
1795
|
+
* Disable auto-debounce mode and emit any pending events immediately.
|
|
1796
|
+
* No-op if autoDebounceCommit is not active.
|
|
1797
|
+
*/
|
|
1798
|
+
disableAutoDebounceCommit(): void {
|
|
1799
|
+
if (this.debounceState === undefined) {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
const { timerId, maxTimerId, pendingEvents } = this.debounceState;
|
|
1804
|
+
if (timerId !== undefined) {
|
|
1805
|
+
clearTimeout(timerId);
|
|
1806
|
+
}
|
|
1807
|
+
if (maxTimerId !== undefined) {
|
|
1808
|
+
clearTimeout(maxTimerId);
|
|
1809
|
+
}
|
|
1810
|
+
this.debounceState = undefined;
|
|
1811
|
+
|
|
1812
|
+
if (pendingEvents.length > 0) {
|
|
1813
|
+
this.emitEvents("local", pendingEvents);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Force immediate emission of any pending debounced events.
|
|
1819
|
+
* Does not disable auto-debounce mode - new operations will continue to be debounced.
|
|
1820
|
+
* No-op if autoDebounceCommit is not active or no events are pending.
|
|
1821
|
+
*/
|
|
1822
|
+
commit(): void {
|
|
1823
|
+
if (this.debounceState === undefined) {
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
const { timerId, maxTimerId, pendingEvents } = this.debounceState;
|
|
1828
|
+
if (timerId !== undefined) {
|
|
1829
|
+
clearTimeout(timerId);
|
|
1830
|
+
this.debounceState.timerId = undefined;
|
|
1831
|
+
}
|
|
1832
|
+
if (maxTimerId !== undefined) {
|
|
1833
|
+
clearTimeout(maxTimerId);
|
|
1834
|
+
this.debounceState.maxTimerId = undefined;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
if (pendingEvents.length > 0) {
|
|
1838
|
+
this.emitEvents("local", pendingEvents);
|
|
1839
|
+
this.debounceState.pendingEvents = [];
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/**
|
|
1844
|
+
* Check if auto-debounce mode is currently active.
|
|
1845
|
+
*/
|
|
1846
|
+
isAutoDebounceActive(): boolean {
|
|
1847
|
+
return this.debounceState !== undefined;
|
|
1848
|
+
}
|
|
1600
1849
|
}
|
|
1601
1850
|
|
|
1602
1851
|
export type {
|