@hraness/sponge 0.2.6 → 0.3.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 +21 -9
- package/bin/sponge.js +8 -7
- package/dist/contracts/index.js +24 -6
- package/dist/contracts/validate.js +106 -29
- package/dist/migrations/008.d.ts +3 -0
- package/dist/migrations/008.js +5 -0
- package/dist/migrations/009.d.ts +3 -0
- package/dist/migrations/009.js +8 -0
- package/dist/migrations/010.d.ts +3 -0
- package/dist/migrations/010.js +11 -0
- package/dist/migrations/index.d.ts +15 -0
- package/dist/migrations/index.js +39 -0
- package/dist/src/acquisition/capture.js +87 -28
- package/dist/src/acquisition/cleanup.d.ts +0 -4
- package/dist/src/acquisition/cleanup.js +3 -5
- package/dist/src/acquisition/errors.js +17 -2
- package/dist/src/acquisition/files.d.ts +0 -1
- package/dist/src/acquisition/files.js +14 -11
- package/dist/src/acquisition/index.d.ts +2 -3
- package/dist/src/acquisition/index.js +1 -2
- package/dist/src/acquisition/lock.js +1 -1
- package/dist/src/acquisition/normalize.js +57 -18
- package/dist/src/acquisition/pins.js +22 -4
- package/dist/src/acquisition/process.js +67 -29
- package/dist/src/acquisition/runtime.d.ts +5 -7
- package/dist/src/acquisition/runtime.js +129 -68
- package/dist/src/acquisition/verify-helper.js +10 -3
- package/dist/src/annotations/quote.js +40 -12
- package/dist/src/annotations/store.d.ts +0 -1
- package/dist/src/annotations/store.js +66 -14
- package/dist/src/app/article.d.ts +2 -1
- package/dist/src/app/article.js +66 -70
- package/dist/src/app/bundle-files.d.ts +0 -4
- package/dist/src/app/bundle-files.js +11 -14
- package/dist/src/app/markdown.js +18 -6
- package/dist/src/app/source.d.ts +0 -4
- package/dist/src/app/source.js +18 -14
- package/dist/src/cli/arguments.d.ts +2 -8
- package/dist/src/cli/arguments.js +67 -80
- package/dist/src/cli/commands.d.ts +42 -0
- package/dist/src/cli/commands.js +795 -0
- package/dist/src/cli/config.js +12 -9
- package/dist/src/cli/digest.d.ts +1 -1
- package/dist/src/cli/digest.js +38 -16
- package/dist/src/cli/import.js +25 -4
- package/dist/src/cli/indexing.d.ts +2 -0
- package/dist/src/cli/indexing.js +26 -9
- package/dist/src/cli/main.js +47 -386
- package/dist/src/cli/portability.d.ts +0 -2
- package/dist/src/cli/portability.js +13 -8
- package/dist/src/cli/queue.js +12 -8
- package/dist/src/cli/runtimes.d.ts +1 -3
- package/dist/src/cli/runtimes.js +13 -6
- package/dist/src/cli/terminal.js +1 -1
- package/dist/src/cli/worker.d.ts +7 -0
- package/dist/src/cli/worker.js +54 -15
- package/dist/src/derivations/kinds.d.ts +34 -0
- package/dist/src/derivations/kinds.js +14 -0
- package/dist/src/derivations/types.d.ts +48 -0
- package/dist/src/derivations/types.js +1 -0
- package/dist/src/digest/derivation.d.ts +25 -0
- package/dist/src/digest/derivation.js +124 -0
- package/dist/src/digest/editorial.d.ts +9 -46
- package/dist/src/digest/editorial.js +57 -49
- package/dist/src/digest/endpoint.js +63 -27
- package/dist/src/digest/pipeline.d.ts +49 -28
- package/dist/src/digest/pipeline.js +369 -102
- package/dist/src/digest/schema.d.ts +11 -25
- package/dist/src/digest/schema.js +86 -18
- package/dist/src/errors.d.ts +46 -0
- package/dist/src/errors.js +117 -0
- package/dist/src/identity/query-policy.d.ts +0 -5
- package/dist/src/identity/query-policy.js +61 -9
- package/dist/src/identity/uri.d.ts +6 -5
- package/dist/src/identity/uri.js +37 -18
- package/dist/src/identity/visibility.d.ts +11 -1
- package/dist/src/identity/visibility.js +81 -17
- package/dist/src/importers/apply.js +104 -36
- package/dist/src/importers/custody.js +27 -9
- package/dist/src/importers/inspect.js +125 -38
- package/dist/src/importers/jungle-digest.d.ts +3 -6
- package/dist/src/importers/jungle-digest.js +196 -69
- package/dist/src/importers/jungle-store.d.ts +19 -6
- package/dist/src/importers/jungle-store.js +124 -28
- package/dist/src/importers/jungle.d.ts +14 -14
- package/dist/src/importers/jungle.js +142 -31
- package/dist/src/importers/manifest.js +57 -21
- package/dist/src/importers/verify-helper.js +18 -4
- package/dist/src/indexing/image.js +29 -10
- package/dist/src/indexing/index.d.ts +37 -6
- package/dist/src/indexing/index.js +360 -124
- package/dist/src/indexing/jobs.js +156 -47
- package/dist/src/indexing/prune-run.d.ts +2 -0
- package/dist/src/indexing/prune-run.js +69 -26
- package/dist/src/indexing/prune.d.ts +12 -3
- package/dist/src/indexing/prune.js +85 -35
- package/dist/src/indexing/runner.d.ts +2 -1
- package/dist/src/indexing/runner.js +83 -40
- package/dist/src/indexing/upgrade.d.ts +9 -0
- package/dist/src/indexing/upgrade.js +79 -0
- package/dist/src/inference/asr/bridge.js +6 -2
- package/dist/src/inference/asr/protocol.js +109 -19
- package/dist/src/inference/asr/runtime.js +58 -21
- package/dist/src/inference/asr/windows.js +9 -5
- package/dist/src/inference/embedding/index.d.ts +2 -6
- package/dist/src/inference/embedding/index.js +89 -29
- package/dist/src/ingest/attempt.d.ts +29 -0
- package/dist/src/ingest/attempt.js +55 -0
- package/dist/src/media/capture-helper.js +7 -2
- package/dist/src/media/capture.d.ts +2 -1
- package/dist/src/media/capture.js +124 -43
- package/dist/src/media/handler.d.ts +4 -2
- package/dist/src/media/handler.js +88 -70
- package/dist/src/media/transcribe.js +63 -21
- package/dist/src/metadata/article.js +49 -17
- package/dist/src/models/artifacts.d.ts +10 -8
- package/dist/src/models/artifacts.js +237 -106
- package/dist/src/models/catalog.d.ts +2 -5
- package/dist/src/models/catalog.js +94 -25
- package/dist/src/models/download.js +55 -15
- package/dist/src/models/runtime.d.ts +0 -2
- package/dist/src/models/runtime.js +69 -33
- package/dist/src/pdf/capture-helper.js +3 -1
- package/dist/src/pdf/capture.d.ts +2 -1
- package/dist/src/pdf/capture.js +95 -27
- package/dist/src/pdf/errors.js +12 -1
- package/dist/src/pdf/handler.d.ts +2 -1
- package/dist/src/pdf/handler.js +70 -71
- package/dist/src/pdf/index.d.ts +1 -3
- package/dist/src/pdf/index.js +0 -2
- package/dist/src/pdf/input.js +12 -7
- package/dist/src/pdf/normalize.js +80 -21
- package/dist/src/pdf/source-identity.js +3 -2
- package/dist/src/pdf/source.js +12 -8
- package/dist/src/pdf/verify-helper.js +15 -3
- package/dist/src/portability/backup.d.ts +2 -9
- package/dist/src/portability/backup.js +106 -22
- package/dist/src/portability/database.js +51 -17
- package/dist/src/portability/export.js +35 -9
- package/dist/src/portability/files.d.ts +1 -4
- package/dist/src/portability/files.js +127 -67
- package/dist/src/portability/gc.js +65 -22
- package/dist/src/publish/component.js +1 -1
- package/dist/src/publish/feed.d.ts +2 -2
- package/dist/src/publish/feed.js +75 -29
- package/dist/src/publish/identity.d.ts +5 -1
- package/dist/src/publish/identity.js +31 -11
- package/dist/src/publish/index.d.ts +2 -3
- package/dist/src/publish/index.js +34 -21
- package/dist/src/publish/projection.d.ts +2 -9
- package/dist/src/publish/projection.js +21 -14
- package/dist/src/search/extension.js +3 -1
- package/dist/src/search/hybrid.d.ts +3 -8
- package/dist/src/search/hybrid.js +45 -11
- package/dist/src/search/lexical.js +18 -4
- package/dist/src/search/query.js +56 -25
- package/dist/src/search/vector.js +37 -10
- package/dist/src/service/launch-agent.d.ts +5 -3
- package/dist/src/service/launch-agent.js +272 -83
- package/dist/src/service/lifetime.js +47 -14
- package/dist/src/service/loop.d.ts +3 -3
- package/dist/src/service/loop.js +47 -19
- package/dist/src/store/annotations.d.ts +32 -0
- package/dist/src/store/annotations.js +44 -0
- package/dist/src/store/backups.d.ts +18 -0
- package/dist/src/store/backups.js +110 -0
- package/dist/src/store/blobs.d.ts +24 -0
- package/dist/src/store/blobs.js +139 -0
- package/dist/src/store/changes.d.ts +5 -12
- package/dist/src/store/changes.js +108 -56
- package/dist/src/store/context.d.ts +15 -0
- package/dist/src/store/context.js +19 -0
- package/dist/src/store/derivations.d.ts +42 -0
- package/dist/src/store/derivations.js +104 -0
- package/dist/src/store/files.d.ts +0 -2
- package/dist/src/store/files.js +19 -24
- package/dist/src/store/ids.d.ts +30 -0
- package/dist/src/store/ids.js +34 -0
- package/dist/src/store/imports.d.ts +26 -0
- package/dist/src/store/imports.js +57 -0
- package/dist/src/store/index.d.ts +3 -186
- package/dist/src/store/index.js +2 -809
- package/dist/src/store/lease.d.ts +20 -0
- package/dist/src/store/lease.js +62 -0
- package/dist/src/store/library.d.ts +75 -0
- package/dist/src/store/library.js +174 -0
- package/dist/src/store/open.d.ts +7 -0
- package/dist/src/store/open.js +183 -0
- package/dist/src/store/queue.d.ts +65 -0
- package/dist/src/store/queue.js +327 -0
- package/dist/src/store/revisions.d.ts +12 -0
- package/dist/src/store/revisions.js +189 -0
- package/dist/src/store/search-index.d.ts +63 -0
- package/dist/src/store/search-index.js +190 -0
- package/dist/src/store/sync-ready.d.ts +6 -0
- package/dist/src/store/sync-ready.js +121 -0
- package/dist/src/store/types.d.ts +1 -5
- package/dist/src/store/types.js +1 -8
- package/dist/src/util/canonical.d.ts +2 -0
- package/dist/src/util/canonical.js +17 -0
- package/dist/src/util/exact.d.ts +2 -0
- package/dist/src/util/exact.js +9 -0
- package/dist/src/util/fsync.d.ts +4 -0
- package/dist/src/util/fsync.js +22 -0
- package/dist/src/util/hash.d.ts +4 -0
- package/dist/src/util/hash.js +8 -0
- package/dist/src/worker/basic.d.ts +0 -1
- package/dist/src/worker/basic.js +23 -7
- package/dist/src/worker/custody.d.ts +4 -1
- package/dist/src/worker/custody.js +111 -38
- package/dist/src/worker/engine.d.ts +2 -1
- package/dist/src/worker/engine.js +42 -20
- package/dist/src/worker/identity.js +13 -5
- package/dist/src/worker/journal.d.ts +1 -1
- package/dist/src/worker/journal.js +54 -24
- package/dist/src/worker/lock.d.ts +18 -0
- package/dist/src/worker/lock.js +196 -0
- package/dist/src/worker/runtime-gc.d.ts +4 -3
- package/dist/src/worker/runtime-gc.js +68 -41
- package/dist/src/worker/supervisor-child.js +19 -6
- package/dist/src/worker/supervisor.js +41 -8
- package/native/runtime-manifest.json +4 -4
- package/package.json +10 -2
- package/dist/src/digest/store.d.ts +0 -57
- package/dist/src/digest/store.js +0 -79
- package/dist/src/store/foundation.d.ts +0 -7
- package/dist/src/store/foundation.js +0 -7
|
@@ -1,49 +1,158 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { ownerMatches, pendingCleanup } from '../store/lease.js';
|
|
3
|
+
import { SpongeError } from '../errors.js';
|
|
4
|
+
import { syncGeneration } from './index.js';
|
|
3
5
|
const now = () => new Date().toISOString();
|
|
4
|
-
function atomic(db, fn) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
db.exec('
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
export function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
export function
|
|
49
|
-
db.prepare(
|
|
6
|
+
function atomic(db, fn) {
|
|
7
|
+
if (db.isTransaction)
|
|
8
|
+
return fn();
|
|
9
|
+
db.exec('BEGIN IMMEDIATE');
|
|
10
|
+
try {
|
|
11
|
+
const result = fn();
|
|
12
|
+
db.exec('COMMIT');
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
db.exec('ROLLBACK');
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function attempt(db, token, active = true) {
|
|
21
|
+
ownerMatches(db, token);
|
|
22
|
+
const row = db
|
|
23
|
+
.prepare('SELECT a.*,j.state,j.cancel_requested,j.generation_id FROM index_attempts a JOIN index_jobs j ON j.id=a.job_id WHERE a.id=? AND a.job_id=?')
|
|
24
|
+
.get(token.attemptId, token.jobId);
|
|
25
|
+
if (!row ||
|
|
26
|
+
row.owner_incarnation !== token.ownerIncarnation ||
|
|
27
|
+
row.fence !== token.fence ||
|
|
28
|
+
(active && (row.finished_at !== null || row.state !== 'running')))
|
|
29
|
+
throw new SpongeError('owner-busy');
|
|
30
|
+
if (active && row.cancel_requested === 1)
|
|
31
|
+
throw new SpongeError('cancelled');
|
|
32
|
+
return row;
|
|
33
|
+
}
|
|
34
|
+
export function submitIndexJob(db, generationId) {
|
|
35
|
+
return atomic(db, () => {
|
|
36
|
+
const generation = db.prepare('SELECT state FROM index_generations WHERE id=?').get(generationId);
|
|
37
|
+
if (!generation || generation.state !== 'building')
|
|
38
|
+
throw new SpongeError('unavailable');
|
|
39
|
+
const prior = db.prepare('SELECT id FROM index_jobs WHERE generation_id=?').get(generationId);
|
|
40
|
+
if (prior)
|
|
41
|
+
return String(prior.id);
|
|
42
|
+
const id = randomUUID();
|
|
43
|
+
db.prepare("INSERT INTO index_jobs(id,generation_id,state,created_at) VALUES(?,?,'queued',?)").run(id, generationId, now());
|
|
44
|
+
return id;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function listIndexJobs(db) {
|
|
48
|
+
return db.prepare("SELECT *, 'index' AS operation, NULL AS item_id FROM index_jobs ORDER BY created_at,id").all();
|
|
49
|
+
}
|
|
50
|
+
export function indexJob(db, id) {
|
|
51
|
+
return db.prepare('SELECT * FROM index_jobs WHERE id=?').get(id);
|
|
52
|
+
}
|
|
53
|
+
export function claimIndexJob(db, token, id) {
|
|
54
|
+
return atomic(db, () => {
|
|
55
|
+
ownerMatches(db, token);
|
|
56
|
+
const job = id
|
|
57
|
+
? indexJob(db, id)
|
|
58
|
+
: db
|
|
59
|
+
.prepare("SELECT * FROM index_jobs WHERE state='queued' AND cancel_requested=0 ORDER BY created_at,id LIMIT 1")
|
|
60
|
+
.get();
|
|
61
|
+
if (!job) {
|
|
62
|
+
if (id)
|
|
63
|
+
throw new SpongeError('unavailable');
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (job.state !== 'queued' || job.cancel_requested)
|
|
67
|
+
throw new SpongeError('unavailable');
|
|
68
|
+
if (pendingCleanup(db))
|
|
69
|
+
throw new SpongeError('owner-busy');
|
|
70
|
+
const attemptId = randomUUID();
|
|
71
|
+
db.prepare("UPDATE index_jobs SET state='running',attempt_count=attempt_count+1 WHERE id=?").run(job.id);
|
|
72
|
+
db.prepare("INSERT INTO index_attempts VALUES(?,?,?,?,'acquiring','pending',?,NULL)").run(attemptId, job.id, token.ownerIncarnation, token.fence, now());
|
|
73
|
+
return { ...token, jobId: String(job.id), attemptId };
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function withIndexAttempt(db, token, fn) {
|
|
77
|
+
return atomic(db, () => {
|
|
78
|
+
const row = attempt(db, token);
|
|
79
|
+
return fn(String(row.generation_id));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export function checkpointIndexAttempt(db, token, stage) {
|
|
83
|
+
const stages = ['acquiring', 'extracting', 'indexing', 'promoting', 'cleanup'];
|
|
84
|
+
atomic(db, () => {
|
|
85
|
+
const row = attempt(db, token);
|
|
86
|
+
if (!stages.includes(stage) || stages.indexOf(stage) < stages.indexOf(String(row.stage)))
|
|
87
|
+
throw new SpongeError('invalid-input');
|
|
88
|
+
db.prepare('UPDATE index_attempts SET stage=? WHERE id=?').run(stage, token.attemptId);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function finishIndexAttempt(db, token, reason) {
|
|
92
|
+
atomic(db, () => {
|
|
93
|
+
const row = attempt(db, token, false);
|
|
94
|
+
if (row.finished_at !== null || row.state !== 'running')
|
|
95
|
+
throw new SpongeError('owner-busy');
|
|
96
|
+
if (reason !== undefined && !/^[a-z][a-z-]{0,63}$/.test(reason))
|
|
97
|
+
throw new SpongeError('invalid-input');
|
|
98
|
+
if (!reason && row.cancel_requested)
|
|
99
|
+
throw new SpongeError('cancelled');
|
|
100
|
+
db.prepare('UPDATE index_jobs SET state=?,reason=? WHERE id=?').run(row.cancel_requested ? 'cancelled' : reason ? 'failed' : 'succeeded', reason ?? null, token.jobId);
|
|
101
|
+
db.prepare("UPDATE index_attempts SET stage='cleanup',finished_at=? WHERE id=?").run(now(), token.attemptId);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function settleIndexAttempt(db, token) {
|
|
105
|
+
atomic(db, () => {
|
|
106
|
+
const row = attempt(db, token, false);
|
|
107
|
+
if (row.finished_at === null)
|
|
108
|
+
throw new SpongeError('owner-busy');
|
|
109
|
+
db.prepare("UPDATE index_attempts SET cleanup='settled',stage='settled' WHERE id=?").run(token.attemptId);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export function cancelIndexJob(db, id) {
|
|
113
|
+
atomic(db, () => {
|
|
114
|
+
const row = indexJob(db, id);
|
|
115
|
+
if (!row)
|
|
116
|
+
throw new SpongeError('unavailable');
|
|
117
|
+
if (row.state === 'succeeded')
|
|
118
|
+
return;
|
|
119
|
+
db.prepare("UPDATE index_jobs SET cancel_requested=1,state=CASE WHEN state IN ('queued','failed','cancelled') THEN 'cancelled' ELSE state END WHERE id=?").run(id);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export function retryIndexJob(db, id) {
|
|
123
|
+
atomic(db, () => {
|
|
124
|
+
const row = indexJob(db, id);
|
|
125
|
+
if (!row || !['failed', 'cancelled'].includes(String(row.state)))
|
|
126
|
+
throw new SpongeError('unavailable');
|
|
127
|
+
if (db.prepare("SELECT count(*) AS n FROM index_attempts WHERE job_id=? AND cleanup='pending'").get(id)?.n)
|
|
128
|
+
throw new SpongeError('owner-busy');
|
|
129
|
+
const generation = db.prepare('SELECT state FROM index_generations WHERE id=?').get(row.generation_id);
|
|
130
|
+
if (!generation || !['building', 'active'].includes(String(generation.state)))
|
|
131
|
+
throw new SpongeError('unavailable');
|
|
132
|
+
db.prepare("UPDATE index_jobs SET state='queued',cancel_requested=0,reason=NULL WHERE id=?").run(id); // A retried generation catches up with captures made while it was stopped.
|
|
133
|
+
syncGeneration(db, String(row.generation_id));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export function indexOwnerAttempts(db, token) {
|
|
137
|
+
ownerMatches(db, token);
|
|
138
|
+
return db
|
|
139
|
+
.prepare('SELECT a.*,j.state,j.cancel_requested FROM index_attempts a JOIN index_jobs j ON j.id=a.job_id WHERE a.owner_incarnation=? AND a.fence=? ORDER BY a.created_at,a.id')
|
|
140
|
+
.all(token.ownerIncarnation, token.fence)
|
|
141
|
+
.map((a) => ({
|
|
142
|
+
...token,
|
|
143
|
+
kind: 'index',
|
|
144
|
+
attemptId: String(a.id),
|
|
145
|
+
jobId: String(a.job_id),
|
|
146
|
+
stage: String(a.stage),
|
|
147
|
+
cleanup: a.cleanup,
|
|
148
|
+
finished: a.finished_at !== null,
|
|
149
|
+
jobState: String(a.state),
|
|
150
|
+
cancelRequested: a.cancel_requested === 1,
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
export function recoverIndexAttempt(db, token) {
|
|
154
|
+
const row = attempt(db, token, false);
|
|
155
|
+
db.prepare("UPDATE index_attempts SET cleanup='settled',stage='settled',finished_at=COALESCE(finished_at,?) WHERE id=?").run(now(), token.attemptId);
|
|
156
|
+
if (row.finished_at === null)
|
|
157
|
+
db.prepare("UPDATE index_jobs SET state=CASE WHEN cancel_requested=1 THEN 'cancelled' ELSE 'failed' END,reason='unavailable' WHERE id=?").run(token.jobId);
|
|
158
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type LibraryStore } from '../store/index.js';
|
|
2
2
|
import { type PageStats, type PruneEntry } from './prune.js';
|
|
3
|
+
/** `prunedInputs` are coverage rows of pruned generations; `prunedVectors` embeddings no retained generation covers (deleted items' too). */
|
|
3
4
|
export interface PruneReport {
|
|
4
5
|
dryRun: boolean;
|
|
5
6
|
keep: number;
|
|
6
7
|
generations: PruneEntry[];
|
|
8
|
+
prunedInputs: number;
|
|
7
9
|
prunedVectors: number;
|
|
8
10
|
estimatedBytes: number;
|
|
9
11
|
before: PageStats;
|
|
@@ -1,67 +1,110 @@
|
|
|
1
1
|
import { rm } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { StoreError } from '../store/index.js';
|
|
4
3
|
import { acquireWorkerCustody } from '../worker/custody.js';
|
|
5
|
-
import {
|
|
4
|
+
import { syncDirectoryAsync } from '../util/fsync.js';
|
|
6
5
|
import { maximumKeep } from './prune.js';
|
|
6
|
+
import { SpongeError } from '../errors.js';
|
|
7
7
|
const note = 'Rollback to a pruned generation needs a rebuild (`sponge index create`).';
|
|
8
|
-
const blocked = (store) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const blocked = (store) => {
|
|
9
|
+
if (store.queue.ownerSnapshot())
|
|
10
|
+
throw new SpongeError('owner-busy');
|
|
11
|
+
if (store.queue.pendingCleanup())
|
|
12
|
+
throw new SpongeError('cleanup-required');
|
|
13
|
+
if (store.blobs.hasGcFence())
|
|
14
|
+
throw new SpongeError('owner-busy');
|
|
15
|
+
};
|
|
16
|
+
const total = (entries, key) => entries.filter((g) => g.action === 'prune').reduce((n, g) => n + g[key], 0);
|
|
13
17
|
/** Holds worker custody and the store owner fence for the whole run, so no worker, index run or query starts;
|
|
14
18
|
* each bounded batch rechecks owner, cleanup and backup/GC fences in its own transaction. Refuses while index work runs. */
|
|
15
19
|
export async function pruneIndex(store, options = {}) {
|
|
16
20
|
const keep = options.keep ?? 1, dryRun = options.dryRun === true;
|
|
17
21
|
if (!Number.isSafeInteger(keep) || keep < 0 || keep > maximumKeep)
|
|
18
|
-
throw new
|
|
22
|
+
throw new SpongeError('invalid-input');
|
|
19
23
|
if (dryRun) {
|
|
20
|
-
const { generations, stats } = store.
|
|
21
|
-
return {
|
|
24
|
+
const { generations, orphans, stats } = store.index.prunePlan(keep);
|
|
25
|
+
return {
|
|
26
|
+
dryRun,
|
|
27
|
+
keep,
|
|
28
|
+
generations,
|
|
29
|
+
prunedInputs: total(generations, 'inputs'),
|
|
30
|
+
prunedVectors: orphans.vectors,
|
|
31
|
+
estimatedBytes: total(generations, 'bytes') + orphans.bytes,
|
|
32
|
+
before: stats,
|
|
33
|
+
after: stats,
|
|
34
|
+
reclaimedBytes: 0,
|
|
35
|
+
vacuum: options.vacuum ? 'would-run' : 'skipped',
|
|
36
|
+
note,
|
|
37
|
+
};
|
|
22
38
|
}
|
|
39
|
+
await store.backups.expireFences();
|
|
23
40
|
blocked(store);
|
|
24
41
|
const custody = await acquireWorkerCustody(store.root);
|
|
25
42
|
let owner;
|
|
26
43
|
try {
|
|
27
|
-
owner = store.acquireOwner(custody.incarnation);
|
|
28
|
-
const { generations, stats: before } = store.
|
|
29
|
-
let prunedVectors = 0;
|
|
30
|
-
for (const generation of generations.filter(g => g.action === 'prune'))
|
|
44
|
+
owner = store.queue.acquireOwner(custody.incarnation);
|
|
45
|
+
const { generations, orphans, stats: before } = store.index.prunePlan(keep);
|
|
46
|
+
let prunedInputs = 0, prunedVectors = 0;
|
|
47
|
+
for (const generation of generations.filter((g) => g.action === 'prune'))
|
|
31
48
|
for (;;) {
|
|
32
|
-
const n = store.
|
|
33
|
-
|
|
49
|
+
const n = store.index.pruneStep(owner, generation.id, keep);
|
|
50
|
+
prunedInputs += n;
|
|
34
51
|
if (!n)
|
|
35
52
|
break;
|
|
36
53
|
}
|
|
54
|
+
for (;;) {
|
|
55
|
+
const n = store.index.gcStep(owner);
|
|
56
|
+
prunedVectors += n;
|
|
57
|
+
if (!n)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
37
60
|
// A concurrent rollback can make a planned generation a kept one again; report what actually happened.
|
|
38
|
-
const now = new Map(store.
|
|
39
|
-
|
|
61
|
+
const now = new Map(store.index.prunePlan(keep).generations.map((g) => [g.id, g])), final = generations.map((g) => {
|
|
62
|
+
const current = now.get(g.id);
|
|
63
|
+
return g.action === 'prune' && current?.action === 'keep' ? current : g;
|
|
64
|
+
});
|
|
65
|
+
let after = store.index.prunePlan(keep).stats, vacuum = 'skipped';
|
|
40
66
|
if (options.vacuum) {
|
|
41
|
-
after = store.
|
|
67
|
+
after = store.index.vacuum(owner);
|
|
42
68
|
vacuum = 'done';
|
|
43
69
|
}
|
|
44
|
-
return {
|
|
70
|
+
return {
|
|
71
|
+
dryRun,
|
|
72
|
+
keep,
|
|
73
|
+
generations: final,
|
|
74
|
+
prunedInputs,
|
|
75
|
+
prunedVectors,
|
|
76
|
+
estimatedBytes: total(final, 'bytes') + orphans.bytes,
|
|
77
|
+
before,
|
|
78
|
+
after,
|
|
79
|
+
reclaimedBytes: vacuum === 'done' ? before.bytes - after.bytes : (after.freePages - before.freePages) * after.pageSize,
|
|
80
|
+
vacuum,
|
|
81
|
+
note,
|
|
82
|
+
};
|
|
45
83
|
}
|
|
46
84
|
finally {
|
|
47
85
|
try {
|
|
48
86
|
if (owner)
|
|
49
|
-
store.releaseOwner(owner);
|
|
87
|
+
store.queue.releaseOwner(owner);
|
|
50
88
|
}
|
|
51
89
|
finally {
|
|
52
90
|
await custody.release();
|
|
53
91
|
try {
|
|
54
92
|
await rm(custody.directory, { recursive: true });
|
|
55
|
-
await
|
|
93
|
+
await syncDirectoryAsync(join(store.root, 'runtime', 'workers'));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* Runtime GC removes this settled journal. */
|
|
56
97
|
}
|
|
57
|
-
catch { /* Runtime GC removes this settled journal. */ }
|
|
58
98
|
}
|
|
59
99
|
}
|
|
60
100
|
}
|
|
61
101
|
export function pruneText(report) {
|
|
62
|
-
const lines = report.generations.map(g => `${g.action === 'prune' ? (report.dryRun ? 'would prune' : 'pruned') : 'kept'}\t${g.id}\t${g.profileId}\t${g.space}\t${g.state}\t${g.reason}\t${g.
|
|
63
|
-
lines.push(report.dryRun ?
|
|
64
|
-
|
|
102
|
+
const lines = report.generations.map((g) => `${g.action === 'prune' ? (report.dryRun ? 'would prune' : 'pruned') : 'kept'}\t${g.id}\t${g.profileId}\t${g.space}\t${g.state}\t${g.reason}\t${g.inputs} inputs`);
|
|
103
|
+
lines.push(`${report.dryRun ? 'Would delete' : 'Deleted'} ${report.prunedVectors} uncovered vectors`);
|
|
104
|
+
lines.push(report.dryRun
|
|
105
|
+
? `Would free about ${report.estimatedBytes} bytes of rows${report.vacuum === 'would-run' ? '; --vacuum would then shrink the file' : ''}`
|
|
106
|
+
: `Reclaimed ${report.reclaimedBytes} bytes (${report.vacuum === 'done' ? 'file shrank after VACUUM' : 'freed pages are reused; --vacuum shrinks the file'})`);
|
|
107
|
+
if (report.generations.some((g) => g.action === 'prune'))
|
|
65
108
|
lines.push(report.note);
|
|
66
109
|
return lines.join('\n');
|
|
67
110
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
|
|
2
|
+
type PruneReason = 'active' | 'building' | 'queued' | 'running' | 'rollback' | 'unknown' | 'retired' | 'stale' | 'cancelled' | 'failed' | 'pruned';
|
|
3
|
+
/** `inputs` are the generation's coverage rows; `bytes` their size. Vectors are shared by content and counted by `orphanVectors`. */
|
|
3
4
|
export interface PruneEntry {
|
|
4
5
|
id: string;
|
|
5
6
|
profileId: string;
|
|
@@ -8,7 +9,7 @@ export interface PruneEntry {
|
|
|
8
9
|
state: string;
|
|
9
10
|
action: 'keep' | 'prune';
|
|
10
11
|
reason: PruneReason;
|
|
11
|
-
|
|
12
|
+
inputs: number;
|
|
12
13
|
bytes: number;
|
|
13
14
|
}
|
|
14
15
|
export interface PageStats {
|
|
@@ -17,7 +18,7 @@ export interface PageStats {
|
|
|
17
18
|
freePages: number;
|
|
18
19
|
bytes: number;
|
|
19
20
|
}
|
|
20
|
-
export declare const maximumKeep = 16
|
|
21
|
+
export declare const maximumKeep = 16;
|
|
21
22
|
/** Caller holds a write transaction. Keeps active, building/resumable and job-referenced work, plus the `keep` most
|
|
22
23
|
* recent rollback-ready retired generations per profile. Stale retired generations can never activate again (the
|
|
23
24
|
* revision epoch only grows), so they are pruned. Recency is index job creation; VACUUM may renumber rowids. */
|
|
@@ -25,6 +26,14 @@ export declare function planPrune(db: DatabaseSync, keep: number): PruneEntry[];
|
|
|
25
26
|
/** One bounded step for one planned generation, inside the caller's write transaction. The first step marks it pruned
|
|
26
27
|
* and drops its coverage, so it is never rollback-ready while partially deleted. Returns rows deleted; 0 means done. */
|
|
27
28
|
export declare function pruneStep(db: DatabaseSync, id: string, keep: number, limit?: number): number;
|
|
29
|
+
/** Embeddings, and their bytes, that pruning these generations leaves without coverage (deleted items' included). */
|
|
30
|
+
export declare function orphanVectors(db: DatabaseSync, pruning: readonly string[]): {
|
|
31
|
+
vectors: number;
|
|
32
|
+
bytes: number;
|
|
33
|
+
};
|
|
34
|
+
/** One bounded batch of orphan embeddings, inside the caller's write transaction. Returns rows deleted; 0 means done. */
|
|
35
|
+
export declare function gcStep(db: DatabaseSync, limit?: number): number;
|
|
28
36
|
export declare function pageStats(db: DatabaseSync): PageStats;
|
|
29
37
|
/** VACUUM rewrites the database through a temporary copy and the WAL: about the file size again on each volume. */
|
|
30
38
|
export declare function vacuumSpace(root: string, bytes: number, margin?: number): void;
|
|
39
|
+
export {};
|
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
import { statSync, statfsSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
export const maximumKeep = 16
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { activateGeneration, coverageMarker, spaceEncoding } from './index.js';
|
|
4
|
+
import { SpongeError, indexCodes, codeOf } from '../errors.js';
|
|
5
|
+
export const maximumKeep = 16;
|
|
6
|
+
const pruneBatchRows = 1000;
|
|
7
|
+
/** Pruned rows keep their id (index_jobs history references it) with no coverage. */
|
|
8
8
|
/** Exactly the rollback rules: a probe activation inside a savepoint that is always rolled back. */
|
|
9
|
-
function rollbackReady(db, id) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
function rollbackReady(db, id) {
|
|
10
|
+
db.exec('SAVEPOINT prune_probe');
|
|
11
|
+
try {
|
|
12
|
+
activateGeneration(db, id);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (indexCodes.includes(codeOf(error)))
|
|
17
|
+
return false;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
db.exec('ROLLBACK TO prune_probe; RELEASE prune_probe');
|
|
22
|
+
}
|
|
17
23
|
}
|
|
18
|
-
finally {
|
|
19
|
-
db.exec('ROLLBACK TO prune_probe; RELEASE prune_probe');
|
|
20
|
-
} }
|
|
21
24
|
/** Caller holds a write transaction. Keeps active, building/resumable and job-referenced work, plus the `keep` most
|
|
22
25
|
* recent rollback-ready retired generations per profile. Stale retired generations can never activate again (the
|
|
23
26
|
* revision epoch only grows), so they are pruned. Recency is index job creation; VACUUM may renumber rowids. */
|
|
24
27
|
export function planPrune(db, keep) {
|
|
25
28
|
if (!Number.isSafeInteger(keep) || keep < 0 || keep > maximumKeep)
|
|
26
|
-
throw new
|
|
29
|
+
throw new SpongeError('invalid-input');
|
|
27
30
|
if (!db.isTransaction)
|
|
28
|
-
throw new
|
|
29
|
-
const epoch = Number(db.prepare('SELECT revision_epoch FROM library_info WHERE id=1').get()?.revision_epoch), active = new Set(db
|
|
30
|
-
|
|
31
|
+
throw new SpongeError('invalid-input');
|
|
32
|
+
const epoch = Number(db.prepare('SELECT revision_epoch FROM library_info WHERE id=1').get()?.revision_epoch), active = new Set(db
|
|
33
|
+
.prepare('SELECT generation_id FROM active_indexes')
|
|
34
|
+
.all()
|
|
35
|
+
.map((r) => String(r.generation_id)));
|
|
36
|
+
const rows = db
|
|
37
|
+
.prepare('SELECT g.id,g.fingerprint,g.state,g.revision_epoch,g.profile_json,length(g.coverage_json) AS coverage,s.dimensions,s.identity_json,j.state AS job,(SELECT count(*) FROM index_inputs c WHERE c.generation_id=g.id) AS inputs,(SELECT COALESCE(sum(length(c.generation_id)+length(c.source_id)+length(c.revision_id)+length(c.content_hash)),0) FROM index_inputs c WHERE c.generation_id=g.id) AS bytes FROM index_generations g JOIN embedding_spaces s ON s.fingerprint=g.fingerprint LEFT JOIN index_jobs j ON j.generation_id=g.id ORDER BY j.created_at IS NULL,j.created_at DESC,g.rowid DESC')
|
|
38
|
+
.all();
|
|
31
39
|
const kept = new Map();
|
|
32
|
-
return rows.flatMap(row => {
|
|
33
|
-
const id = String(row.id), state = String(row.state), job = row.job === null ? null : String(row.job),
|
|
40
|
+
return rows.flatMap((row) => {
|
|
41
|
+
const id = String(row.id), state = String(row.state), job = row.job === null ? null : String(row.job), inputs = Number(row.inputs);
|
|
34
42
|
let profileId, space;
|
|
35
43
|
try {
|
|
36
44
|
profileId = String(JSON.parse(String(row.profile_json)).profileId);
|
|
@@ -38,11 +46,23 @@ export function planPrune(db, keep) {
|
|
|
38
46
|
space = `${spaceEncoding(identity)}-${Number(row.dimensions)}`;
|
|
39
47
|
}
|
|
40
48
|
catch {
|
|
41
|
-
throw new
|
|
49
|
+
throw new SpongeError('integrity');
|
|
42
50
|
}
|
|
43
|
-
if (state === 'pruned' && !
|
|
51
|
+
if (state === 'pruned' && !inputs && Number(row.coverage) <= coverageMarker.length)
|
|
44
52
|
return [];
|
|
45
|
-
const entry = (action, reason) => [
|
|
53
|
+
const entry = (action, reason) => [
|
|
54
|
+
{
|
|
55
|
+
id,
|
|
56
|
+
profileId,
|
|
57
|
+
fingerprint: String(row.fingerprint),
|
|
58
|
+
space,
|
|
59
|
+
state,
|
|
60
|
+
action,
|
|
61
|
+
reason,
|
|
62
|
+
inputs,
|
|
63
|
+
bytes: Number(row.bytes) + (action === 'prune' ? Math.max(0, Number(row.coverage) - coverageMarker.length) : 0),
|
|
64
|
+
},
|
|
65
|
+
];
|
|
46
66
|
if (active.has(id))
|
|
47
67
|
return entry('keep', 'active');
|
|
48
68
|
if (job === 'running' || job === 'cleanup-pending')
|
|
@@ -53,7 +73,11 @@ export function planPrune(db, keep) {
|
|
|
53
73
|
return entry('prune', 'cancelled');
|
|
54
74
|
// A building generation stays resumable until the revision epoch moves; after that it can never activate.
|
|
55
75
|
if (state === 'building')
|
|
56
|
-
return job === 'queued'
|
|
76
|
+
return job === 'queued'
|
|
77
|
+
? entry('keep', 'queued')
|
|
78
|
+
: Number(row.revision_epoch) === epoch
|
|
79
|
+
? entry('keep', 'building')
|
|
80
|
+
: entry('prune', 'failed');
|
|
57
81
|
if (state !== 'retired')
|
|
58
82
|
return entry('keep', 'unknown');
|
|
59
83
|
if (!rollbackReady(db, id))
|
|
@@ -70,26 +94,52 @@ export function planPrune(db, keep) {
|
|
|
70
94
|
* and drops its coverage, so it is never rollback-ready while partially deleted. Returns rows deleted; 0 means done. */
|
|
71
95
|
export function pruneStep(db, id, keep, limit = pruneBatchRows) {
|
|
72
96
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10000)
|
|
73
|
-
throw new
|
|
97
|
+
throw new SpongeError('invalid-input');
|
|
74
98
|
const row = db.prepare('SELECT state FROM index_generations WHERE id=?').get(id);
|
|
75
99
|
if (!row)
|
|
76
|
-
throw new
|
|
100
|
+
throw new SpongeError('unavailable');
|
|
77
101
|
// Once marked, a generation can never become active, building or retired again: no replanning per batch.
|
|
78
102
|
if (row.state !== 'pruned') {
|
|
79
|
-
const planned = planPrune(db, keep).find(e => e.id === id);
|
|
103
|
+
const planned = planPrune(db, keep).find((e) => e.id === id);
|
|
80
104
|
if (!planned || planned.action !== 'prune')
|
|
81
105
|
return 0;
|
|
82
|
-
db.prepare("UPDATE index_generations SET state='pruned',coverage_json=? WHERE id=?").run(
|
|
106
|
+
db.prepare("UPDATE index_generations SET state='pruned',coverage_json=? WHERE id=?").run(coverageMarker, id);
|
|
107
|
+
db.prepare('DELETE FROM index_items WHERE generation_id=?').run(id);
|
|
83
108
|
db.prepare("UPDATE index_jobs SET state='cancelled',cancel_requested=1 WHERE generation_id=? AND state='queued'").run(id);
|
|
84
109
|
}
|
|
85
110
|
else if (db.prepare('SELECT 1 FROM active_indexes WHERE generation_id=?').get(id))
|
|
86
|
-
throw new
|
|
87
|
-
return Number(db
|
|
111
|
+
throw new SpongeError('integrity');
|
|
112
|
+
return Number(db
|
|
113
|
+
.prepare('DELETE FROM index_inputs WHERE rowid IN (SELECT rowid FROM index_inputs WHERE generation_id=? LIMIT ?)')
|
|
114
|
+
.run(id, limit).changes);
|
|
115
|
+
}
|
|
116
|
+
/** An embedding is an orphan when no generation outside `pruning` covers its content in its space. */
|
|
117
|
+
const orphan = 'NOT EXISTS(SELECT 1 FROM index_inputs c JOIN index_generations g ON g.id=c.generation_id WHERE c.content_hash=e.content_hash AND g.fingerprint=e.fingerprint AND g.id NOT IN (SELECT value FROM json_each(?)))';
|
|
118
|
+
/** Embeddings, and their bytes, that pruning these generations leaves without coverage (deleted items' included). */
|
|
119
|
+
export function orphanVectors(db, pruning) {
|
|
120
|
+
const row = db
|
|
121
|
+
.prepare(`SELECT count(*) n,COALESCE(sum(length(e.fingerprint)+length(e.content_hash)+length(e.input_hash)+length(e.vector)),0) b FROM embeddings e WHERE ${orphan}`)
|
|
122
|
+
.get(JSON.stringify(pruning));
|
|
123
|
+
return { vectors: Number(row?.n), bytes: Number(row?.b) };
|
|
124
|
+
}
|
|
125
|
+
/** One bounded batch of orphan embeddings, inside the caller's write transaction. Returns rows deleted; 0 means done. */
|
|
126
|
+
export function gcStep(db, limit = pruneBatchRows) {
|
|
127
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10000)
|
|
128
|
+
throw new SpongeError('invalid-input');
|
|
129
|
+
return Number(db
|
|
130
|
+
.prepare(`DELETE FROM embeddings WHERE rowid IN (SELECT e.rowid FROM embeddings e WHERE ${orphan} LIMIT ?)`)
|
|
131
|
+
.run('[]', limit).changes);
|
|
132
|
+
}
|
|
133
|
+
export function pageStats(db) {
|
|
134
|
+
const pageSize = Number(db.prepare('PRAGMA page_size').get()?.page_size), pages = Number(db.prepare('PRAGMA page_count').get()?.page_count), freePages = Number(db.prepare('PRAGMA freelist_count').get()?.freelist_count);
|
|
135
|
+
return { pageSize, pages, freePages, bytes: pages * pageSize };
|
|
88
136
|
}
|
|
89
|
-
export function pageStats(db) { const pageSize = Number(db.prepare('PRAGMA page_size').get()?.page_size), pages = Number(db.prepare('PRAGMA page_count').get()?.page_count), freePages = Number(db.prepare('PRAGMA freelist_count').get()?.freelist_count); return { pageSize, pages, freePages, bytes: pages * pageSize }; }
|
|
90
137
|
/** VACUUM rewrites the database through a temporary copy and the WAL: about the file size again on each volume. */
|
|
91
138
|
export function vacuumSpace(root, bytes, margin = 64 * 1024 ** 2) {
|
|
92
|
-
const free = (path) => {
|
|
139
|
+
const free = (path) => {
|
|
140
|
+
const s = statfsSync(path);
|
|
141
|
+
return s.bavail * s.bsize;
|
|
142
|
+
}, temp = tmpdir(), same = statSync(root).dev === statSync(temp).dev;
|
|
93
143
|
if (same ? free(root) < 2 * bytes + margin : free(root) < bytes + margin || free(temp) < bytes + margin)
|
|
94
|
-
throw new
|
|
144
|
+
throw new SpongeError('limit-exceeded');
|
|
95
145
|
}
|
|
@@ -19,5 +19,6 @@ export interface IndexRunOptions {
|
|
|
19
19
|
onCheckpoint?: (attempt: AttemptToken, stage: string) => Promise<void>;
|
|
20
20
|
onBatch?: (attempt: AttemptToken, completed: number) => Promise<void>;
|
|
21
21
|
}
|
|
22
|
-
/** Same durable worker lock/owner/supervisor as acquisition, with sequential bounded batches.
|
|
22
|
+
/** Same durable worker lock/owner/supervisor as acquisition, with sequential bounded batches. Only inputs whose content the space
|
|
23
|
+
* has not embedded run: a delta after captures, or everything new for a new fingerprint. Captures during a run join it. */
|
|
23
24
|
export declare function drainIndexQueue(store: LibraryStore, options: IndexRunOptions): Promise<IndexRunResult[]>;
|