@equationalapplications/core-llm-wiki 7.12.0 → 7.13.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/dist/{chunk-KN4XSLLB.mjs → chunk-PCRYIUV5.mjs} +232 -18
- package/dist/chunk-PCRYIUV5.mjs.map +1 -0
- package/dist/index.d.mts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +730 -146
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +502 -133
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-7d76Ptlq.d.mts → testing-CqsXv2_l.d.mts} +111 -18
- package/dist/{testing-7d76Ptlq.d.ts → testing-CqsXv2_l.d.ts} +111 -18
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +80 -15
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +3 -3
- package/dist/chunk-KN4XSLLB.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BaseRepository, WikiGraphNodeOwnershipConflict, parseJsonObject, parseJsonArray, liveAtSql, __privateAdd, MetadataRepository, EmbeddingService, SearchService, TemporalService, JobManager, PromptService, resolveGrounding, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, createLegacySourceTypeIndex, writeNormalizeStamp, CURRENT_SCHEMA_VERSION, detectSchemaVersion, MIGRATIONS, runSourceRefNormalize, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, WikiSourceRefHashCollision, DiagnosticBuffer, validateManifest, WikiDraftNotFound, generateId, emptyManifest, resolveNodeType, WikiStrictOntologyViolation, validateInlineEdges, resolveEdgeDefinitions, typeSatisfies, normalizeTitleKey, edgeTripleAllowed, MAX_EMBED_ATTEMPTS, UsageMeter, isWikiSearchUnavailableError, WikiBusyError, WikiInvalidReadOptions, isLiveAt, packFactsByBudget, factTokenCost, probePendingWork, WikiMigrationRequiredError, setupDatabase, UnavailableIndexStrategy,
|
|
2
|
-
export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, EMBED_CHARS_CEILING, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiMigrationRequiredError, WikiParseError, WikiSearchUnavailableError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiSupersedeError, WikiTransactionError, chunkText, configureRandomSource, isWikiSearchUnavailableError, parseEmbedding, safeSlice, validateManifest } from './chunk-
|
|
1
|
+
import { BaseRepository, WikiGraphNodeOwnershipConflict, parseJsonObject, parseJsonArray, liveAtSql, reanchorRebuildFingerprint, __privateAdd, MetadataRepository, EmbeddingService, SearchService, TemporalService, JobManager, PromptService, resolveGrounding, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, createLegacySourceTypeIndex, writeNormalizeStamp, CURRENT_SCHEMA_VERSION, detectSchemaVersion, MIGRATIONS, runSourceRefNormalize, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, WikiSourceRefHashCollision, DiagnosticBuffer, validateManifest, WikiDraftNotFound, generateId, emptyManifest, resolveNodeType, WikiStrictOntologyViolation, validateInlineEdges, resolveEdgeDefinitions, typeSatisfies, normalizeTitleKey, edgeTripleAllowed, MAX_EMBED_ATTEMPTS, UsageMeter, isWikiSearchUnavailableError, WikiBusyError, WikiInvalidReadOptions, isLiveAt, packFactsByBudget, factTokenCost, probePendingWork, WikiMigrationRequiredError, setupDatabase, UnavailableIndexStrategy, FTS5_STATE_KEY, fts5LedgerDropSql, fts5TablesDdl, fts5PendingIndexesDdl, fts5TriggersDdl, writeRebuildStart, FTS5_RETIRED_COUNTER_KEY, retiredTableName, FTS5_RETIRED_TABLES_KEY, isRetiredTableName, idReindexChunkSql, readRebuildProgress, FTS5_DRAIN_CHUNK, FTS5_RECLAIM_MERGE_PAGES, windowHiSql, windowChunkSql, FTS5_REBUILD_WM_KEY, sweepHiSql, sweepChunkSql, FTS5_SWEEP_CURSOR_KEY, drainHiSql, drainChunkSql, FTS5_REBUILD_SNAPSHOT_KEY, idDrainChunkSql, entityDrainHiSql, entityDrainChunkSql, fts5TriggerNames, extractSqliteCode, WikiTransactionError } from './chunk-PCRYIUV5.mjs';
|
|
2
|
+
export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, EMBED_CHARS_CEILING, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiMigrationRequiredError, WikiParseError, WikiSearchUnavailableError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiSupersedeError, WikiTransactionError, chunkText, configureRandomSource, isWikiSearchUnavailableError, parseEmbedding, safeSlice, validateManifest } from './chunk-PCRYIUV5.mjs';
|
|
3
3
|
import { deriveTrustTier, isStaleAfter, appendRelatedSection, buildConceptDocument, buildLogMd, buildEntityIndexMd, buildRootIndexMd, isAllowedOkfPath, parseRootIndexMd, parseEntityIndexMd, parseConcept, splitRelatedSection, extractMarkdownLinks, parseLogMd, parseEventIdComment, latestVerified, formatSourcesJson, appendEventIdComment, parseVerifiedFlexible, parseCitationsList } from '@equationalapplications/core-okf';
|
|
4
4
|
import MiniSearch from 'minisearch';
|
|
5
5
|
|
|
@@ -695,6 +695,7 @@ var _EntryRepository = class _EntryRepository extends BaseRepository {
|
|
|
695
695
|
}
|
|
696
696
|
}
|
|
697
697
|
}
|
|
698
|
+
if (totalChanges > 0) await reanchorRebuildFingerprint(executor, this.prefix, ids);
|
|
698
699
|
return totalChanges;
|
|
699
700
|
}
|
|
700
701
|
/**
|
|
@@ -2512,87 +2513,11 @@ function assertOwnedBy(entityId, documents) {
|
|
|
2512
2513
|
}
|
|
2513
2514
|
}
|
|
2514
2515
|
|
|
2515
|
-
// src/
|
|
2516
|
-
var
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
}
|
|
2521
|
-
function tagsExpr(col) {
|
|
2522
|
-
return `CASE WHEN json_valid(${col}) THEN
|
|
2523
|
-
CASE WHEN json_type(${col}) = 'array' THEN
|
|
2524
|
-
(SELECT group_concat(CASE type WHEN 'true' THEN 'true' WHEN 'false' THEN 'false'
|
|
2525
|
-
WHEN 'null' THEN '' ELSE value END, ' ')
|
|
2526
|
-
FROM json_each(${col}))
|
|
2527
|
-
ELSE ${col} END
|
|
2528
|
-
ELSE ${col} END`;
|
|
2529
|
-
}
|
|
2530
|
-
function fts5TablesDdl(p) {
|
|
2531
|
-
return `
|
|
2532
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS ${p}entries_fts USING fts5(
|
|
2533
|
-
id UNINDEXED, entity_id UNINDEXED, title, body, tags,
|
|
2534
|
-
tokenize = 'porter unicode61'
|
|
2535
|
-
);
|
|
2536
|
-
CREATE TABLE IF NOT EXISTS ${p}fts_map (
|
|
2537
|
-
fts_rowid INTEGER PRIMARY KEY,
|
|
2538
|
-
id TEXT NOT NULL UNIQUE,
|
|
2539
|
-
entity_id TEXT NOT NULL
|
|
2540
|
-
);
|
|
2541
|
-
CREATE INDEX IF NOT EXISTS ${p}fts_map_entity ON ${p}fts_map(entity_id);
|
|
2542
|
-
CREATE TABLE IF NOT EXISTS ${p}fts_pending (
|
|
2543
|
-
seq INTEGER PRIMARY KEY,
|
|
2544
|
-
entity_id TEXT NOT NULL,
|
|
2545
|
-
id TEXT NOT NULL
|
|
2546
|
-
);`;
|
|
2547
|
-
}
|
|
2548
|
-
function fts5TriggersDdl(p) {
|
|
2549
|
-
const [ai, au, ad] = fts5TriggerNames(p);
|
|
2550
|
-
return `
|
|
2551
|
-
CREATE TRIGGER IF NOT EXISTS ${ai} AFTER INSERT ON ${p}entries BEGIN
|
|
2552
|
-
INSERT INTO ${p}fts_pending (entity_id, id) VALUES (NEW.entity_id, NEW.id);
|
|
2553
|
-
END;
|
|
2554
|
-
CREATE TRIGGER IF NOT EXISTS ${au}
|
|
2555
|
-
AFTER UPDATE OF id, entity_id, title, body, tags, deleted_at ON ${p}entries BEGIN
|
|
2556
|
-
INSERT INTO ${p}fts_pending (entity_id, id) VALUES (OLD.entity_id, OLD.id);
|
|
2557
|
-
INSERT INTO ${p}fts_pending (entity_id, id)
|
|
2558
|
-
SELECT NEW.entity_id, NEW.id WHERE NEW.id <> OLD.id OR NEW.entity_id <> OLD.entity_id;
|
|
2559
|
-
END;
|
|
2560
|
-
CREATE TRIGGER IF NOT EXISTS ${ad} AFTER DELETE ON ${p}entries BEGIN
|
|
2561
|
-
INSERT INTO ${p}fts_pending (entity_id, id) VALUES (OLD.entity_id, OLD.id);
|
|
2562
|
-
END;`;
|
|
2563
|
-
}
|
|
2564
|
-
function drainHiSql(p) {
|
|
2565
|
-
return `SELECT max(seq) AS hi FROM (SELECT seq FROM ${p}fts_pending ORDER BY seq LIMIT ${FTS5_DRAIN_CHUNK})`;
|
|
2566
|
-
}
|
|
2567
|
-
function drainChunkSql(p) {
|
|
2568
|
-
const chunkIds = `SELECT id FROM ${p}fts_pending WHERE seq <= ?`;
|
|
2569
|
-
return [
|
|
2570
|
-
`DELETE FROM ${p}entries_fts WHERE rowid IN (SELECT m.fts_rowid FROM ${p}fts_map m WHERE m.id IN (${chunkIds}))`,
|
|
2571
|
-
`DELETE FROM ${p}fts_map WHERE id IN (${chunkIds})`,
|
|
2572
|
-
`INSERT INTO ${p}fts_map (id, entity_id)
|
|
2573
|
-
SELECT e.id, e.entity_id FROM ${p}entries e WHERE e.id IN (${chunkIds}) AND e.deleted_at IS NULL`,
|
|
2574
|
-
`INSERT INTO ${p}entries_fts (rowid, id, entity_id, title, body, tags)
|
|
2575
|
-
SELECT m.fts_rowid, e.id, e.entity_id, e.title, e.body, ${tagsExpr("e.tags")}
|
|
2576
|
-
FROM ${p}fts_map m JOIN ${p}entries e ON e.id = m.id
|
|
2577
|
-
WHERE m.id IN (${chunkIds})`,
|
|
2578
|
-
`DELETE FROM ${p}fts_pending WHERE seq <= ?`
|
|
2579
|
-
];
|
|
2580
|
-
}
|
|
2581
|
-
function rebuildSql(p) {
|
|
2582
|
-
return [
|
|
2583
|
-
`INSERT INTO ${p}fts_map (id, entity_id) SELECT id, entity_id FROM ${p}entries WHERE deleted_at IS NULL`,
|
|
2584
|
-
`INSERT INTO ${p}entries_fts (rowid, id, entity_id, title, body, tags)
|
|
2585
|
-
SELECT m.fts_rowid, e.id, e.entity_id, e.title, e.body, ${tagsExpr("e.tags")}
|
|
2586
|
-
FROM ${p}fts_map m JOIN ${p}entries e ON e.id = m.id`
|
|
2587
|
-
];
|
|
2588
|
-
}
|
|
2589
|
-
function fts5LedgerDropSql(p) {
|
|
2590
|
-
return [
|
|
2591
|
-
...fts5TriggerNames(p).map((n) => `DROP TRIGGER IF EXISTS ${n};`),
|
|
2592
|
-
`DROP TABLE IF EXISTS ${p}fts_pending;`,
|
|
2593
|
-
`DROP TABLE IF EXISTS ${p}fts_map;`
|
|
2594
|
-
].join("\n");
|
|
2595
|
-
}
|
|
2516
|
+
// src/utils/monotonicNow.ts
|
|
2517
|
+
var monotonicNow = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
2518
|
+
|
|
2519
|
+
// src/utils/setupDefaults.ts
|
|
2520
|
+
var DEFAULT_SETUP_BUDGET_MS = 1500;
|
|
2596
2521
|
|
|
2597
2522
|
// src/services/search/ftsQuery.ts
|
|
2598
2523
|
var TOKEN = /[\p{L}\p{N}]+/gu;
|
|
@@ -2605,41 +2530,363 @@ function buildFtsMatchQuery(query) {
|
|
|
2605
2530
|
|
|
2606
2531
|
// src/services/search/Fts5IndexStrategy.ts
|
|
2607
2532
|
var Fts5IndexStrategy = class {
|
|
2608
|
-
constructor(db, prefix, metadataRepo) {
|
|
2533
|
+
constructor(db, prefix, metadataRepo, budgetMs = DEFAULT_SETUP_BUDGET_MS) {
|
|
2609
2534
|
this.db = db;
|
|
2610
2535
|
this.prefix = prefix;
|
|
2611
2536
|
this.metadataRepo = metadataRepo;
|
|
2537
|
+
this.budgetMs = budgetMs;
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* Resolves the index state (spec #280 Change 3 transition table). Every
|
|
2541
|
+
* read-only probe runs before the one transition transaction, so a probe
|
|
2542
|
+
* failure leaves zero mutation (Change 4 ordering).
|
|
2543
|
+
*/
|
|
2544
|
+
async init(opts = {}) {
|
|
2545
|
+
try {
|
|
2546
|
+
await this.initOnce(opts);
|
|
2547
|
+
} catch (err) {
|
|
2548
|
+
const wrapped = unwrapFtsOnly(err);
|
|
2549
|
+
const cls = ftsCorruptionClass(wrapped.cause);
|
|
2550
|
+
if (cls === "vtab" || cls === "corrupt" && wrapped.ftsOnly) {
|
|
2551
|
+
await this.resetAndRearm();
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
throw wrapped.cause;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
/** Marks errors from statements that touch only FTS tables (entries_fts + shadows). */
|
|
2558
|
+
async ftsOnly(fn) {
|
|
2559
|
+
try {
|
|
2560
|
+
return await fn();
|
|
2561
|
+
} catch (err) {
|
|
2562
|
+
throw new FtsOnlyStatementError(err);
|
|
2563
|
+
}
|
|
2612
2564
|
}
|
|
2613
|
-
async
|
|
2565
|
+
async initOnce(opts) {
|
|
2566
|
+
const p = this.prefix;
|
|
2614
2567
|
const state = await this.metadataRepo.getMeta(FTS5_STATE_KEY);
|
|
2615
|
-
|
|
2616
|
-
|
|
2568
|
+
const schema = await this.readSchema();
|
|
2569
|
+
const ftsExists = schema.tables.has(`${p}entries_fts`);
|
|
2570
|
+
const mapExists = schema.tables.has(`${p}fts_map`);
|
|
2571
|
+
const pendingExists = schema.tables.has(`${p}fts_pending`);
|
|
2572
|
+
const trig = schema.triggersPresent;
|
|
2573
|
+
const ftsDdl = schema.tables.get(`${p}entries_fts`) ?? null;
|
|
2574
|
+
const tokenizerOk = ftsDdl === null || /tokenize\s*=\s*['"]porter\s+unicode61['"]/i.test(ftsDdl);
|
|
2575
|
+
if (state === null && opts.entriesExisted === false) {
|
|
2576
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
2577
|
+
await tx.execAsync(fts5TablesDdl(p) + fts5PendingIndexesDdl(p) + fts5TriggersDdl(p));
|
|
2578
|
+
await this.metadataRepo.setMeta(FTS5_STATE_KEY, "live", tx);
|
|
2579
|
+
});
|
|
2617
2580
|
return;
|
|
2618
2581
|
}
|
|
2582
|
+
if (state === "detached") return this.resetAndRearm();
|
|
2583
|
+
if (!tokenizerOk) return this.resetAndRearm();
|
|
2584
|
+
const intact = trig && ftsExists && mapExists && pendingExists;
|
|
2585
|
+
if (intact && state === "rebuilding") return;
|
|
2586
|
+
if (intact && state === "live") {
|
|
2587
|
+
await this.ftsOnly(() => this.db.getFirstAsync(`SELECT rowid FROM ${p}entries_fts ORDER BY rowid DESC LIMIT 1`));
|
|
2588
|
+
if (await this.liveRowUnindexed()) return this.resetAndRearm();
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
if (ftsExists && mapExists && await this.keepMapConsistent()) {
|
|
2592
|
+
await this.db.withTransactionAsync((tx) => this.rearmInTx(tx));
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
return this.resetAndRearm();
|
|
2596
|
+
}
|
|
2597
|
+
/**
|
|
2598
|
+
* The index was lost (rev 8 m3 / rev 9 m4): the oldest or the newest live entry is neither mapped nor
|
|
2599
|
+
* ledgered, which `live` never allows. The oldest one keeps detection armed after post-loss writes
|
|
2600
|
+
* repopulate the map and the ledger (#284 review). Every-open path: each side reads at most
|
|
2601
|
+
* FTS5_DRAIN_CHUNK entries rows (final review M1), so an all-soft-deleted store is never fully scanned;
|
|
2602
|
+
* a lost index whose oldest and newest rows are all deleted goes undetected.
|
|
2603
|
+
*/
|
|
2604
|
+
async liveRowUnindexed() {
|
|
2605
|
+
const p = this.prefix;
|
|
2606
|
+
const edge = (dir) => `SELECT id FROM (SELECT id FROM (SELECT id, deleted_at FROM ${p}entries ORDER BY rowid ${dir} LIMIT ${FTS5_DRAIN_CHUNK}) WHERE deleted_at IS NULL LIMIT 1)`;
|
|
2607
|
+
return await this.db.getFirstAsync(
|
|
2608
|
+
`SELECT 1 AS x FROM (${edge("ASC")} UNION ALL ${edge("DESC")}) e
|
|
2609
|
+
WHERE NOT EXISTS (SELECT 1 FROM ${p}fts_map m WHERE m.id = e.id)
|
|
2610
|
+
AND NOT EXISTS (SELECT 1 FROM ${p}fts_pending q WHERE q.id = e.id)
|
|
2611
|
+
LIMIT 1`
|
|
2612
|
+
) !== null;
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Read-only keep-map check (spec DDL note): every surviving entries_fts
|
|
2616
|
+
* rowid must have a map row. Max-rowid seek, then docsize vs map counts.
|
|
2617
|
+
*/
|
|
2618
|
+
async keepMapConsistent() {
|
|
2619
|
+
const p = this.prefix;
|
|
2620
|
+
const fts = await this.ftsOnly(() => this.db.getFirstAsync(`SELECT rowid FROM ${p}entries_fts ORDER BY rowid DESC LIMIT 1`));
|
|
2621
|
+
const map = await this.db.getFirstAsync(
|
|
2622
|
+
`SELECT coalesce(max(fts_rowid), 0) AS hi, count(*) AS n FROM ${p}fts_map`
|
|
2623
|
+
);
|
|
2624
|
+
if ((fts?.rowid ?? 0) > (map?.hi ?? 0)) return false;
|
|
2625
|
+
const doc = await this.ftsOnly(() => this.db.getFirstAsync(`SELECT count(*) AS n FROM ${p}entries_fts_docsize`));
|
|
2626
|
+
return (doc?.n ?? 0) === (map?.n ?? 0);
|
|
2627
|
+
}
|
|
2628
|
+
/** Keep-map re-arm: triggers, empty ledger, rebuild from 0, state rebuilding. One transaction. */
|
|
2629
|
+
async rearmInTx(tx) {
|
|
2630
|
+
const p = this.prefix;
|
|
2631
|
+
await tx.execAsync(fts5TablesDdl(p));
|
|
2632
|
+
await tx.execAsync(`DELETE FROM ${p}fts_pending;`);
|
|
2633
|
+
await tx.execAsync(fts5PendingIndexesDdl(p) + fts5TriggersDdl(p));
|
|
2634
|
+
await writeRebuildStart(tx, p, this.metadataRepo);
|
|
2635
|
+
await this.metadataRepo.setMeta(FTS5_STATE_KEY, "rebuilding", tx);
|
|
2636
|
+
}
|
|
2637
|
+
/** Reset path: retire entries_fts, recreate it empty, clear map + ledger, then the re-arm. One transaction. */
|
|
2638
|
+
async resetAndRearm() {
|
|
2639
|
+
const p = this.prefix;
|
|
2619
2640
|
await this.db.withTransactionAsync(async (tx) => {
|
|
2620
|
-
await
|
|
2621
|
-
await tx.execAsync(fts5TablesDdl(
|
|
2622
|
-
await
|
|
2623
|
-
await
|
|
2641
|
+
await this.retireFtsTable(tx);
|
|
2642
|
+
await tx.execAsync(fts5TablesDdl(p));
|
|
2643
|
+
await tx.execAsync(`DELETE FROM ${p}fts_map; DELETE FROM ${p}fts_pending;`);
|
|
2644
|
+
await this.rearmInTx(tx);
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
/** Rename-retire entries_fts (spec #280 rev 11); DROP only when the RENAME throws. */
|
|
2648
|
+
async retireFtsTable(tx) {
|
|
2649
|
+
const p = this.prefix;
|
|
2650
|
+
if (!await this.metadataRepo.tableExists(`${p}entries_fts`, tx)) return;
|
|
2651
|
+
const n = Number(await this.metadataRepo.getMeta(FTS5_RETIRED_COUNTER_KEY, tx) ?? "0") + 1;
|
|
2652
|
+
const name = retiredTableName(p, n);
|
|
2653
|
+
try {
|
|
2654
|
+
await tx.execAsync(`ALTER TABLE ${p}entries_fts RENAME TO ${name}`);
|
|
2655
|
+
} catch (err) {
|
|
2656
|
+
if (isTransientOrTxFatal(err)) throw err;
|
|
2657
|
+
await tx.execAsync(`DROP TABLE ${p}entries_fts`);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
await this.metadataRepo.setMeta(FTS5_RETIRED_TABLES_KEY, JSON.stringify([...await this.retiredList(tx), name]), tx);
|
|
2661
|
+
await this.metadataRepo.setMeta(FTS5_RETIRED_COUNTER_KEY, String(n), tx);
|
|
2662
|
+
}
|
|
2663
|
+
async retiredList(tx = this.db) {
|
|
2664
|
+
const raw = parseJsonArray(await this.metadataRepo.getMeta(FTS5_RETIRED_TABLES_KEY, tx), []);
|
|
2665
|
+
return raw.filter((n) => isRetiredTableName(this.prefix, n));
|
|
2666
|
+
}
|
|
2667
|
+
async drain(opts = {}) {
|
|
2668
|
+
try {
|
|
2669
|
+
await this.drainOnce(opts);
|
|
2670
|
+
} catch (err) {
|
|
2671
|
+
if (ftsCorruptionClass(err) !== "vtab") throw err;
|
|
2672
|
+
await this.resetAndRearm();
|
|
2673
|
+
if (opts.ids && opts.ids.length > 0) await this.drainIds(opts.ids, idReindexChunkSql(this.prefix));
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
async drainOnce(opts) {
|
|
2677
|
+
const ids = opts.ids ?? [];
|
|
2678
|
+
if (ids.length > 0) await this.drainIds(ids);
|
|
2679
|
+
if (opts.mode === "ledger") {
|
|
2680
|
+
if (ids.length === 0 && opts.entityId !== void 0) await this.drainEntityChunk(opts.entityId);
|
|
2681
|
+
else await this.drainLedgerChunk();
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
await this.fullQuantum(opts.deadline ?? monotonicNow() + this.budgetMs);
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* One full quantum (spec #280 Change 3): ledger first (at most half the
|
|
2688
|
+
* budget while `rebuilding`); then, while `rebuilding`, ≥1 window unless
|
|
2689
|
+
* the ledger drain overran the deadline, one sweep tx, and the flip check;
|
|
2690
|
+
* then reclamation. A `live` store reads only the state.
|
|
2691
|
+
*/
|
|
2692
|
+
async fullQuantum(deadline) {
|
|
2693
|
+
const rebuilding = await this.metadataRepo.getMeta(FTS5_STATE_KEY) === "rebuilding";
|
|
2694
|
+
const start = monotonicNow();
|
|
2695
|
+
const ledgerRan = await this.drainLedgerUntil(rebuilding ? start + (deadline - start) / 2 : deadline);
|
|
2696
|
+
const ledgerOverran = ledgerRan && monotonicNow() >= deadline;
|
|
2697
|
+
if (rebuilding) {
|
|
2698
|
+
const progress = await readRebuildProgress(this.metadataRepo);
|
|
2699
|
+
if (!progress || !await this.fingerprintValid(progress.wm)) {
|
|
2700
|
+
await this.restartRebuild();
|
|
2701
|
+
} else {
|
|
2702
|
+
if (!ledgerOverran) {
|
|
2703
|
+
let step;
|
|
2704
|
+
do {
|
|
2705
|
+
step = await this.runWindow(progress);
|
|
2706
|
+
} while (step === "more" && monotonicNow() < deadline);
|
|
2707
|
+
if (step !== "stale") await this.runSweepTx(progress);
|
|
2708
|
+
}
|
|
2709
|
+
await this.tryFlip();
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
try {
|
|
2713
|
+
await this.reclaimRetired(deadline);
|
|
2714
|
+
} catch (err) {
|
|
2715
|
+
if (!isSqliteError(err)) throw err;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
/** Bounded reclamation steps, each its own tx, until the deadline or nothing is retired. */
|
|
2719
|
+
async reclaimRetired(deadline) {
|
|
2720
|
+
while (monotonicNow() < deadline) {
|
|
2721
|
+
const [name] = await this.retiredList();
|
|
2722
|
+
if (name === void 0) return;
|
|
2723
|
+
try {
|
|
2724
|
+
if (!await this.metadataRepo.tableExists(name)) {
|
|
2725
|
+
await this.delistRetired(name, false);
|
|
2726
|
+
continue;
|
|
2727
|
+
}
|
|
2728
|
+
while (!await this.reclaimStep(name) && monotonicNow() < deadline) ;
|
|
2729
|
+
} catch (err) {
|
|
2730
|
+
if (ftsCorruptionClass(err) === null) throw err;
|
|
2731
|
+
try {
|
|
2732
|
+
await this.delistRetired(name, true);
|
|
2733
|
+
} catch (dropErr) {
|
|
2734
|
+
if (!isSqliteError(dropErr)) throw dropErr;
|
|
2735
|
+
await this.delistRetired(name, false);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
/** (1) delete <=500 rows, else (2) one bounded merge, else (3) DROP + delist. True once delisted. */
|
|
2741
|
+
async reclaimStep(name) {
|
|
2742
|
+
if (await this.db.getFirstAsync(`SELECT rowid FROM ${name} LIMIT 1`)) {
|
|
2743
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
2744
|
+
await tx.runAsync(`DELETE FROM ${name} WHERE rowid IN (SELECT rowid FROM ${name} ORDER BY rowid LIMIT ${FTS5_DRAIN_CHUNK})`);
|
|
2745
|
+
});
|
|
2746
|
+
return false;
|
|
2747
|
+
}
|
|
2748
|
+
let merged = false;
|
|
2749
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
2750
|
+
const before = await tx.getFirstAsync(`SELECT total_changes() AS n`);
|
|
2751
|
+
await tx.runAsync(`INSERT INTO ${name}(${name}, rank) VALUES('merge', ${FTS5_RECLAIM_MERGE_PAGES})`);
|
|
2752
|
+
const after = await tx.getFirstAsync(`SELECT total_changes() AS n`);
|
|
2753
|
+
merged = (after?.n ?? 0) - (before?.n ?? 0) >= 2;
|
|
2754
|
+
});
|
|
2755
|
+
if (!merged) await this.delistRetired(name, true);
|
|
2756
|
+
return !merged;
|
|
2757
|
+
}
|
|
2758
|
+
async delistRetired(name, drop) {
|
|
2759
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
2760
|
+
if (drop) await tx.execAsync(`DROP TABLE IF EXISTS ${name}`);
|
|
2761
|
+
const rest = (await this.retiredList(tx)).filter((n) => n !== name);
|
|
2762
|
+
await this.metadataRepo.setMeta(FTS5_RETIRED_TABLES_KEY, JSON.stringify(rest), tx);
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
/** The id at the watermark's rowid still matches. A null id (start, or windows complete) has nothing to check. */
|
|
2766
|
+
async fingerprintValid(wm, tx = this.db) {
|
|
2767
|
+
if (wm.id === null) return true;
|
|
2768
|
+
const row = await tx.getFirstAsync(`SELECT id FROM ${this.prefix}entries WHERE rowid = ?`, [wm.rowid]);
|
|
2769
|
+
return row?.id === wm.id;
|
|
2770
|
+
}
|
|
2771
|
+
/** External change under the watermark: start over (same re-arm transaction as a trigger-loss restart). */
|
|
2772
|
+
async restartRebuild() {
|
|
2773
|
+
await this.db.withTransactionAsync((tx) => this.rearmInTx(tx));
|
|
2774
|
+
}
|
|
2775
|
+
/**
|
|
2776
|
+
* Progress as stored, re-read inside a window/sweep tx: false when it no
|
|
2777
|
+
* longer matches what this quantum assumed (final review I1). Writing a
|
|
2778
|
+
* watermark or cursor computed from a stale `pr` would overwrite a
|
|
2779
|
+
* concurrent restart (runPrune VACUUM is not on the sync chain).
|
|
2780
|
+
*/
|
|
2781
|
+
async progressUnchanged(pr, tx) {
|
|
2782
|
+
if (await this.metadataRepo.getMeta(FTS5_STATE_KEY, tx) !== "rebuilding") return false;
|
|
2783
|
+
const cur = await readRebuildProgress(this.metadataRepo, tx);
|
|
2784
|
+
return cur !== null && cur.wm.rowid === pr.wm.rowid && cur.wm.id === pr.wm.id && cur.cursor === pr.cursor && cur.snapshot.entries === pr.snapshot.entries && cur.snapshot.map === pr.snapshot.map;
|
|
2785
|
+
}
|
|
2786
|
+
/** One window tx. 'done' once no rows remain in (wm, snapshot] (the watermark is then pinned at the snapshot, F9); 'stale' when progress moved under the quantum. */
|
|
2787
|
+
async runWindow(pr) {
|
|
2788
|
+
if (pr.wm.rowid >= pr.snapshot.entries) return "done";
|
|
2789
|
+
const p = this.prefix;
|
|
2790
|
+
let next = pr.wm;
|
|
2791
|
+
let more = false;
|
|
2792
|
+
let stale = false;
|
|
2793
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
2794
|
+
if (!await this.progressUnchanged(pr, tx)) {
|
|
2795
|
+
stale = true;
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
const row = await tx.getFirstAsync(windowHiSql(p), [pr.wm.rowid, pr.snapshot.entries]);
|
|
2799
|
+
if (row?.hi == null) {
|
|
2800
|
+
next = { rowid: pr.snapshot.entries, id: null };
|
|
2801
|
+
} else {
|
|
2802
|
+
for (const sql of windowChunkSql(p)) await tx.runAsync(sql, [pr.wm.rowid, row.hi]);
|
|
2803
|
+
const fp = await tx.getFirstAsync(`SELECT id FROM ${p}entries WHERE rowid = ?`, [row.hi]);
|
|
2804
|
+
next = { rowid: row.hi, id: fp.id };
|
|
2805
|
+
more = true;
|
|
2806
|
+
}
|
|
2807
|
+
await this.metadataRepo.setMeta(FTS5_REBUILD_WM_KEY, JSON.stringify(next), tx);
|
|
2808
|
+
});
|
|
2809
|
+
if (stale) return "stale";
|
|
2810
|
+
pr.wm = next;
|
|
2811
|
+
return more ? "more" : "done";
|
|
2812
|
+
}
|
|
2813
|
+
/** At most one bounded sweep tx; the cursor advances in the same tx. */
|
|
2814
|
+
async runSweepTx(pr) {
|
|
2815
|
+
if (pr.cursor >= pr.snapshot.map) return;
|
|
2816
|
+
const p = this.prefix;
|
|
2817
|
+
let next = pr.cursor;
|
|
2818
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
2819
|
+
if (!await this.progressUnchanged(pr, tx)) return;
|
|
2820
|
+
const row = await tx.getFirstAsync(sweepHiSql(p), [pr.cursor, pr.snapshot.map]);
|
|
2821
|
+
const hi = row?.hi ?? pr.snapshot.map;
|
|
2822
|
+
for (const sql of sweepChunkSql(p)) await tx.runAsync(sql, [pr.cursor, hi]);
|
|
2823
|
+
await this.metadataRepo.setMeta(FTS5_SWEEP_CURSOR_KEY, String(hi), tx);
|
|
2824
|
+
next = hi;
|
|
2825
|
+
});
|
|
2826
|
+
pr.cursor = next;
|
|
2827
|
+
}
|
|
2828
|
+
/** Completion, re-checked inside the flipping tx; the residual ledger (≤ one chunk) drains in it. */
|
|
2829
|
+
async tryFlip() {
|
|
2830
|
+
const p = this.prefix;
|
|
2831
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
2832
|
+
if (await this.metadataRepo.getMeta(FTS5_STATE_KEY, tx) !== "rebuilding") return;
|
|
2833
|
+
const pr = await readRebuildProgress(this.metadataRepo, tx);
|
|
2834
|
+
if (!pr || pr.wm.rowid < pr.snapshot.entries || pr.cursor < pr.snapshot.map) return;
|
|
2835
|
+
if (!await this.fingerprintValid(pr.wm, tx)) return;
|
|
2836
|
+
const residual = await tx.getFirstAsync(
|
|
2837
|
+
`SELECT count(*) AS n FROM (SELECT 1 FROM ${p}fts_pending LIMIT ${FTS5_DRAIN_CHUNK + 1})`
|
|
2838
|
+
);
|
|
2839
|
+
if ((residual?.n ?? 0) > FTS5_DRAIN_CHUNK) return;
|
|
2840
|
+
const hi = await tx.getFirstAsync(drainHiSql(p));
|
|
2841
|
+
if (hi?.hi != null) for (const sql of drainChunkSql(p)) await tx.runAsync(sql, [hi.hi]);
|
|
2842
|
+
await this.metadataRepo.deleteMeta(FTS5_REBUILD_WM_KEY, tx);
|
|
2843
|
+
await this.metadataRepo.deleteMeta(FTS5_REBUILD_SNAPSHOT_KEY, tx);
|
|
2844
|
+
await this.metadataRepo.deleteMeta(FTS5_SWEEP_CURSOR_KEY, tx);
|
|
2624
2845
|
await this.metadataRepo.setMeta(FTS5_STATE_KEY, "live", tx);
|
|
2625
2846
|
});
|
|
2626
2847
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2848
|
+
/** Seq-ordered ledger chunks until empty or `stopAt`. True when a chunk ran. */
|
|
2849
|
+
async drainLedgerUntil(stopAt) {
|
|
2850
|
+
let ran = false;
|
|
2851
|
+
while (await this.drainLedgerChunk()) {
|
|
2852
|
+
ran = true;
|
|
2853
|
+
if (monotonicNow() >= stopAt) break;
|
|
2854
|
+
}
|
|
2855
|
+
return ran;
|
|
2629
2856
|
}
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
const
|
|
2633
|
-
for (; ; ) {
|
|
2634
|
-
const
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
for (const sql of chunk) await tx.runAsync(sql, [row.hi]);
|
|
2638
|
-
return false;
|
|
2857
|
+
/** The caller's own ids, to completion, ≤ FTS5_DRAIN_CHUNK per transaction. */
|
|
2858
|
+
async drainIds(ids, chunk = idDrainChunkSql(this.prefix)) {
|
|
2859
|
+
const unique = [...new Set(ids)];
|
|
2860
|
+
for (let i = 0; i < unique.length; i += FTS5_DRAIN_CHUNK) {
|
|
2861
|
+
const json = JSON.stringify(unique.slice(i, i + FTS5_DRAIN_CHUNK));
|
|
2862
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
2863
|
+
for (const sql of chunk) await tx.runAsync(sql, [json]);
|
|
2639
2864
|
});
|
|
2640
|
-
if (done) return;
|
|
2641
2865
|
}
|
|
2642
2866
|
}
|
|
2867
|
+
/** One seq-ordered chunk. False when the ledger was empty. */
|
|
2868
|
+
async drainLedgerChunk() {
|
|
2869
|
+
const hiSql = drainHiSql(this.prefix);
|
|
2870
|
+
const chunk = drainChunkSql(this.prefix);
|
|
2871
|
+
let ran = false;
|
|
2872
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
2873
|
+
const row = await tx.getFirstAsync(hiSql);
|
|
2874
|
+
if (row?.hi == null) return;
|
|
2875
|
+
for (const sql of chunk) await tx.runAsync(sql, [row.hi]);
|
|
2876
|
+
ran = true;
|
|
2877
|
+
});
|
|
2878
|
+
return ran;
|
|
2879
|
+
}
|
|
2880
|
+
/** One entity-targeted chunk (ORDER BY seq, ≤ FTS5_DRAIN_CHUNK). */
|
|
2881
|
+
async drainEntityChunk(entityId) {
|
|
2882
|
+
const hiSql = entityDrainHiSql(this.prefix);
|
|
2883
|
+
const chunk = entityDrainChunkSql(this.prefix);
|
|
2884
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
2885
|
+
const row = await tx.getFirstAsync(hiSql, [entityId]);
|
|
2886
|
+
if (row?.hi == null) return;
|
|
2887
|
+
for (const sql of chunk) await tx.runAsync(sql, [entityId, row.hi]);
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2643
2890
|
async replace(entityId, ids, documents) {
|
|
2644
2891
|
assertEntity(entityId, documents);
|
|
2645
2892
|
const docIds = documents.map((d) => d.id);
|
|
@@ -2717,20 +2964,57 @@ var Fts5IndexStrategy = class {
|
|
|
2717
2964
|
);
|
|
2718
2965
|
}
|
|
2719
2966
|
}
|
|
2720
|
-
|
|
2967
|
+
/** The init probes' schema facts in one sqlite_master read (#284 review): FTS tables with their DDL, and the triggers. */
|
|
2968
|
+
async readSchema() {
|
|
2721
2969
|
const p = this.prefix;
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
}
|
|
2725
|
-
async triggersPresent() {
|
|
2726
|
-
const names = fts5TriggerNames(this.prefix);
|
|
2970
|
+
const triggers = fts5TriggerNames(p);
|
|
2971
|
+
const names = [`${p}entries_fts`, `${p}fts_map`, `${p}fts_pending`, ...triggers];
|
|
2727
2972
|
const rows = await this.db.getAllAsync(
|
|
2728
|
-
`SELECT name FROM sqlite_master WHERE
|
|
2729
|
-
|
|
2973
|
+
`SELECT type, name, sql FROM sqlite_master WHERE name IN (${names.map(() => "?").join(", ")})`,
|
|
2974
|
+
names
|
|
2730
2975
|
);
|
|
2731
|
-
|
|
2976
|
+
const tables = new Map(rows.filter((r) => r.type === "table").map((r) => [r.name, r.sql]));
|
|
2977
|
+
const present = new Set(rows.filter((r) => r.type === "trigger").map((r) => r.name));
|
|
2978
|
+
return { tables, triggersPresent: triggers.every((n) => present.has(n)) };
|
|
2979
|
+
}
|
|
2980
|
+
};
|
|
2981
|
+
var FtsOnlyStatementError = class extends Error {
|
|
2982
|
+
constructor(cause) {
|
|
2983
|
+
super("FTS-only statement failed");
|
|
2984
|
+
this.ftsOnlyCause = cause;
|
|
2732
2985
|
}
|
|
2733
2986
|
};
|
|
2987
|
+
function unwrapFtsOnly(err) {
|
|
2988
|
+
try {
|
|
2989
|
+
if (err instanceof FtsOnlyStatementError) return { cause: err.ftsOnlyCause, ftsOnly: true };
|
|
2990
|
+
} catch {
|
|
2991
|
+
}
|
|
2992
|
+
return { cause: err, ftsOnly: false };
|
|
2993
|
+
}
|
|
2994
|
+
function safeSqliteCode(err) {
|
|
2995
|
+
try {
|
|
2996
|
+
const code = extractSqliteCode(err);
|
|
2997
|
+
if (code !== void 0) return code;
|
|
2998
|
+
const lifted = err?.sqliteErrorCode;
|
|
2999
|
+
return typeof lifted === "string" ? lifted : void 0;
|
|
3000
|
+
} catch {
|
|
3001
|
+
return void 0;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
var TRANSIENT_OR_TX_FATAL = ["SQLITE_BUSY", "SQLITE_LOCKED", "SQLITE_FULL", "SQLITE_IOERR", "SQLITE_NOMEM", "SQLITE_INTERRUPT", "SQLITE_ABORT", "SQLITE_READONLY"];
|
|
3005
|
+
function isTransientOrTxFatal(err) {
|
|
3006
|
+
const code = safeSqliteCode(err);
|
|
3007
|
+
return code !== void 0 && TRANSIENT_OR_TX_FATAL.some((c) => code === c || code.startsWith(`${c}_`));
|
|
3008
|
+
}
|
|
3009
|
+
function ftsCorruptionClass(err) {
|
|
3010
|
+
const code = safeSqliteCode(err);
|
|
3011
|
+
if (code === "SQLITE_CORRUPT_VTAB") return "vtab";
|
|
3012
|
+
if (code?.startsWith("SQLITE_CORRUPT")) return "corrupt";
|
|
3013
|
+
return null;
|
|
3014
|
+
}
|
|
3015
|
+
function isSqliteError(err) {
|
|
3016
|
+
return safeSqliteCode(err) !== void 0;
|
|
3017
|
+
}
|
|
2734
3018
|
function assertEntity(entityId, documents) {
|
|
2735
3019
|
for (const d of documents) {
|
|
2736
3020
|
if (d.entity_id !== entityId) {
|
|
@@ -2788,15 +3072,15 @@ async function detachFts5(db, prefix, metadataRepo) {
|
|
|
2788
3072
|
} catch {
|
|
2789
3073
|
}
|
|
2790
3074
|
}
|
|
2791
|
-
async function createIndexStrategy(db, prefix, metadataRepo, preferred) {
|
|
3075
|
+
async function createIndexStrategy(db, prefix, metadataRepo, preferred, opts = {}) {
|
|
2792
3076
|
if (preferred === "minisearch") {
|
|
2793
3077
|
await detachFts5(db, prefix, metadataRepo);
|
|
2794
3078
|
return new MiniSearchIndexStrategy();
|
|
2795
3079
|
}
|
|
2796
3080
|
const attempt = async () => {
|
|
2797
3081
|
if (!await probeFts5(db, prefix)) return "absent";
|
|
2798
|
-
const strategy = new Fts5IndexStrategy(db, prefix, metadataRepo);
|
|
2799
|
-
await strategy.init();
|
|
3082
|
+
const strategy = new Fts5IndexStrategy(db, prefix, metadataRepo, opts.setupBudgetMs);
|
|
3083
|
+
await strategy.init({ entriesExisted: opts.entriesExisted });
|
|
2800
3084
|
return strategy;
|
|
2801
3085
|
};
|
|
2802
3086
|
const absent = async () => {
|
|
@@ -3508,7 +3792,6 @@ var TABLE_PREFIX_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,30}_$/;
|
|
|
3508
3792
|
var SEARCH_REINIT_COOLDOWN_MS = 5e3;
|
|
3509
3793
|
var SEARCH_REINIT_COOLDOWN_MAX_MS = 5 * 6e4;
|
|
3510
3794
|
var SEARCH_REINIT_COST_FACTOR = 10;
|
|
3511
|
-
var monotonicNow = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
3512
3795
|
function decodeDraftCursor(cursor) {
|
|
3513
3796
|
const sep = typeof cursor === "string" ? cursor.indexOf(":") : -1;
|
|
3514
3797
|
const createdAt = sep > 0 ? Number(cursor.slice(0, sep)) : Number.NaN;
|
|
@@ -3523,6 +3806,16 @@ var WikiMemory = class {
|
|
|
3523
3806
|
constructor(db, options) {
|
|
3524
3807
|
/** Emits `__testAccess` console warning at most once per instance when NODE_ENV ≠ "test". */
|
|
3525
3808
|
__privateAdd(this, _testAccessNonTestEnvWarned, false);
|
|
3809
|
+
/**
|
|
3810
|
+
* upsertGraph node and superseded-fact ids per entity, awaiting syncSearchIndex(), each with the
|
|
3811
|
+
* generation of its latest stash. Ids leave the stash only once a drain
|
|
3812
|
+
* succeeded, so a failed drain retries them next call.
|
|
3813
|
+
*/
|
|
3814
|
+
this.upsertIdStash = /* @__PURE__ */ new Map();
|
|
3815
|
+
/** Total ids across upsertIdStash, kept in step so the cap check is O(1). */
|
|
3816
|
+
this.upsertIdStashSize = 0;
|
|
3817
|
+
/** Bumped per stashed id; a re-stash during an in-flight drain outlives that drain's drop. */
|
|
3818
|
+
this.upsertIdStashGen = 0;
|
|
3526
3819
|
this.reinitSearch = null;
|
|
3527
3820
|
/** Monotonic time before which a no-arg syncSearchIndex() skips the re-init (0 = no cooldown). */
|
|
3528
3821
|
this.reinitRetryAt = 0;
|
|
@@ -3659,6 +3952,49 @@ var WikiMemory = class {
|
|
|
3659
3952
|
jobManager: this.jobManager
|
|
3660
3953
|
};
|
|
3661
3954
|
}
|
|
3955
|
+
stashUpsertIds(entityId, ids) {
|
|
3956
|
+
if (!this.searchService.hasLedger()) return;
|
|
3957
|
+
const cap = this.constructor.UPSERT_ID_STASH_CAP;
|
|
3958
|
+
const stash = this.upsertIdStash.get(entityId) ?? /* @__PURE__ */ new Map();
|
|
3959
|
+
for (const id of ids) {
|
|
3960
|
+
if (!stash.has(id)) {
|
|
3961
|
+
if (this.upsertIdStashSize >= cap) continue;
|
|
3962
|
+
this.upsertIdStashSize++;
|
|
3963
|
+
}
|
|
3964
|
+
stash.set(id, ++this.upsertIdStashGen);
|
|
3965
|
+
}
|
|
3966
|
+
if (stash.size > 0) this.upsertIdStash.set(entityId, stash);
|
|
3967
|
+
}
|
|
3968
|
+
/** Snapshot of the stash (one entity, or all) with generations, without removing anything. */
|
|
3969
|
+
peekStashedIds(entityId) {
|
|
3970
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
3971
|
+
for (const [id, stash] of this.upsertIdStash) {
|
|
3972
|
+
if (entityId === void 0 || id === entityId) snapshot.set(id, new Map(stash));
|
|
3973
|
+
}
|
|
3974
|
+
return snapshot;
|
|
3975
|
+
}
|
|
3976
|
+
/**
|
|
3977
|
+
* Removes a drained snapshot. An id re-stashed after the peek has a newer
|
|
3978
|
+
* generation and stays: the drain may have run before that write committed.
|
|
3979
|
+
*/
|
|
3980
|
+
dropStashedIds(snapshot) {
|
|
3981
|
+
for (const [entityId, drained] of snapshot) {
|
|
3982
|
+
const stash = this.upsertIdStash.get(entityId);
|
|
3983
|
+
if (!stash) continue;
|
|
3984
|
+
for (const [id, gen] of drained) {
|
|
3985
|
+
if (stash.get(id) === gen && stash.delete(id)) this.upsertIdStashSize--;
|
|
3986
|
+
}
|
|
3987
|
+
if (stash.size === 0) this.upsertIdStash.delete(entityId);
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
/** `config.setupBudgetMs` or the default, validated: a positive finite number. */
|
|
3991
|
+
setupBudgetMs() {
|
|
3992
|
+
const budget = this.options.config?.setupBudgetMs ?? DEFAULT_SETUP_BUDGET_MS;
|
|
3993
|
+
if (typeof budget !== "number" || !Number.isFinite(budget) || budget <= 0) {
|
|
3994
|
+
throw new TypeError(`setupBudgetMs must be a positive finite number; got ${String(budget)}`);
|
|
3995
|
+
}
|
|
3996
|
+
return budget;
|
|
3997
|
+
}
|
|
3662
3998
|
/**
|
|
3663
3999
|
* Opens the store: migration gate, DDL, schema migrations, then the stamped
|
|
3664
4000
|
* source_ref normalize. Await it before any other call on this instance — the
|
|
@@ -3669,8 +4005,15 @@ var WikiMemory = class {
|
|
|
3669
4005
|
* Pass `{ migrations: 'refuse' }` from inside a writer lease: pending one-time
|
|
3670
4006
|
* work then throws WikiMigrationRequiredError before any mutation, and the
|
|
3671
4007
|
* host runs a plain `setup()` outside the lease.
|
|
4008
|
+
*
|
|
4009
|
+
* Keyword-index catch-up work is bounded by `config.setupBudgetMs`; anything
|
|
4010
|
+
* left converges on later opens and no-arg `syncSearchIndex()` calls. Not
|
|
4011
|
+
* bounded: an index reset (tokenizer change, re-attach after detach, a lost
|
|
4012
|
+
* or inconsistent index), which runs as one unchunked transaction.
|
|
3672
4013
|
*/
|
|
3673
4014
|
async setup(opts) {
|
|
4015
|
+
const setupBudgetMs = this.setupBudgetMs();
|
|
4016
|
+
const openDeadline = monotonicNow() + setupBudgetMs;
|
|
3674
4017
|
const entriesExistedBeforeSetup = await this.metadataRepo.tableExists(`${this.prefix}entries`);
|
|
3675
4018
|
const migrationPolicy = opts?.migrations ?? this.options.config?.migrations ?? "allow";
|
|
3676
4019
|
await setupDatabaseWithGate(this.db, this.prefix, this.metadataRepo, migrationPolicy);
|
|
@@ -3705,10 +4048,11 @@ var WikiMemory = class {
|
|
|
3705
4048
|
this.db,
|
|
3706
4049
|
this.prefix,
|
|
3707
4050
|
this.metadataRepo,
|
|
3708
|
-
this.options.config?.indexStrategy ?? "auto"
|
|
4051
|
+
this.options.config?.indexStrategy ?? "auto",
|
|
4052
|
+
{ setupBudgetMs, entriesExisted: entriesExistedBeforeSetup }
|
|
3709
4053
|
)
|
|
3710
4054
|
);
|
|
3711
|
-
await this.searchService.sync();
|
|
4055
|
+
await this.searchService.sync(void 0, { deadline: openDeadline });
|
|
3712
4056
|
}
|
|
3713
4057
|
async hasChanged(entityId, sourceRefOrEntries, sourceHashArg) {
|
|
3714
4058
|
if (typeof sourceRefOrEntries === "string") {
|
|
@@ -3959,14 +4303,18 @@ var WikiMemory = class {
|
|
|
3959
4303
|
* commit, so without this the nodes stay out of keyword, hybrid and
|
|
3960
4304
|
* fallback retrieval until the entity's next core write.
|
|
3961
4305
|
*
|
|
3962
|
-
* With `entityId
|
|
3963
|
-
*
|
|
3964
|
-
*
|
|
3965
|
-
*
|
|
3966
|
-
*
|
|
3967
|
-
*
|
|
3968
|
-
*
|
|
3969
|
-
*
|
|
4306
|
+
* With `entityId`: drains that entity's pending `upsertGraph` ids (its new
|
|
4307
|
+
* nodes and the facts they superseded) first, to completion, then at most
|
|
4308
|
+
* one more ledger chunk — so its new nodes are searchable, and the
|
|
4309
|
+
* superseded facts gone, when the call resolves, under any backlog. Without an
|
|
4310
|
+
* argument: drains every stashed `upsertGraph` id across entities first,
|
|
4311
|
+
* then runs one keyword-index quantum bounded by `config.setupBudgetMs`
|
|
4312
|
+
* (backlog drain, then rebuild work while the index is rebuilding). Hosts
|
|
4313
|
+
* with a long-lived process should call the no-arg form on their own
|
|
4314
|
+
* schedule: that is what converges a rebuilding index. While keyword search
|
|
4315
|
+
* is unavailable it first retries the index init; after a failed attempt it
|
|
4316
|
+
* waits at least 5 s, doubling per consecutive failure up to 5 min (longer
|
|
4317
|
+
* when an attempt is itself slow), before retrying again.
|
|
3970
4318
|
*
|
|
3971
4319
|
* Call only after the host's transaction commits: called inside it, the
|
|
3972
4320
|
* rebuild may index rows that later roll back, or miss them and clear the
|
|
@@ -3992,10 +4340,11 @@ var WikiMemory = class {
|
|
|
3992
4340
|
this.db,
|
|
3993
4341
|
this.prefix,
|
|
3994
4342
|
this.metadataRepo,
|
|
3995
|
-
this.options.config?.indexStrategy ?? "auto"
|
|
4343
|
+
this.options.config?.indexStrategy ?? "auto",
|
|
4344
|
+
{ setupBudgetMs: this.setupBudgetMs() }
|
|
3996
4345
|
);
|
|
3997
4346
|
this.searchService.setIndexStrategy(strategy);
|
|
3998
|
-
if (this.searchService.isKeywordAvailable()) await this.searchService.sync();
|
|
4347
|
+
if (this.searchService.isKeywordAvailable() && !this.searchService.hasLedger()) await this.searchService.sync();
|
|
3999
4348
|
} finally {
|
|
4000
4349
|
if (this.searchService.isKeywordAvailable()) {
|
|
4001
4350
|
this.reinitFailures = 0;
|
|
@@ -4011,10 +4360,24 @@ var WikiMemory = class {
|
|
|
4011
4360
|
})());
|
|
4012
4361
|
await this.reinitSearch;
|
|
4013
4362
|
}
|
|
4014
|
-
|
|
4363
|
+
const stashed2 = this.peekStashedIds();
|
|
4364
|
+
const ids = [...stashed2.values()].flatMap((m) => [...m.keys()]);
|
|
4365
|
+
if (await this.syncDrained(this.searchService.syncStale({ ids }))) this.dropStashedIds(stashed2);
|
|
4366
|
+
return;
|
|
4015
4367
|
}
|
|
4016
4368
|
assertEntityId(entityId);
|
|
4017
|
-
|
|
4369
|
+
const stashed = this.peekStashedIds(entityId);
|
|
4370
|
+
if (await this.syncDrained(this.searchService.syncEntries(entityId, stashed.get(entityId)?.keys() ?? []))) {
|
|
4371
|
+
this.dropStashedIds(stashed);
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4375
|
+
* True when a sync applied the stashed ids. While keyword search is
|
|
4376
|
+
* unavailable the stand-in strategy's drain is a no-op that reports success,
|
|
4377
|
+
* so the ids stay stashed for the first sync after a re-init (#284 review).
|
|
4378
|
+
*/
|
|
4379
|
+
async syncDrained(sync) {
|
|
4380
|
+
return await sync && this.searchService.isKeywordAvailable();
|
|
4018
4381
|
}
|
|
4019
4382
|
async exportDump(entityIds) {
|
|
4020
4383
|
return this.importExportService.exportDump(entityIds);
|
|
@@ -4065,7 +4428,10 @@ var WikiMemory = class {
|
|
|
4065
4428
|
* post-commit work (search sync, embedding, cache eviction). After the
|
|
4066
4429
|
* transaction commits, call `syncSearchIndex(entityId)` — or
|
|
4067
4430
|
* `syncSearchIndex()` after writing several entities in one transaction —
|
|
4068
|
-
* to make the new nodes keyword-searchable
|
|
4431
|
+
* to make the new nodes keyword-searchable: both forms drain this call's node ids
|
|
4432
|
+
* first, to completion, so the nodes are searchable under any backlog (up to
|
|
4433
|
+
* `WikiMemory.UPSERT_ID_STASH_CAP` pending ids; beyond that they converge with
|
|
4434
|
+
* the backlog). Embeddings still come from the maintenance
|
|
4069
4435
|
* sweep (`runLibrarian` / `runHeal` / `runOntologyBackfill` / `runPrune`,
|
|
4070
4436
|
* scoped via `listEntityIds`).
|
|
4071
4437
|
*
|
|
@@ -4115,12 +4481,13 @@ var WikiMemory = class {
|
|
|
4115
4481
|
});
|
|
4116
4482
|
}
|
|
4117
4483
|
const diagBuffer = new DiagnosticBuffer();
|
|
4118
|
-
const result = await this.ingestionService.upsertGraphCore(
|
|
4484
|
+
const { supersededFactIds, ...result } = await this.ingestionService.upsertGraphCore(
|
|
4119
4485
|
entityId,
|
|
4120
4486
|
{ sourceRef, sourceHash, nodes: params.nodes, edges: params.edges },
|
|
4121
4487
|
adapter,
|
|
4122
4488
|
{ diag: { buffer: diagBuffer, operation: "upsertGraph" } }
|
|
4123
4489
|
);
|
|
4490
|
+
this.stashUpsertIds(entityId, [...supersededFactIds, ...params.nodes.map((n) => n.id)]);
|
|
4124
4491
|
this.searchService.markStale(entityId);
|
|
4125
4492
|
diagBuffer.flush(this.options);
|
|
4126
4493
|
return result;
|
|
@@ -4303,6 +4670,8 @@ var WikiMemory = class {
|
|
|
4303
4670
|
}
|
|
4304
4671
|
};
|
|
4305
4672
|
_testAccessNonTestEnvWarned = new WeakMap();
|
|
4673
|
+
/** Max upsertGraph node ids held for their post-commit sync (spec #280 id threading). */
|
|
4674
|
+
WikiMemory.UPSERT_ID_STASH_CAP = 1e4;
|
|
4306
4675
|
function assertEntityId(entityId) {
|
|
4307
4676
|
if (typeof entityId !== "string" || entityId.length === 0) {
|
|
4308
4677
|
throw new TypeError("Invalid entityId: must be a non-empty string.");
|
|
@@ -5228,6 +5597,6 @@ function createWiki(db, options) {
|
|
|
5228
5597
|
return new WikiMemory(db, options);
|
|
5229
5598
|
}
|
|
5230
5599
|
|
|
5231
|
-
export { DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT, WikiMemory, createWiki, formatContext, formatGraphContext, formatMemoryDump, formatOkfBundle, hydrateLibrarianPrompt, mapLibrarianOptionsToReadOptions, parseOkfBundle, validateLibrarianPromptTemplate };
|
|
5600
|
+
export { DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT, DEFAULT_SETUP_BUDGET_MS, WikiMemory, createWiki, formatContext, formatGraphContext, formatMemoryDump, formatOkfBundle, hydrateLibrarianPrompt, mapLibrarianOptionsToReadOptions, parseOkfBundle, validateLibrarianPromptTemplate };
|
|
5232
5601
|
//# sourceMappingURL=index.mjs.map
|
|
5233
5602
|
//# sourceMappingURL=index.mjs.map
|