@inixiative/archive 0.5.1 → 0.7.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/src/local.ts DELETED
@@ -1,149 +0,0 @@
1
- import { Database } from 'bun:sqlite';
2
- import { randomUUID } from 'node:crypto';
3
- import { chmodSync, mkdirSync } from 'node:fs';
4
- import { dirname } from 'node:path';
5
- import {
6
- type ArchiveChunk,
7
- type ArchiveSnapshot,
8
- archiveKey,
9
- archiveSnapshotSchema,
10
- chunkArchive,
11
- selectChunks,
12
- snapshotDigest,
13
- } from './index';
14
- import { suggestTags } from './tags';
15
-
16
- export class LocalArchiveStore {
17
- private readonly db: Database;
18
- readonly sourceId: string;
19
- constructor(path: string) {
20
- if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
21
- this.db = new Database(path, { create: true });
22
- if (path !== ':memory:') chmodSync(path, 0o600);
23
- this.db.exec(`PRAGMA foreign_keys=ON; PRAGMA busy_timeout=1000;
24
- CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);
25
- CREATE TABLE IF NOT EXISTS archives (id TEXT PRIMARY KEY, digest TEXT NOT NULL, revision INTEGER NOT NULL);
26
- CREATE TABLE IF NOT EXISTS revisions (archive_id TEXT NOT NULL REFERENCES archives(id), revision INTEGER NOT NULL,
27
- digest TEXT NOT NULL, snapshot TEXT NOT NULL, chunks TEXT NOT NULL, PRIMARY KEY(archive_id, revision));
28
- CREATE TABLE IF NOT EXISTS receipts (archive_id TEXT NOT NULL REFERENCES archives(id), destination TEXT NOT NULL,
29
- digest TEXT NOT NULL, PRIMARY KEY(archive_id, destination));
30
- CREATE TABLE IF NOT EXISTS outbox (archive_id TEXT NOT NULL REFERENCES archives(id), destination TEXT NOT NULL,
31
- pending TEXT NOT NULL, PRIMARY KEY(archive_id, destination));`);
32
- this.db.query("INSERT OR IGNORE INTO settings VALUES ('sourceId', ?)").run(randomUUID());
33
- this.sourceId = (
34
- this.db.query("SELECT value FROM settings WHERE key='sourceId'").get() as { value: string }
35
- ).value;
36
- }
37
- capture(input: unknown) {
38
- const snapshot = archiveSnapshotSchema.parse(input);
39
- const id = archiveKey(snapshot);
40
- const hash = snapshotDigest(snapshot);
41
- return this.db.transaction(() => {
42
- const old = this.read(id);
43
- if (old?.digest === hash) return { id, digest: hash, revision: old.revision, changed: false };
44
- if (old && old.snapshot.capturedAt > snapshot.capturedAt)
45
- throw new Error('Older capture cannot replace newer archive');
46
- const revision = (old?.revision ?? 0) + 1;
47
- const chunks = chunkArchive(snapshot);
48
- this.db
49
- .query(
50
- 'INSERT INTO archives VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET digest=excluded.digest, revision=excluded.revision',
51
- )
52
- .run(id, hash, revision);
53
- this.db
54
- .query('INSERT INTO revisions VALUES (?, ?, ?, ?, ?)')
55
- .run(id, revision, hash, JSON.stringify(snapshot), JSON.stringify(chunks));
56
- return { id, digest: hash, revision, changed: true };
57
- })();
58
- }
59
- read(
60
- id: string,
61
- revision?: number,
62
- ):
63
- | {
64
- id: string;
65
- revision: number;
66
- digest: string;
67
- snapshot: ArchiveSnapshot;
68
- chunks: ArchiveChunk[];
69
- }
70
- | undefined {
71
- const row = this.db
72
- .query(`SELECT r.* FROM revisions r JOIN archives a ON a.id=r.archive_id
73
- WHERE a.id=? AND r.revision=COALESCE(?, a.revision)`)
74
- .get(id, revision ?? null) as any;
75
- return row
76
- ? {
77
- id,
78
- revision: row.revision,
79
- digest: row.digest,
80
- snapshot: archiveSnapshotSchema.parse(JSON.parse(row.snapshot)),
81
- chunks: JSON.parse(row.chunks),
82
- }
83
- : undefined;
84
- }
85
- list() {
86
- return (
87
- this.db.query('SELECT id FROM archives ORDER BY rowid DESC').all() as { id: string }[]
88
- ).map(({ id }) => {
89
- const archive = this.read(id)!;
90
- const { entries, ...snapshot } = archive.snapshot;
91
- return {
92
- id,
93
- revision: archive.revision,
94
- digest: archive.digest,
95
- ...snapshot,
96
- entries: entries.length,
97
- suggestedTags: suggestTags(archive.snapshot),
98
- };
99
- });
100
- }
101
- search(ids: string[], query: string, budget = 2048) {
102
- return ids.map((id) => {
103
- const archive = this.read(id);
104
- if (!archive) throw new Error('Archive unavailable');
105
- return {
106
- id,
107
- revision: archive.revision,
108
- digest: archive.digest,
109
- ...selectChunks(archive.chunks, query, budget),
110
- };
111
- });
112
- }
113
- receipt(id: string, destination: string): string | null {
114
- return (
115
- (
116
- this.db
117
- .query('SELECT digest FROM receipts WHERE archive_id=? AND destination=?')
118
- .get(id, destination) as { digest: string } | null
119
- )?.digest ?? null
120
- );
121
- }
122
- acknowledge(id: string, destination: string, hash: string) {
123
- this.db
124
- .query(
125
- 'INSERT INTO receipts VALUES (?, ?, ?) ON CONFLICT(archive_id,destination) DO UPDATE SET digest=excluded.digest',
126
- )
127
- .run(id, destination, hash);
128
- }
129
- pending(id: string, destination: string): { revision: number } | null {
130
- const row = this.db
131
- .query('SELECT pending FROM outbox WHERE archive_id=? AND destination=?')
132
- .get(id, destination) as { pending: string } | null;
133
- return row ? JSON.parse(row.pending) : null;
134
- }
135
- enqueue(id: string, destination: string, pending: { revision: number }) {
136
- this.db
137
- .query('INSERT OR IGNORE INTO outbox VALUES (?, ?, ?)')
138
- .run(id, destination, JSON.stringify(pending));
139
- }
140
- delivered(id: string, destination: string, hash: string) {
141
- this.db.transaction(() => {
142
- this.acknowledge(id, destination, hash);
143
- this.db.query('DELETE FROM outbox WHERE archive_id=? AND destination=?').run(id, destination);
144
- })();
145
- }
146
- close() {
147
- this.db.close();
148
- }
149
- }