@backloghq/opslog 0.1.1 → 0.1.3

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 CHANGED
@@ -103,6 +103,8 @@ await store.batch(() => { // Multiple ops, single disk write
103
103
  });
104
104
  ```
105
105
 
106
+ Empty batches (no `set`/`delete` calls) are no-ops — no I/O is performed.
107
+
106
108
  ### History
107
109
 
108
110
  ```typescript
package/dist/archive.js CHANGED
@@ -11,8 +11,10 @@ export async function writeArchiveSegment(dir, period, records) {
11
11
  const parsed = validateArchiveSegment(JSON.parse(content));
12
12
  existing = parsed.records;
13
13
  }
14
- catch {
15
- // First write to this period
14
+ catch (err) {
15
+ const isNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
16
+ if (!isNotFound)
17
+ throw err;
16
18
  }
17
19
  const merged = { ...existing, ...Object.fromEntries(records) };
18
20
  const segment = {
package/dist/manifest.js CHANGED
@@ -3,13 +3,14 @@ import { join } from "node:path";
3
3
  import { validateManifest } from "./validate.js";
4
4
  const MANIFEST_FILE = "manifest.json";
5
5
  export async function readManifest(dir) {
6
+ let content;
6
7
  try {
7
- const content = await readFile(join(dir, MANIFEST_FILE), "utf-8");
8
- return validateManifest(JSON.parse(content));
8
+ content = await readFile(join(dir, MANIFEST_FILE), "utf-8");
9
9
  }
10
10
  catch {
11
- return null;
11
+ return null; // File not found — fresh store
12
12
  }
13
+ return validateManifest(JSON.parse(content));
13
14
  }
14
15
  export async function writeManifest(dir, manifest) {
15
16
  const path = join(dir, MANIFEST_FILE);
package/dist/store.js CHANGED
@@ -46,13 +46,24 @@ export class Store {
46
46
  }
47
47
  else {
48
48
  // Load existing state
49
- const { records, version: storedVersion } = await loadSnapshot(dir, manifest.currentSnapshot);
49
+ let snapshotData;
50
+ try {
51
+ snapshotData = await loadSnapshot(dir, manifest.currentSnapshot);
52
+ }
53
+ catch (err) {
54
+ const isNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
55
+ if (isNotFound) {
56
+ throw new Error(`Snapshot file not found: ${manifest.currentSnapshot}. The data directory may be corrupted.`, { cause: err });
57
+ }
58
+ throw err;
59
+ }
60
+ const { records, version: storedVersion } = snapshotData;
50
61
  this.records = records;
51
62
  this.version = storedVersion;
52
63
  this.activeOpsPath = manifest.activeOps;
53
64
  this.created = manifest.stats.created;
54
65
  this.archiveSegments = manifest.archiveSegments;
55
- this.archivedRecordCount = manifest.stats?.archivedRecords ?? 0;
66
+ this.archivedRecordCount = manifest.stats.archivedRecords;
56
67
  // Migrate if needed
57
68
  if (storedVersion < this.options.version) {
58
69
  for (const [id, record] of this.records) {
@@ -154,6 +165,7 @@ export class Store {
154
165
  this.batchOps = [];
155
166
  try {
156
167
  fn();
168
+ // Empty batches are no-ops — no I/O if fn() didn't call set/delete
157
169
  if (this.batchOps.length > 0) {
158
170
  await appendOps(join(this.dir, this.activeOpsPath), this.batchOps);
159
171
  this.ops.push(...this.batchOps);
package/dist/validate.js CHANGED
@@ -17,12 +17,29 @@ export function validateManifest(raw) {
17
17
  throw new Error("Invalid manifest: not an object");
18
18
  }
19
19
  const obj = raw;
20
- if (typeof obj.version !== "number")
21
- throw new Error("Invalid manifest: missing version");
20
+ if (typeof obj.version !== "number" || !Number.isInteger(obj.version) || obj.version < 1) {
21
+ throw new Error("Invalid manifest: version must be a positive integer");
22
+ }
22
23
  if (typeof obj.currentSnapshot !== "string")
23
24
  throw new Error("Invalid manifest: missing currentSnapshot");
24
25
  if (typeof obj.activeOps !== "string")
25
26
  throw new Error("Invalid manifest: missing activeOps");
27
+ if (!Array.isArray(obj.archiveSegments))
28
+ throw new Error("Invalid manifest: archiveSegments must be an array");
29
+ if (typeof obj.stats !== "object" || obj.stats === null || Array.isArray(obj.stats)) {
30
+ throw new Error("Invalid manifest: missing stats");
31
+ }
32
+ const stats = obj.stats;
33
+ if (typeof stats.activeRecords !== "number")
34
+ throw new Error("Invalid manifest: stats.activeRecords must be a number");
35
+ if (typeof stats.archivedRecords !== "number")
36
+ throw new Error("Invalid manifest: stats.archivedRecords must be a number");
37
+ if (typeof stats.opsCount !== "number")
38
+ throw new Error("Invalid manifest: stats.opsCount must be a number");
39
+ if (typeof stats.created !== "string")
40
+ throw new Error("Invalid manifest: stats.created must be a string");
41
+ if (typeof stats.lastCheckpoint !== "string")
42
+ throw new Error("Invalid manifest: stats.lastCheckpoint must be a string");
26
43
  return raw;
27
44
  }
28
45
  export function validateSnapshot(raw) {
@@ -30,8 +47,11 @@ export function validateSnapshot(raw) {
30
47
  throw new Error("Invalid snapshot: not an object");
31
48
  }
32
49
  const obj = raw;
33
- if (typeof obj.version !== "number")
34
- throw new Error("Invalid snapshot: missing version");
50
+ if (typeof obj.version !== "number" || !Number.isInteger(obj.version) || obj.version < 1) {
51
+ throw new Error("Invalid snapshot: version must be a positive integer");
52
+ }
53
+ if (typeof obj.timestamp !== "string")
54
+ throw new Error("Invalid snapshot: missing timestamp");
35
55
  if (typeof obj.records !== "object" || obj.records === null || Array.isArray(obj.records)) {
36
56
  throw new Error("Invalid snapshot: records must be an object");
37
57
  }
@@ -42,10 +62,13 @@ export function validateArchiveSegment(raw) {
42
62
  throw new Error("Invalid archive segment: not an object");
43
63
  }
44
64
  const obj = raw;
45
- if (typeof obj.version !== "number")
46
- throw new Error("Invalid archive segment: missing version");
65
+ if (typeof obj.version !== "number" || !Number.isInteger(obj.version) || obj.version < 1) {
66
+ throw new Error("Invalid archive segment: version must be a positive integer");
67
+ }
47
68
  if (typeof obj.period !== "string")
48
69
  throw new Error("Invalid archive segment: missing period");
70
+ if (typeof obj.timestamp !== "string")
71
+ throw new Error("Invalid archive segment: missing timestamp");
49
72
  if (typeof obj.records !== "object" || obj.records === null || Array.isArray(obj.records)) {
50
73
  throw new Error("Invalid archive segment: records must be an object");
51
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backloghq/opslog",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",