@backloghq/opslog 0.7.1 → 0.8.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 CHANGED
@@ -140,6 +140,18 @@ store.stats() // { activeRecords, opsCount, archiveSegments
140
140
  await store.refresh() // Reload from all agent WALs (multi-writer only)
141
141
  ```
142
142
 
143
+ ### Blob Storage (via StorageBackend)
144
+
145
+ ```typescript
146
+ await backend.writeBlob("data/file.jsonl", buffer); // Write at relative path
147
+ const buf = await backend.readBlob("data/file.jsonl"); // Read full file
148
+ const range = await backend.readBlobRange("data/file.jsonl", 1024, 256); // Byte-range read
149
+ const names = await backend.listBlobs("data"); // List files under prefix
150
+ await backend.deleteBlob("data/file.jsonl"); // Delete file
151
+ ```
152
+
153
+ `readBlobRange` enables O(1) point lookups in JSONL record stores — seek to byte offset, read exact length. FsBackend uses `fs.read` with file offset. S3Backend uses HTTP Range header.
154
+
143
155
  ## Options
144
156
 
145
157
  ```typescript
package/dist/backend.d.ts CHANGED
@@ -29,6 +29,7 @@ export declare class FsBackend implements StorageBackend {
29
29
  getManifestVersion(): Promise<string | null>;
30
30
  writeBlob(relativePath: string, content: Buffer): Promise<void>;
31
31
  readBlob(relativePath: string): Promise<Buffer>;
32
+ readBlobRange(relativePath: string, offset: number, length: number): Promise<Buffer>;
32
33
  listBlobs(prefix: string): Promise<string[]>;
33
34
  deleteBlob(relativePath: string): Promise<void>;
34
35
  deleteBlobDir(prefix: string): Promise<void>;
package/dist/backend.js CHANGED
@@ -140,6 +140,21 @@ export class FsBackend {
140
140
  async readBlob(relativePath) {
141
141
  return readFile(join(this.dir, relativePath));
142
142
  }
143
+ async readBlobRange(relativePath, offset, length) {
144
+ if (offset < 0 || length < 0)
145
+ throw new Error("readBlobRange: offset and length must be non-negative");
146
+ if (length === 0)
147
+ return Buffer.alloc(0);
148
+ const fd = await open(join(this.dir, relativePath), "r");
149
+ try {
150
+ const buf = Buffer.alloc(length);
151
+ const { bytesRead } = await fd.read(buf, 0, length, offset);
152
+ return bytesRead < length ? buf.subarray(0, bytesRead) : buf;
153
+ }
154
+ finally {
155
+ await fd.close();
156
+ }
157
+ }
143
158
  async listBlobs(prefix) {
144
159
  try {
145
160
  return await readdir(join(this.dir, prefix));
package/dist/snapshot.js CHANGED
@@ -27,9 +27,19 @@ export async function loadSnapshot(dir, relativePath) {
27
27
  // Detect format: JSONL (first line is header without "records" key) vs legacy JSON
28
28
  const firstNewline = content.indexOf("\n");
29
29
  const firstLine = firstNewline === -1 ? content : content.slice(0, firstNewline);
30
- const parsed = JSON.parse(firstLine);
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(firstLine);
33
+ }
34
+ catch {
35
+ // First line isn't valid JSON (e.g., pretty-printed legacy JSON starts with "{")
36
+ // Fall back to parsing the full content as legacy JSON
37
+ const snapshot = validateSnapshot(JSON.parse(content));
38
+ const records = new Map(Object.entries(snapshot.records));
39
+ return { records, version: snapshot.version };
40
+ }
31
41
  if ("records" in parsed) {
32
- // Legacy monolithic JSON format
42
+ // Legacy monolithic JSON format (single-line)
33
43
  const snapshot = validateSnapshot(parsed);
34
44
  const records = new Map(Object.entries(snapshot.records));
35
45
  return { records, version: snapshot.version };
@@ -58,7 +68,14 @@ export async function* streamSnapshotFile(dir, relativePath) {
58
68
  const content = await readFile(path, "utf-8");
59
69
  const firstNewline = content.indexOf("\n");
60
70
  const firstLine = firstNewline === -1 ? content : content.slice(0, firstNewline);
61
- const parsed = JSON.parse(firstLine);
71
+ let parsed;
72
+ try {
73
+ parsed = JSON.parse(firstLine);
74
+ }
75
+ catch {
76
+ // Pretty-printed legacy JSON — first line isn't valid JSON
77
+ parsed = JSON.parse(content);
78
+ }
62
79
  if ("records" in parsed) {
63
80
  // Legacy JSON: must load all, then yield
64
81
  const snapshot = validateSnapshot(parsed);
package/dist/types.d.ts CHANGED
@@ -115,6 +115,8 @@ export interface StorageBackend {
115
115
  writeBlob(relativePath: string, content: Buffer): Promise<void>;
116
116
  /** Read a blob from a relative path. */
117
117
  readBlob(relativePath: string): Promise<Buffer>;
118
+ /** Read a byte range from a blob. For O(1) point lookups in record stores. */
119
+ readBlobRange(relativePath: string, offset: number, length: number): Promise<Buffer>;
118
120
  /** List blob names under a prefix directory. */
119
121
  listBlobs(prefix: string): Promise<string[]>;
120
122
  /** Delete a single blob. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backloghq/opslog",
3
- "version": "0.7.1",
3
+ "version": "0.8.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",