@backloghq/opslog 0.4.1 → 0.5.1
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/README.md +55 -0
- package/dist/store.d.ts +19 -0
- package/dist/store.js +82 -2
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -134,11 +134,66 @@ await store.open(dir, {
|
|
|
134
134
|
version: 1, // Schema version
|
|
135
135
|
migrate: (record, fromVersion) => record, // Migration function
|
|
136
136
|
readOnly: false, // Open in read-only mode (default: false)
|
|
137
|
+
writeMode: "immediate", // "immediate" (default), "group" (~12x faster), or "async" (~50x faster, lossy on crash)
|
|
138
|
+
groupCommitSize: 50, // Group: flush after N ops (default: 50)
|
|
139
|
+
groupCommitMs: 100, // Group: flush after N ms (default: 100)
|
|
137
140
|
agentId: "agent-A", // Enable multi-writer mode (optional)
|
|
138
141
|
backend: new FsBackend(), // Custom storage backend (optional, default: FsBackend)
|
|
139
142
|
});
|
|
140
143
|
```
|
|
141
144
|
|
|
145
|
+
## Group Commit
|
|
146
|
+
|
|
147
|
+
Buffer writes in memory and flush as a single disk write. ~12x faster for sustained writes.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const store = new Store();
|
|
151
|
+
await store.open("./data", {
|
|
152
|
+
writeMode: "group", // Buffer ops, flush periodically
|
|
153
|
+
groupCommitSize: 50, // Flush when buffer has 50 ops
|
|
154
|
+
groupCommitMs: 100, // Or after 100ms idle
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Writes are buffered — no fsync per op
|
|
158
|
+
await store.set("a", valueA);
|
|
159
|
+
await store.set("b", valueB); // Both flushed together
|
|
160
|
+
|
|
161
|
+
// Explicit flush if needed
|
|
162
|
+
await store.flush();
|
|
163
|
+
|
|
164
|
+
// close() always flushes before shutting down
|
|
165
|
+
await store.close();
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Async Mode
|
|
169
|
+
|
|
170
|
+
For maximum write throughput, use `writeMode: "async"`. Writes resolve immediately after buffering in memory — no disk I/O on the hot path. ~50x faster than immediate mode.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const store = new Store();
|
|
174
|
+
await store.open("./data", {
|
|
175
|
+
writeMode: "async",
|
|
176
|
+
groupCommitSize: 50,
|
|
177
|
+
groupCommitMs: 100,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await store.set("a", valueA); // Returns instantly — buffered in memory
|
|
181
|
+
await store.set("b", valueB); // Same
|
|
182
|
+
|
|
183
|
+
// Ensure durability before exit
|
|
184
|
+
await store.sync();
|
|
185
|
+
await store.close();
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Crash semantics:** Data buffered since the last flush is **lost** on unclean shutdown. Call `sync()` before process exit for durability. `close()` always flushes.
|
|
189
|
+
|
|
190
|
+
**Safety:** Forced to `"immediate"` when `agentId` is set (multi-writer mode). Other agents can't see buffered ops, so group/async commit is single-writer only.
|
|
191
|
+
|
|
192
|
+
**When to use which mode:**
|
|
193
|
+
- `"immediate"` (default) — every write is durable. Use when data loss is unacceptable.
|
|
194
|
+
- `"group"` — writes are batched but caller still waits for flush. ~12x faster. Crash loses up to `groupCommitMs` ms of data.
|
|
195
|
+
- `"async"` — writes return instantly. ~50x faster. Crash loses all unflushed data. Best for high-throughput, latency-sensitive, crash-tolerant workloads.
|
|
196
|
+
|
|
142
197
|
## Multi-Writer Mode
|
|
143
198
|
|
|
144
199
|
Multiple agents can write to the same store concurrently. Each agent gets its own WAL file — no write contention.
|
package/dist/store.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export declare class Store<T = Record<string, unknown>> {
|
|
|
17
17
|
private backend;
|
|
18
18
|
private agentId?;
|
|
19
19
|
private clock;
|
|
20
|
+
private groupCommit;
|
|
21
|
+
private asyncMode;
|
|
22
|
+
private groupBuffer;
|
|
23
|
+
private groupSize;
|
|
24
|
+
private groupMs;
|
|
25
|
+
private groupTimer;
|
|
20
26
|
private manifestVersion;
|
|
21
27
|
/**
|
|
22
28
|
* Serialize all state-mutating operations through a promise chain.
|
|
@@ -88,5 +94,18 @@ export declare class Store<T = Record<string, unknown>> {
|
|
|
88
94
|
private applyOp;
|
|
89
95
|
private reverseOp;
|
|
90
96
|
private persistOp;
|
|
97
|
+
/**
|
|
98
|
+
* Flush buffered ops to disk in a single write.
|
|
99
|
+
* In group/async mode: drains the in-memory buffer to disk.
|
|
100
|
+
* Safe to call at any time. No-op if nothing is buffered.
|
|
101
|
+
*/
|
|
102
|
+
flush(): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Ensure all buffered operations are durably persisted to disk.
|
|
105
|
+
* Alias for flush(). Use before process exit when using async write mode
|
|
106
|
+
* to prevent data loss.
|
|
107
|
+
*/
|
|
108
|
+
sync(): Promise<void>;
|
|
109
|
+
private flushGroupBuffer;
|
|
91
110
|
private defaultPeriod;
|
|
92
111
|
}
|
package/dist/store.js
CHANGED
|
@@ -27,6 +27,13 @@ export class Store {
|
|
|
27
27
|
// Multi-writer state
|
|
28
28
|
agentId;
|
|
29
29
|
clock = null;
|
|
30
|
+
// Group commit state
|
|
31
|
+
groupCommit = false;
|
|
32
|
+
asyncMode = false;
|
|
33
|
+
groupBuffer = [];
|
|
34
|
+
groupSize = 50;
|
|
35
|
+
groupMs = 100;
|
|
36
|
+
groupTimer = null;
|
|
30
37
|
manifestVersion = null;
|
|
31
38
|
/**
|
|
32
39
|
* Serialize all state-mutating operations through a promise chain.
|
|
@@ -46,12 +53,26 @@ export class Store {
|
|
|
46
53
|
async open(dir, options) {
|
|
47
54
|
this.dir = dir;
|
|
48
55
|
if (options) {
|
|
49
|
-
const { backend, agentId, ...rest } = options;
|
|
56
|
+
const { backend, agentId, writeMode, groupCommitSize, groupCommitMs, ...rest } = options;
|
|
50
57
|
this.coreOpts = { ...this.coreOpts, ...rest };
|
|
51
58
|
if (backend)
|
|
52
59
|
this.backend = backend;
|
|
53
60
|
if (agentId)
|
|
54
61
|
this.agentId = agentId;
|
|
62
|
+
// Group/async commit: enabled when writeMode is "group"/"async" AND not multi-writer
|
|
63
|
+
if (writeMode === "group" || writeMode === "async") {
|
|
64
|
+
if (agentId) {
|
|
65
|
+
console.error(`opslog: writeMode '${writeMode}' is not compatible with multi-writer (agentId). Using 'immediate'.`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.groupCommit = true;
|
|
69
|
+
this.asyncMode = writeMode === "async";
|
|
70
|
+
if (groupCommitSize)
|
|
71
|
+
this.groupSize = groupCommitSize;
|
|
72
|
+
if (groupCommitMs)
|
|
73
|
+
this.groupMs = groupCommitMs;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
55
76
|
}
|
|
56
77
|
this.backend ??= new FsBackend();
|
|
57
78
|
await this.backend.initialize(dir, { readOnly: this.coreOpts.readOnly });
|
|
@@ -183,6 +204,10 @@ export class Store {
|
|
|
183
204
|
async close() {
|
|
184
205
|
this.ensureOpen();
|
|
185
206
|
this.unwatch();
|
|
207
|
+
// Flush any buffered group commit ops before checkpoint
|
|
208
|
+
if (this.groupCommit && this.groupBuffer.length > 0) {
|
|
209
|
+
await this.serialize(() => this.flushGroupBuffer());
|
|
210
|
+
}
|
|
186
211
|
if (!this.coreOpts.readOnly &&
|
|
187
212
|
this.coreOpts.checkpointOnClose &&
|
|
188
213
|
this.ops.length > 0) {
|
|
@@ -483,6 +508,9 @@ export class Store {
|
|
|
483
508
|
return true;
|
|
484
509
|
}
|
|
485
510
|
async _compact() {
|
|
511
|
+
// Flush group buffer before checkpoint
|
|
512
|
+
if (this.groupCommit)
|
|
513
|
+
await this.flushGroupBuffer();
|
|
486
514
|
if (this.isMultiWriter()) {
|
|
487
515
|
await this._compactMultiWriter();
|
|
488
516
|
return;
|
|
@@ -656,12 +684,64 @@ export class Store {
|
|
|
656
684
|
}
|
|
657
685
|
}
|
|
658
686
|
async persistOp(op) {
|
|
659
|
-
await this.backend.appendOps(this.activeOpsPath, [op]);
|
|
660
687
|
this.ops.push(op);
|
|
688
|
+
if (this.groupCommit) {
|
|
689
|
+
// Buffer the op, flush when buffer is full or timer fires
|
|
690
|
+
this.groupBuffer.push(op);
|
|
691
|
+
if (this.groupBuffer.length >= this.groupSize) {
|
|
692
|
+
if (this.asyncMode) {
|
|
693
|
+
// Async: trigger flush in background, don't await
|
|
694
|
+
this.serialize(() => this.flushGroupBuffer()).catch(() => { });
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
await this.flushGroupBuffer();
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
else if (!this.groupTimer) {
|
|
701
|
+
this.groupTimer = setTimeout(() => {
|
|
702
|
+
this.serialize(() => this.flushGroupBuffer()).catch(() => { });
|
|
703
|
+
}, this.groupMs);
|
|
704
|
+
}
|
|
705
|
+
// Async mode: return immediately without waiting for disk I/O
|
|
706
|
+
if (this.asyncMode)
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
// Immediate: write to disk now
|
|
711
|
+
await this.backend.appendOps(this.activeOpsPath, [op]);
|
|
712
|
+
}
|
|
661
713
|
if (this.ops.length >= this.coreOpts.checkpointThreshold) {
|
|
662
714
|
await this._compact();
|
|
663
715
|
}
|
|
664
716
|
}
|
|
717
|
+
/**
|
|
718
|
+
* Flush buffered ops to disk in a single write.
|
|
719
|
+
* In group/async mode: drains the in-memory buffer to disk.
|
|
720
|
+
* Safe to call at any time. No-op if nothing is buffered.
|
|
721
|
+
*/
|
|
722
|
+
async flush() {
|
|
723
|
+
if (!this.groupCommit || this.groupBuffer.length === 0)
|
|
724
|
+
return;
|
|
725
|
+
return this.serialize(() => this.flushGroupBuffer());
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Ensure all buffered operations are durably persisted to disk.
|
|
729
|
+
* Alias for flush(). Use before process exit when using async write mode
|
|
730
|
+
* to prevent data loss.
|
|
731
|
+
*/
|
|
732
|
+
async sync() {
|
|
733
|
+
return this.flush();
|
|
734
|
+
}
|
|
735
|
+
async flushGroupBuffer() {
|
|
736
|
+
if (this.groupBuffer.length === 0)
|
|
737
|
+
return;
|
|
738
|
+
if (this.groupTimer) {
|
|
739
|
+
clearTimeout(this.groupTimer);
|
|
740
|
+
this.groupTimer = null;
|
|
741
|
+
}
|
|
742
|
+
await this.backend.appendOps(this.activeOpsPath, this.groupBuffer);
|
|
743
|
+
this.groupBuffer = [];
|
|
744
|
+
}
|
|
665
745
|
defaultPeriod() {
|
|
666
746
|
const now = new Date();
|
|
667
747
|
const q = Math.ceil((now.getMonth() + 1) / 3);
|
package/dist/types.d.ts
CHANGED
|
@@ -58,6 +58,12 @@ export interface StoreOptions {
|
|
|
58
58
|
backend?: StorageBackend;
|
|
59
59
|
/** Agent ID for multi-writer mode. Enables per-agent WAL streams and LWW conflict resolution. */
|
|
60
60
|
agentId?: string;
|
|
61
|
+
/** Write mode: "immediate" flushes every op (default, safe for multi-writer). "group" buffers ops and flushes periodically (~12x faster writes). "async" buffers ops and resolves immediately without waiting for flush (~50x faster, data lost on crash). Forced to "immediate" when agentId is set. */
|
|
62
|
+
writeMode?: "immediate" | "group" | "async";
|
|
63
|
+
/** Group commit: max ops to buffer before flush (default: 50). Only used when writeMode is "group". */
|
|
64
|
+
groupCommitSize?: number;
|
|
65
|
+
/** Group commit: max milliseconds before flush (default: 100). Only used when writeMode is "group". */
|
|
66
|
+
groupCommitMs?: number;
|
|
61
67
|
}
|
|
62
68
|
export interface StoreStats {
|
|
63
69
|
activeRecords: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backloghq/opslog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Embedded event-sourced document store. Append-only operation log with immutable snapshots, zero native dependencies.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|