@loro-dev/flock-sqlite 0.4.0 → 0.5.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 +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +220 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loro-dev/flock-sqlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.3.0"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsdown",
|
package/src/index.ts
CHANGED
|
@@ -627,6 +627,22 @@ 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
|
+
timerId: ReturnType<typeof setTimeout> | undefined;
|
|
639
|
+
pendingEvents: Array<{
|
|
640
|
+
key: KeyPart[];
|
|
641
|
+
payload: ExportPayload;
|
|
642
|
+
source: string;
|
|
643
|
+
}>;
|
|
644
|
+
}
|
|
645
|
+
| undefined;
|
|
630
646
|
|
|
631
647
|
private constructor(
|
|
632
648
|
db: UniStoreConnection,
|
|
@@ -641,6 +657,8 @@ export class FlockSQLite {
|
|
|
641
657
|
this.maxHlc = maxHlc;
|
|
642
658
|
this.listeners = new Set();
|
|
643
659
|
this.tables = tables;
|
|
660
|
+
this.txnEventSink = undefined;
|
|
661
|
+
this.debounceState = undefined;
|
|
644
662
|
}
|
|
645
663
|
|
|
646
664
|
static async open(options: FlockSQLiteOptions): Promise<FlockSQLite> {
|
|
@@ -662,6 +680,20 @@ export class FlockSQLite {
|
|
|
662
680
|
}
|
|
663
681
|
|
|
664
682
|
async close(): Promise<void> {
|
|
683
|
+
// Commit any pending debounced events
|
|
684
|
+
if (this.debounceState !== undefined) {
|
|
685
|
+
this.disableAutoDebounceCommit();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Commit any transaction events (edge case: close during txn)
|
|
689
|
+
if (this.txnEventSink !== undefined) {
|
|
690
|
+
const pending = this.txnEventSink;
|
|
691
|
+
this.txnEventSink = undefined;
|
|
692
|
+
if (pending.length > 0) {
|
|
693
|
+
this.emitEvents("local", pending);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
665
697
|
await this.db.close();
|
|
666
698
|
}
|
|
667
699
|
|
|
@@ -908,14 +940,37 @@ export class FlockSQLite {
|
|
|
908
940
|
source: operation.source,
|
|
909
941
|
};
|
|
910
942
|
if (operation.eventSink) {
|
|
943
|
+
// Explicit event sink provided (e.g., import)
|
|
911
944
|
operation.eventSink.push(eventPayload);
|
|
945
|
+
} else if (this.txnEventSink) {
|
|
946
|
+
// In transaction: accumulate events
|
|
947
|
+
this.txnEventSink.push(eventPayload);
|
|
948
|
+
} else if (this.debounceState) {
|
|
949
|
+
// Debounce active: accumulate and reset timer
|
|
950
|
+
this.debounceState.pendingEvents.push(eventPayload);
|
|
951
|
+
this.resetDebounceTimer();
|
|
912
952
|
} else {
|
|
953
|
+
// Normal: emit immediately
|
|
913
954
|
this.emitEvents(operation.source, [eventPayload]);
|
|
914
955
|
}
|
|
915
956
|
}
|
|
916
957
|
return applied;
|
|
917
958
|
}
|
|
918
959
|
|
|
960
|
+
private resetDebounceTimer(): void {
|
|
961
|
+
if (this.debounceState === undefined) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (this.debounceState.timerId !== undefined) {
|
|
966
|
+
clearTimeout(this.debounceState.timerId);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
this.debounceState.timerId = setTimeout(() => {
|
|
970
|
+
this.commit();
|
|
971
|
+
}, this.debounceState.timeout);
|
|
972
|
+
}
|
|
973
|
+
|
|
919
974
|
private emitEvents(
|
|
920
975
|
source: string,
|
|
921
976
|
events: Array<{ key: KeyPart[]; payload: ExportPayload }>,
|
|
@@ -1473,6 +1528,23 @@ export class FlockSQLite {
|
|
|
1473
1528
|
}
|
|
1474
1529
|
|
|
1475
1530
|
private async importInternal(bundle: ExportBundle): Promise<ImportReport> {
|
|
1531
|
+
// Force commit if in transaction - this is an error condition
|
|
1532
|
+
if (this.txnEventSink !== undefined) {
|
|
1533
|
+
const pending = this.txnEventSink;
|
|
1534
|
+
this.txnEventSink = undefined;
|
|
1535
|
+
if (pending.length > 0) {
|
|
1536
|
+
this.emitEvents("local", pending);
|
|
1537
|
+
}
|
|
1538
|
+
throw new Error(
|
|
1539
|
+
"import called during transaction - transaction was auto-committed",
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Force commit if in debounce mode
|
|
1544
|
+
if (this.debounceState !== undefined) {
|
|
1545
|
+
this.commit();
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1476
1548
|
if (bundle.version !== 0) {
|
|
1477
1549
|
throw new TypeError("Unsupported bundle version");
|
|
1478
1550
|
}
|
|
@@ -1597,6 +1669,154 @@ export class FlockSQLite {
|
|
|
1597
1669
|
this.listeners.delete(listener);
|
|
1598
1670
|
};
|
|
1599
1671
|
}
|
|
1672
|
+
|
|
1673
|
+
/**
|
|
1674
|
+
* Execute operations within a transaction. All put/delete operations inside
|
|
1675
|
+
* the callback will be batched and emitted as a single EventBatch when the
|
|
1676
|
+
* transaction commits successfully.
|
|
1677
|
+
*
|
|
1678
|
+
* If the callback throws or rejects, the transaction is rolled back and no
|
|
1679
|
+
* events are emitted. Note: Database operations are NOT rolled back - only
|
|
1680
|
+
* event emission is affected.
|
|
1681
|
+
*
|
|
1682
|
+
* @param callback - Async function containing put/delete operations
|
|
1683
|
+
* @returns The return value of the callback
|
|
1684
|
+
* @throws Error if nested transaction attempted
|
|
1685
|
+
* @throws Error if called while autoDebounceCommit is active
|
|
1686
|
+
*
|
|
1687
|
+
* @example
|
|
1688
|
+
* ```ts
|
|
1689
|
+
* await flock.txn(async () => {
|
|
1690
|
+
* await flock.put(["a"], 1);
|
|
1691
|
+
* await flock.put(["b"], 2);
|
|
1692
|
+
* await flock.put(["c"], 3);
|
|
1693
|
+
* });
|
|
1694
|
+
* // Subscribers receive a single EventBatch with 3 events
|
|
1695
|
+
* ```
|
|
1696
|
+
*/
|
|
1697
|
+
async txn<T>(callback: () => Promise<T>): Promise<T> {
|
|
1698
|
+
if (this.txnEventSink !== undefined) {
|
|
1699
|
+
throw new Error("Nested transactions are not supported");
|
|
1700
|
+
}
|
|
1701
|
+
if (this.debounceState !== undefined) {
|
|
1702
|
+
throw new Error(
|
|
1703
|
+
"Cannot start transaction while autoDebounceCommit is active",
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
const eventSink: Array<{
|
|
1708
|
+
key: KeyPart[];
|
|
1709
|
+
payload: ExportPayload;
|
|
1710
|
+
source: string;
|
|
1711
|
+
}> = [];
|
|
1712
|
+
this.txnEventSink = eventSink;
|
|
1713
|
+
|
|
1714
|
+
try {
|
|
1715
|
+
const result = await callback();
|
|
1716
|
+
// Commit: emit all accumulated events as single batch
|
|
1717
|
+
if (eventSink.length > 0) {
|
|
1718
|
+
this.emitEvents("local", eventSink);
|
|
1719
|
+
}
|
|
1720
|
+
return result;
|
|
1721
|
+
} finally {
|
|
1722
|
+
this.txnEventSink = undefined;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/**
|
|
1727
|
+
* Check if a transaction is currently active.
|
|
1728
|
+
*/
|
|
1729
|
+
isInTxn(): boolean {
|
|
1730
|
+
return this.txnEventSink !== undefined;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* Enable auto-debounce mode. Events will be accumulated and emitted after
|
|
1735
|
+
* the specified timeout of inactivity. Each new operation resets the timer.
|
|
1736
|
+
*
|
|
1737
|
+
* Use `commit()` to force immediate emission of pending events.
|
|
1738
|
+
* Use `disableAutoDebounceCommit()` to disable and emit pending events.
|
|
1739
|
+
*
|
|
1740
|
+
* Import operations will automatically call `commit()` before proceeding.
|
|
1741
|
+
*
|
|
1742
|
+
* @param timeout - Debounce timeout in milliseconds
|
|
1743
|
+
* @throws Error if called while a transaction is active
|
|
1744
|
+
* @throws Error if autoDebounceCommit is already active
|
|
1745
|
+
*
|
|
1746
|
+
* @example
|
|
1747
|
+
* ```ts
|
|
1748
|
+
* flock.autoDebounceCommit(100);
|
|
1749
|
+
* await flock.put(["a"], 1);
|
|
1750
|
+
* await flock.put(["b"], 2);
|
|
1751
|
+
* // No events emitted yet...
|
|
1752
|
+
* // After 100ms of inactivity, subscribers receive single EventBatch
|
|
1753
|
+
* ```
|
|
1754
|
+
*/
|
|
1755
|
+
autoDebounceCommit(timeout: number): void {
|
|
1756
|
+
if (this.txnEventSink !== undefined) {
|
|
1757
|
+
throw new Error(
|
|
1758
|
+
"Cannot enable autoDebounceCommit while transaction is active",
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
if (this.debounceState !== undefined) {
|
|
1762
|
+
throw new Error("autoDebounceCommit is already active");
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
this.debounceState = {
|
|
1766
|
+
timeout,
|
|
1767
|
+
timerId: undefined,
|
|
1768
|
+
pendingEvents: [],
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* Disable auto-debounce mode and emit any pending events immediately.
|
|
1774
|
+
* No-op if autoDebounceCommit is not active.
|
|
1775
|
+
*/
|
|
1776
|
+
disableAutoDebounceCommit(): void {
|
|
1777
|
+
if (this.debounceState === undefined) {
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const { timerId, pendingEvents } = this.debounceState;
|
|
1782
|
+
if (timerId !== undefined) {
|
|
1783
|
+
clearTimeout(timerId);
|
|
1784
|
+
}
|
|
1785
|
+
this.debounceState = undefined;
|
|
1786
|
+
|
|
1787
|
+
if (pendingEvents.length > 0) {
|
|
1788
|
+
this.emitEvents("local", pendingEvents);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/**
|
|
1793
|
+
* Force immediate emission of any pending debounced events.
|
|
1794
|
+
* Does not disable auto-debounce mode - new operations will continue to be debounced.
|
|
1795
|
+
* No-op if autoDebounceCommit is not active or no events are pending.
|
|
1796
|
+
*/
|
|
1797
|
+
commit(): void {
|
|
1798
|
+
if (this.debounceState === undefined) {
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const { timerId, pendingEvents } = this.debounceState;
|
|
1803
|
+
if (timerId !== undefined) {
|
|
1804
|
+
clearTimeout(timerId);
|
|
1805
|
+
this.debounceState.timerId = undefined;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
if (pendingEvents.length > 0) {
|
|
1809
|
+
this.emitEvents("local", pendingEvents);
|
|
1810
|
+
this.debounceState.pendingEvents = [];
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/**
|
|
1815
|
+
* Check if auto-debounce mode is currently active.
|
|
1816
|
+
*/
|
|
1817
|
+
isAutoDebounceActive(): boolean {
|
|
1818
|
+
return this.debounceState !== undefined;
|
|
1819
|
+
}
|
|
1600
1820
|
}
|
|
1601
1821
|
|
|
1602
1822
|
export type {
|