@dikolab/kbdb 0.2.2 → 0.3.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/README.md +50 -15
- package/dist/cli.cjs +461 -72
- package/dist/cli.cjs.map +4 -4
- package/dist/cli.mjs +453 -64
- package/dist/cli.mjs.map +4 -4
- package/dist/kbdb-worker.cjs +395 -60
- package/dist/kbdb-worker.cjs.map +4 -4
- package/dist/mod.cjs +17 -0
- package/dist/mod.cjs.map +2 -2
- package/dist/mod.mjs +17 -0
- package/dist/mod.mjs.map +2 -2
- package/dist/src/shared/cli/functions/dispatch-command.function.d.ts +3 -1
- package/dist/src/shared/cli/functions/install-default-skills.function.d.ts +12 -0
- package/dist/src/shared/cli/functions/run-check.function.d.ts +47 -5
- package/dist/src/shared/cli/functions/run-export.function.d.ts +34 -0
- package/dist/src/shared/cli/functions/run-gc.function.d.ts +6 -0
- package/dist/src/shared/cli/functions/run-learn.function.d.ts +35 -6
- package/dist/src/shared/cli/functions/run-recall.function.d.ts +45 -0
- package/dist/src/shared/cli/functions/run-search.function.d.ts +52 -15
- package/dist/src/shared/cli/index.d.ts +4 -0
- package/dist/src/shared/cli/typings/cli-options.interface.d.ts +13 -0
- package/dist/src/shared/cli/typings/learn-result.model.d.ts +19 -0
- package/dist/src/shared/cli/typings/search-display.model.d.ts +18 -0
- package/dist/src/shared/mcp/classes/mcp-server.class.d.ts +8 -0
- package/dist/src/shared/mcp/typings/mcp-client.interface.d.ts +43 -0
- package/dist/src/shared/wasm-codec/functions/decode-add-section-result.function.d.ts +48 -0
- package/dist/src/shared/wasm-codec/functions/decode-scored-results.function.d.ts +58 -0
- package/dist/src/shared/wasm-codec/functions/decode-section-records.function.d.ts +33 -0
- package/dist/src/shared/wasm-codec/functions/encode-add-section-params.function.d.ts +52 -0
- package/dist/src/shared/wasm-codec/index.d.ts +2 -0
- package/dist/src/shared/worker-client/classes/worker-client.class.d.ts +16 -0
- package/dist/src/shared/worker-client/index.d.ts +1 -1
- package/dist/src/shared/worker-client/typings/add-section-result.model.d.ts +29 -15
- package/dist/src/shared/worker-client/typings/export-result.model.d.ts +14 -0
- package/dist/src/shared/worker-client/typings/gc-result.model.d.ts +6 -0
- package/dist/src/shared/worker-client/typings/search-result.model.d.ts +12 -0
- package/dist/src/shared/worker-client/typings/section.model.d.ts +16 -0
- package/dist/src/shared/worker-daemon/classes/worker-daemon.class.d.ts +35 -0
- package/dist/wasm/fs-database/kbdb_fs_database.d.ts +22 -1
- package/dist/wasm/fs-database/kbdb_fs_database.js +28 -1
- package/dist/wasm/fs-database/kbdb_fs_database_bg.wasm +0 -0
- package/dist/wasm/fs-database/kbdb_fs_database_bg.wasm.d.ts +1 -0
- package/dist/wasm/kb-worker/kbdb_worker.d.ts +48 -1
- package/dist/wasm/kb-worker/kbdb_worker.js +60 -1
- package/dist/wasm/kb-worker/kbdb_worker_bg.wasm +0 -0
- package/dist/wasm/kb-worker/kbdb_worker_bg.wasm.d.ts +2 -0
- package/dist/wasm/query-parser/kbdb_query_parser_bg.wasm +0 -0
- package/dist/worker.mjs +396 -60
- package/dist/worker.mjs.map +4 -4
- package/docs/details/cli.md +102 -15
- package/docs/details/library-api.md +55 -6
- package/docs/details/mcp-server.md +8 -4
- package/docs/details/search-and-ranking.md +63 -5
- package/docs/details/storage.md +2 -0
- package/docs/goals/cli.md +255 -14
- package/docs/goals/database.md +244 -17
- package/docs/goals/document.md +1 -1
- package/docs/goals/mcp.md +83 -14
- package/docs/goals/query-parser.md +98 -4
- package/docs/goals/query-result.md +177 -10
- package/docs/goals/skills.md +72 -0
- package/docs/goals/worker-daemon.md +54 -7
- package/docs/modules/rust/fs-database/functions/integrity.md +11 -0
- package/docs/modules/rust/fs-database/functions/section.md +23 -6
- package/docs/modules/rust/fs-database/module.md +1 -0
- package/docs/modules/rust/fs-database/types/section.md +27 -1
- package/docs/modules/rust/overview.md +2 -1
- package/docs/modules/rust/query-parser/functions/module.md +2 -2
- package/docs/modules/rust/query-parser/functions/pipeline.md +44 -6
- package/docs/modules/rust/query-parser/module.md +5 -4
- package/docs/modules/rust/query-parser/types/keyphrase.md +11 -0
- package/docs/modules/rust/query-parser/types/module.md +3 -3
- package/docs/modules/typescript/shared/cli/functions.md +68 -3
- package/package.json +1 -1
- package/src/shared/cli/functions/dispatch-command.function.ts +53 -1
- package/src/shared/cli/functions/format-output.function.ts +36 -12
- package/src/shared/cli/functions/generate-help.function.ts +56 -1
- package/src/shared/cli/functions/install-default-skills.function.ts +296 -0
- package/src/shared/cli/functions/parse-args.function.ts +13 -0
- package/src/shared/cli/functions/run-check.function.ts +87 -6
- package/src/shared/cli/functions/run-export.function.ts +40 -0
- package/src/shared/cli/functions/run-gc.function.ts +6 -0
- package/src/shared/cli/functions/run-init.function.ts +2 -0
- package/src/shared/cli/functions/run-learn.function.ts +68 -16
- package/src/shared/cli/functions/run-recall.function.ts +66 -0
- package/src/shared/cli/functions/run-search.function.ts +58 -15
- package/src/shared/cli/index.ts +4 -0
- package/src/shared/cli/typings/cli-options.interface.ts +13 -0
- package/src/shared/cli/typings/learn-result.model.ts +16 -0
- package/src/shared/cli/typings/search-display.model.ts +18 -0
- package/src/shared/mcp/classes/mcp-server.class.ts +30 -1
- package/src/shared/mcp/functions/create-tool-definitions.function.ts +46 -5
- package/src/shared/mcp/functions/handle-tool-call.function.ts +70 -2
- package/src/shared/mcp/typings/mcp-client.interface.ts +45 -0
- package/src/shared/wasm-codec/functions/decode-add-section-result.function.ts +112 -0
- package/src/shared/wasm-codec/functions/decode-scored-results.function.ts +86 -0
- package/src/shared/wasm-codec/functions/decode-section-records.function.ts +87 -0
- package/src/shared/wasm-codec/functions/encode-add-section-params.function.ts +68 -0
- package/src/shared/wasm-codec/index.ts +5 -0
- package/src/shared/worker-client/classes/worker-client.class.ts +19 -0
- package/src/shared/worker-client/index.ts +4 -1
- package/src/shared/worker-client/typings/add-section-result.model.ts +30 -15
- package/src/shared/worker-client/typings/export-result.model.ts +14 -0
- package/src/shared/worker-client/typings/gc-result.model.ts +6 -0
- package/src/shared/worker-client/typings/search-result.model.ts +12 -0
- package/src/shared/worker-client/typings/section.model.ts +16 -0
- package/src/shared/worker-daemon/classes/worker-daemon.class.ts +315 -46
- package/src/shared/worker-daemon/functions/create-host-imports.function.ts +19 -4
- package/src/shared/worker-daemon/functions/create-ipc-server.function.ts +5 -1
- package/src/wasm/fs-database/kbdb_fs_database.d.ts +22 -1
- package/src/wasm/fs-database/kbdb_fs_database.js +28 -1
- package/src/wasm/fs-database/kbdb_fs_database_bg.wasm +0 -0
- package/src/wasm/fs-database/kbdb_fs_database_bg.wasm.d.ts +1 -0
- package/src/wasm/kb-worker/kbdb_worker.d.ts +48 -1
- package/src/wasm/kb-worker/kbdb_worker.js +60 -1
- package/src/wasm/kb-worker/kbdb_worker_bg.wasm +0 -0
- package/src/wasm/kb-worker/kbdb_worker_bg.wasm.d.ts +2 -0
- package/src/wasm/query-parser/kbdb_query_parser_bg.wasm +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
existsSync,
|
|
3
3
|
readFileSync,
|
|
4
|
+
readdirSync,
|
|
4
5
|
accessSync,
|
|
5
6
|
writeFileSync,
|
|
6
7
|
unlinkSync,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
decodeKeyValueText,
|
|
39
40
|
decodeRemoveFlag,
|
|
40
41
|
decodeRecallResult,
|
|
42
|
+
decodeAddSectionResult,
|
|
41
43
|
} from '../../wasm-codec/index.ts';
|
|
42
44
|
|
|
43
45
|
import type { AddSectionInput } from '../../wasm-codec/index.ts';
|
|
@@ -89,6 +91,8 @@ const WASM_METHOD_MAP: Readonly<Partial<Record<string, string>>> = {
|
|
|
89
91
|
listDocumentsByType: 'worker_list_docs_by_type',
|
|
90
92
|
list_docs_by_type: 'worker_list_docs_by_type',
|
|
91
93
|
recall: 'worker_recall',
|
|
94
|
+
export: 'worker_export',
|
|
95
|
+
exportDb: 'worker_export',
|
|
92
96
|
};
|
|
93
97
|
|
|
94
98
|
/** Config method name prefix. */
|
|
@@ -155,6 +159,18 @@ export class WorkerDaemon {
|
|
|
155
159
|
private _server: Server | null = null;
|
|
156
160
|
private readonly _wasmDir: string;
|
|
157
161
|
|
|
162
|
+
/** Queue of waiters for the single-writer lock. */
|
|
163
|
+
private _writeQueue: { resolve: () => void }[] = [];
|
|
164
|
+
|
|
165
|
+
/** Whether the write lock is currently held. */
|
|
166
|
+
private _writeLocked = false;
|
|
167
|
+
|
|
168
|
+
/** Whether the startup integrity check passed. */
|
|
169
|
+
private _integrityOk = true;
|
|
170
|
+
|
|
171
|
+
/** Integrity errors found at startup, if any. */
|
|
172
|
+
private _integrityErrors: string[] = [];
|
|
173
|
+
|
|
158
174
|
/**
|
|
159
175
|
* @param ctx - context ID (16-hex characters), the first 16 of
|
|
160
176
|
* the SHA-256 hash of `targetDir`
|
|
@@ -229,6 +245,7 @@ export class WorkerDaemon {
|
|
|
229
245
|
this._wasmModules = wasmModules;
|
|
230
246
|
|
|
231
247
|
this.initWorkerContext();
|
|
248
|
+
this.runStartupIntegrityCheck();
|
|
232
249
|
|
|
233
250
|
const pidPath = join(tmpdir(), `kbdb-${this.ctx}.pid`);
|
|
234
251
|
writeFileSync(pidPath, process.pid.toString());
|
|
@@ -470,7 +487,11 @@ export class WorkerDaemon {
|
|
|
470
487
|
params: Record<string, unknown>,
|
|
471
488
|
): unknown {
|
|
472
489
|
if (method === 'status') {
|
|
473
|
-
return
|
|
490
|
+
return {
|
|
491
|
+
status: this._connectionCount > 0 ? 'busy' : 'idle',
|
|
492
|
+
integrityOk: this._integrityOk,
|
|
493
|
+
integrityErrors: this._integrityErrors,
|
|
494
|
+
};
|
|
474
495
|
}
|
|
475
496
|
|
|
476
497
|
if (method === 'idle_at') {
|
|
@@ -493,71 +514,36 @@ export class WorkerDaemon {
|
|
|
493
514
|
throw new IpcError(-32601, `Method not found: ${method}`);
|
|
494
515
|
}
|
|
495
516
|
|
|
496
|
-
private dispatchWasm(
|
|
517
|
+
private async dispatchWasm(
|
|
497
518
|
method: string,
|
|
498
519
|
wasmExport: string,
|
|
499
520
|
params: Record<string, unknown>,
|
|
500
|
-
): unknown {
|
|
521
|
+
): Promise<unknown> {
|
|
522
|
+
// Read-only operations — no write lock required.
|
|
501
523
|
if (method === 'search' || method === 'query') {
|
|
502
524
|
return this.handleSearch(params);
|
|
503
525
|
}
|
|
504
|
-
if (method === 'addSection' || method === 'add_section') {
|
|
505
|
-
return this.handleAddSection(params);
|
|
506
|
-
}
|
|
507
|
-
if (method === 'updateSection' || method === 'update_section') {
|
|
508
|
-
return this.handleUpdateSection(params);
|
|
509
|
-
}
|
|
510
|
-
if (method === 'removeSection' || method === 'remove_section') {
|
|
511
|
-
return this.handleRemoveSection(params);
|
|
512
|
-
}
|
|
513
526
|
if (method === 'readSections' || method === 'read_sections') {
|
|
514
527
|
return this.handleReadSections(params);
|
|
515
528
|
}
|
|
516
529
|
if (method === 'retrieveDocument' || method === 'retrieve_doc') {
|
|
517
530
|
return this.handleRetrieveDoc(params);
|
|
518
531
|
}
|
|
519
|
-
if (method === 'groupSections' || method === 'group_sections') {
|
|
520
|
-
return this.handleGroupSections(params);
|
|
521
|
-
}
|
|
522
|
-
if (method === 'removeGrouping' || method === 'remove_grouping') {
|
|
523
|
-
return this.handleRemoveGrouping(params);
|
|
524
|
-
}
|
|
525
532
|
if (method === 'content' || method === 'compose') {
|
|
526
533
|
return this.handleCompose(params);
|
|
527
534
|
}
|
|
528
535
|
if (method === 'integrityCheck' || method === 'integrity_check') {
|
|
529
536
|
return this.handleIntegrityCheck();
|
|
530
537
|
}
|
|
531
|
-
if (method === 'gc') {
|
|
532
|
-
return this.handleGc();
|
|
533
|
-
}
|
|
534
|
-
if (method === 'rebuildIndexes' || method === 'rebuild_indexes') {
|
|
535
|
-
return this.handleRebuild();
|
|
536
|
-
}
|
|
537
538
|
if (method === 'dbStatus' || method === 'db_status') {
|
|
538
539
|
return this.handleDbStatus();
|
|
539
540
|
}
|
|
540
|
-
if (method === 'initDirectory' || method === 'init_directory') {
|
|
541
|
-
return this.handleInitDirectory(params);
|
|
542
|
-
}
|
|
543
|
-
if (method === 'invalidate') {
|
|
544
|
-
return this.handleInvalidate(params);
|
|
545
|
-
}
|
|
546
|
-
if (method === 'invalidateAll' || method === 'invalidate_all') {
|
|
547
|
-
return this.handleInvalidateAll();
|
|
548
|
-
}
|
|
549
541
|
if (method === 'get_result_status') {
|
|
550
542
|
return this.handleResultStatus(params);
|
|
551
543
|
}
|
|
552
544
|
if (method === 'get_result') {
|
|
553
545
|
return this.handleGetResult(params);
|
|
554
546
|
}
|
|
555
|
-
if (method === 'migrateDatabase') {
|
|
556
|
-
throw new IpcError(
|
|
557
|
-
-32601,
|
|
558
|
-
'migrateDatabase is not yet supported',
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
547
|
if (method === 'listByType' || method === 'list_by_type') {
|
|
562
548
|
return this.handleListByType(params);
|
|
563
549
|
}
|
|
@@ -570,7 +556,74 @@ export class WorkerDaemon {
|
|
|
570
556
|
if (method === 'recall') {
|
|
571
557
|
return this.handleRecall(params);
|
|
572
558
|
}
|
|
573
|
-
|
|
559
|
+
if (method === 'export' || method === 'exportDb') {
|
|
560
|
+
return this.handleExport(params);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Write operations — serialised through the single-writer lock.
|
|
564
|
+
await this.acquireWriteLock();
|
|
565
|
+
try {
|
|
566
|
+
if (method === 'invalidate') {
|
|
567
|
+
return this.handleInvalidate(params);
|
|
568
|
+
}
|
|
569
|
+
if (
|
|
570
|
+
method === 'invalidateAll' ||
|
|
571
|
+
method === 'invalidate_all'
|
|
572
|
+
) {
|
|
573
|
+
return this.handleInvalidateAll();
|
|
574
|
+
}
|
|
575
|
+
if (method === 'addSection' || method === 'add_section') {
|
|
576
|
+
return this.handleAddSection(params);
|
|
577
|
+
}
|
|
578
|
+
if (
|
|
579
|
+
method === 'updateSection' ||
|
|
580
|
+
method === 'update_section'
|
|
581
|
+
) {
|
|
582
|
+
return this.handleUpdateSection(params);
|
|
583
|
+
}
|
|
584
|
+
if (
|
|
585
|
+
method === 'removeSection' ||
|
|
586
|
+
method === 'remove_section'
|
|
587
|
+
) {
|
|
588
|
+
return this.handleRemoveSection(params);
|
|
589
|
+
}
|
|
590
|
+
if (
|
|
591
|
+
method === 'groupSections' ||
|
|
592
|
+
method === 'group_sections'
|
|
593
|
+
) {
|
|
594
|
+
return this.handleGroupSections(params);
|
|
595
|
+
}
|
|
596
|
+
if (
|
|
597
|
+
method === 'removeGrouping' ||
|
|
598
|
+
method === 'remove_grouping'
|
|
599
|
+
) {
|
|
600
|
+
return this.handleRemoveGrouping(params);
|
|
601
|
+
}
|
|
602
|
+
if (method === 'gc') {
|
|
603
|
+
return this.handleGc();
|
|
604
|
+
}
|
|
605
|
+
if (
|
|
606
|
+
method === 'rebuildIndexes' ||
|
|
607
|
+
method === 'rebuild_indexes'
|
|
608
|
+
) {
|
|
609
|
+
return this.handleRebuild();
|
|
610
|
+
}
|
|
611
|
+
if (
|
|
612
|
+
method === 'initDirectory' ||
|
|
613
|
+
method === 'init_directory'
|
|
614
|
+
) {
|
|
615
|
+
return this.handleInitDirectory(params);
|
|
616
|
+
}
|
|
617
|
+
if (method === 'migrateDatabase') {
|
|
618
|
+
throw new IpcError(
|
|
619
|
+
-32601,
|
|
620
|
+
'migrateDatabase is not yet supported',
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
return this.invokeWasmRaw(wasmExport, new Uint8Array(0));
|
|
624
|
+
} finally {
|
|
625
|
+
this.releaseWriteLock();
|
|
626
|
+
}
|
|
574
627
|
}
|
|
575
628
|
|
|
576
629
|
// -- Search/query (token-based async) --
|
|
@@ -643,6 +696,9 @@ export class WorkerDaemon {
|
|
|
643
696
|
: 'text';
|
|
644
697
|
const typeName =
|
|
645
698
|
sectionType !== semanticType ? sectionType : null;
|
|
699
|
+
const tags = Array.isArray(params['tags'])
|
|
700
|
+
? (params['tags'] as string[])
|
|
701
|
+
: [];
|
|
646
702
|
const input: AddSectionInput = {
|
|
647
703
|
sectionType: semanticType,
|
|
648
704
|
mimeType: params['mimeType'] as string | undefined,
|
|
@@ -651,11 +707,32 @@ export class WorkerDaemon {
|
|
|
651
707
|
description: (params['description'] as string | null) ?? null,
|
|
652
708
|
docid: (params['docid'] as string | null) ?? null,
|
|
653
709
|
typeName,
|
|
710
|
+
sourcePath:
|
|
711
|
+
typeof params['sourcePath'] === 'string'
|
|
712
|
+
? params['sourcePath']
|
|
713
|
+
: undefined,
|
|
714
|
+
sourceMtime:
|
|
715
|
+
typeof params['sourceMtime'] === 'string'
|
|
716
|
+
? params['sourceMtime']
|
|
717
|
+
: undefined,
|
|
718
|
+
tags,
|
|
719
|
+
replace: params['replace'] === true,
|
|
654
720
|
};
|
|
655
721
|
const encoded = encodeAddSectionParams(input);
|
|
656
722
|
const bytes = this.callWasmSync('worker_add_section', encoded);
|
|
657
|
-
const
|
|
658
|
-
|
|
723
|
+
const decoded = decodeAddSectionResult(bytes);
|
|
724
|
+
const nearDups =
|
|
725
|
+
decoded.nearDuplicates.length > 0
|
|
726
|
+
? decoded.nearDuplicates
|
|
727
|
+
: this.scanNearDuplicates(input.content, decoded.kbid);
|
|
728
|
+
return {
|
|
729
|
+
kbid: decoded.kbid,
|
|
730
|
+
indexed_terms_count: decoded.indexedTermsCount,
|
|
731
|
+
elapsed_ms: decoded.elapsedMs,
|
|
732
|
+
superseded: decoded.superseded,
|
|
733
|
+
warnings: decoded.warnings,
|
|
734
|
+
near_duplicates: nearDups,
|
|
735
|
+
};
|
|
659
736
|
}
|
|
660
737
|
|
|
661
738
|
private handleUpdateSection(
|
|
@@ -759,7 +836,18 @@ export class WorkerDaemon {
|
|
|
759
836
|
private handleInvalidate(params: Record<string, unknown>): unknown {
|
|
760
837
|
const kbid = (params['kbid'] as string | undefined) ?? '';
|
|
761
838
|
const encoded = new TextEncoder().encode(kbid);
|
|
762
|
-
this.
|
|
839
|
+
const fn = this.getExport('worker_invalidate');
|
|
840
|
+
const ptr = this.wasmAlloc(encoded.length);
|
|
841
|
+
this.wasmWrite(ptr, encoded);
|
|
842
|
+
const rc = fn(ptr, encoded.length) as number;
|
|
843
|
+
this.wasmFree(ptr, encoded.length);
|
|
844
|
+
if (rc === -1) {
|
|
845
|
+
const errBytes = this.readReturnSlot();
|
|
846
|
+
throw new IpcError(
|
|
847
|
+
-32000,
|
|
848
|
+
new TextDecoder().decode(errBytes) || 'invalidate failed',
|
|
849
|
+
);
|
|
850
|
+
}
|
|
763
851
|
return { ok: true };
|
|
764
852
|
}
|
|
765
853
|
|
|
@@ -819,15 +907,161 @@ export class WorkerDaemon {
|
|
|
819
907
|
}
|
|
820
908
|
|
|
821
909
|
private handleGc(): unknown {
|
|
910
|
+
const start = Date.now();
|
|
822
911
|
this.callWasmZeroArg('worker_garbage_collect');
|
|
823
912
|
const bytes = this.readReturnSlot();
|
|
824
913
|
const text = new TextDecoder().decode(bytes);
|
|
825
|
-
|
|
914
|
+
const kv = decodeKeyValueText(text);
|
|
915
|
+
const removedCount = Number(kv['removed'] ?? 0);
|
|
916
|
+
const staleSources = Number(kv['stale_sources'] ?? 0);
|
|
917
|
+
return {
|
|
918
|
+
removed_sections: removedCount,
|
|
919
|
+
removed_bytes: 0,
|
|
920
|
+
elapsed_ms: Date.now() - start,
|
|
921
|
+
stale_sources: staleSources,
|
|
922
|
+
};
|
|
826
923
|
}
|
|
827
924
|
|
|
828
925
|
private handleRebuild(): unknown {
|
|
829
|
-
this.
|
|
830
|
-
|
|
926
|
+
const fn = this.getExport('worker_rebuild_indexes');
|
|
927
|
+
const rc = fn() as number;
|
|
928
|
+
if (rc === -1) {
|
|
929
|
+
const errBytes = this.readReturnSlot();
|
|
930
|
+
const errMsg = new TextDecoder().decode(errBytes);
|
|
931
|
+
throw new IpcError(-32000, errMsg || 'rebuild failed');
|
|
932
|
+
}
|
|
933
|
+
const resultBytes = this.readReturnSlot();
|
|
934
|
+
const reindexed =
|
|
935
|
+
resultBytes.length >= 4
|
|
936
|
+
? new DataView(
|
|
937
|
+
resultBytes.buffer,
|
|
938
|
+
resultBytes.byteOffset,
|
|
939
|
+
resultBytes.byteLength,
|
|
940
|
+
).getUint32(0, true)
|
|
941
|
+
: 0;
|
|
942
|
+
return { ok: true, sections_reindexed: reindexed };
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// -- Export (read operation, no lock required) --
|
|
946
|
+
|
|
947
|
+
private handleExport(params: Record<string, unknown>): unknown {
|
|
948
|
+
const targetPath =
|
|
949
|
+
(params['targetPath'] as string | undefined) ??
|
|
950
|
+
(params['path'] as string | undefined) ??
|
|
951
|
+
'./kbdb-export/';
|
|
952
|
+
const encoded = new TextEncoder().encode(targetPath);
|
|
953
|
+
const start = Date.now();
|
|
954
|
+
const bytes = this.callWasmSync('worker_export', encoded);
|
|
955
|
+
const elapsed = Date.now() - start;
|
|
956
|
+
const text = new TextDecoder().decode(bytes);
|
|
957
|
+
const kv = decodeKeyValueText(text);
|
|
958
|
+
return {
|
|
959
|
+
path: kv['path'] ?? targetPath,
|
|
960
|
+
sections_exported: Number(kv['sections_exported'] ?? 0),
|
|
961
|
+
documents_exported: Number(kv['documents_exported'] ?? 0),
|
|
962
|
+
elapsed_ms: elapsed,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// -- Near-duplicate detection --
|
|
967
|
+
|
|
968
|
+
private scanNearDuplicates(
|
|
969
|
+
content: string,
|
|
970
|
+
newKbid: string,
|
|
971
|
+
): { kbid: string; similarity: number }[] {
|
|
972
|
+
const newTerms = this.extractTermSet(content);
|
|
973
|
+
if (newTerms.size === 0) return [];
|
|
974
|
+
|
|
975
|
+
const sectionsDir = join(this.targetDir, 'sections');
|
|
976
|
+
let shards: string[];
|
|
977
|
+
try {
|
|
978
|
+
shards = readdirSync(sectionsDir);
|
|
979
|
+
} catch {
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const results: { kbid: string; similarity: number }[] = [];
|
|
984
|
+
for (const shard of shards) {
|
|
985
|
+
let files: string[];
|
|
986
|
+
try {
|
|
987
|
+
files = readdirSync(join(sectionsDir, shard));
|
|
988
|
+
} catch {
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
for (const file of files) {
|
|
992
|
+
if (!file.endsWith('.section')) continue;
|
|
993
|
+
const kbid = file.slice(0, -8);
|
|
994
|
+
if (kbid === newKbid) continue;
|
|
995
|
+
try {
|
|
996
|
+
const data = readFileSync(
|
|
997
|
+
join(sectionsDir, shard, file),
|
|
998
|
+
'utf-8',
|
|
999
|
+
);
|
|
1000
|
+
const sep = data.indexOf('\n---\n');
|
|
1001
|
+
const body = sep >= 0 ? data.slice(sep + 5) : data;
|
|
1002
|
+
const existing = this.extractTermSet(body);
|
|
1003
|
+
let intersection = 0;
|
|
1004
|
+
for (const t of newTerms) {
|
|
1005
|
+
if (existing.has(t)) intersection++;
|
|
1006
|
+
}
|
|
1007
|
+
const union =
|
|
1008
|
+
newTerms.size + existing.size - intersection;
|
|
1009
|
+
const sim = union > 0 ? intersection / union : 0;
|
|
1010
|
+
if (sim >= 0.85) {
|
|
1011
|
+
results.push({
|
|
1012
|
+
kbid,
|
|
1013
|
+
similarity: Math.round(sim * 100) / 100,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
} catch {
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return results;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
private extractTermSet(text: string): Set<string> {
|
|
1025
|
+
return new Set(
|
|
1026
|
+
text
|
|
1027
|
+
.toLowerCase()
|
|
1028
|
+
.split(/\W+/)
|
|
1029
|
+
.filter((w) => w.length > 2),
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// -- Single-writer lock --
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Acquires the write lock.
|
|
1037
|
+
*
|
|
1038
|
+
* If the lock is free it is granted immediately. If it is held the
|
|
1039
|
+
* caller is queued and its promise resolves once the lock is
|
|
1040
|
+
* released by the current holder.
|
|
1041
|
+
*/
|
|
1042
|
+
private acquireWriteLock(): Promise<void> {
|
|
1043
|
+
if (!this._writeLocked) {
|
|
1044
|
+
this._writeLocked = true;
|
|
1045
|
+
return Promise.resolve();
|
|
1046
|
+
}
|
|
1047
|
+
return new Promise<void>((resolve) => {
|
|
1048
|
+
this._writeQueue.push({ resolve });
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Releases the write lock.
|
|
1054
|
+
*
|
|
1055
|
+
* If waiters are queued the next one is granted the lock
|
|
1056
|
+
* immediately. Otherwise the lock is marked free.
|
|
1057
|
+
*/
|
|
1058
|
+
private releaseWriteLock(): void {
|
|
1059
|
+
const next = this._writeQueue.shift();
|
|
1060
|
+
if (next !== undefined) {
|
|
1061
|
+
next.resolve();
|
|
1062
|
+
} else {
|
|
1063
|
+
this._writeLocked = false;
|
|
1064
|
+
}
|
|
831
1065
|
}
|
|
832
1066
|
|
|
833
1067
|
// -- WASM lifecycle --
|
|
@@ -837,6 +1071,41 @@ export class WorkerDaemon {
|
|
|
837
1071
|
this.callWasmSync('worker_init', pathBytes);
|
|
838
1072
|
}
|
|
839
1073
|
|
|
1074
|
+
/**
|
|
1075
|
+
* Runs a database integrity check immediately after context
|
|
1076
|
+
* initialisation.
|
|
1077
|
+
*
|
|
1078
|
+
* Stores the result in `_integrityOk` and `_integrityErrors`.
|
|
1079
|
+
* Logs a warning to stderr when issues are found so operators
|
|
1080
|
+
* are alerted without aborting the daemon.
|
|
1081
|
+
*/
|
|
1082
|
+
private runStartupIntegrityCheck(): void {
|
|
1083
|
+
try {
|
|
1084
|
+
this.callWasmZeroArg('worker_check_integrity');
|
|
1085
|
+
const bytes = this.readReturnSlot();
|
|
1086
|
+
const text = new TextDecoder().decode(bytes);
|
|
1087
|
+
const kv = decodeKeyValueText(text);
|
|
1088
|
+
this._integrityOk = kv['ok'] === 'true';
|
|
1089
|
+
if (!this._integrityOk) {
|
|
1090
|
+
this._integrityErrors = Object.entries(kv)
|
|
1091
|
+
.filter(([k]) => k !== 'ok')
|
|
1092
|
+
.map(([k, v]) => `${k}=${v}`);
|
|
1093
|
+
// eslint-disable-next-line no-console
|
|
1094
|
+
console.error(
|
|
1095
|
+
'[kbdb-worker] startup integrity check failed:',
|
|
1096
|
+
this._integrityErrors.join(', '),
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
} catch {
|
|
1100
|
+
this._integrityOk = false;
|
|
1101
|
+
this._integrityErrors = ['integrity_check_failed'];
|
|
1102
|
+
// eslint-disable-next-line no-console
|
|
1103
|
+
console.error(
|
|
1104
|
+
'[kbdb-worker] startup integrity check threw an error',
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
840
1109
|
// -- WASM bridge primitives --
|
|
841
1110
|
|
|
842
1111
|
private getExport(name: string): WasmFn {
|
|
@@ -136,8 +136,9 @@ export function createHostImports(
|
|
|
136
136
|
|
|
137
137
|
function readPathArg(ptr: number, len: number): string {
|
|
138
138
|
const bytes = new Uint8Array(getBuffer(), ptr, len);
|
|
139
|
-
const
|
|
140
|
-
|
|
139
|
+
const decoded = new TextDecoder().decode(bytes);
|
|
140
|
+
if (decoded.startsWith('/')) return decoded;
|
|
141
|
+
return join(contextPath, decoded);
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
return {
|
|
@@ -163,14 +164,21 @@ export function createHostImports(
|
|
|
163
164
|
const filePath = readPathArg(pathPtr, pathLen);
|
|
164
165
|
const data = fs.readFileSync(filePath);
|
|
165
166
|
const dataLen = data.byteLength;
|
|
166
|
-
const dv = new DataView(getBuffer());
|
|
167
167
|
if (dataLen === 0) {
|
|
168
|
+
// No allocation -- buffer cannot have grown; safe to
|
|
169
|
+
// create DataView now.
|
|
170
|
+
const dv = new DataView(getBuffer());
|
|
168
171
|
dv.setUint32(outPtr, 0, true);
|
|
169
172
|
dv.setUint32(outLen, 0, true);
|
|
170
173
|
return 0;
|
|
171
174
|
}
|
|
172
175
|
const dataPtr = allocator.alloc(dataLen);
|
|
173
176
|
if (dataPtr === 0) return -1;
|
|
177
|
+
// Create DataView AFTER alloc() -- alloc may trigger WASM
|
|
178
|
+
// memory growth, which detaches the old ArrayBuffer and
|
|
179
|
+
// replaces it with a new one. A DataView captured before
|
|
180
|
+
// growth would reference the detached buffer and throw.
|
|
181
|
+
const dv = new DataView(getBuffer());
|
|
174
182
|
const memView = new Uint8Array(getBuffer());
|
|
175
183
|
memView.set(
|
|
176
184
|
new Uint8Array(
|
|
@@ -267,14 +275,21 @@ export function createHostImports(
|
|
|
267
275
|
const entries = fs.readdirSync(dirPath);
|
|
268
276
|
const text = entries.join('\n');
|
|
269
277
|
const encoded = new TextEncoder().encode(text);
|
|
270
|
-
const dv = new DataView(getBuffer());
|
|
271
278
|
if (encoded.length === 0) {
|
|
279
|
+
// No allocation -- buffer cannot have grown; safe to
|
|
280
|
+
// create DataView now.
|
|
281
|
+
const dv = new DataView(getBuffer());
|
|
272
282
|
dv.setUint32(outPtr, 0, true);
|
|
273
283
|
dv.setUint32(outLen, 0, true);
|
|
274
284
|
return 0;
|
|
275
285
|
}
|
|
276
286
|
const dataPtr = allocator.alloc(encoded.length);
|
|
277
287
|
if (dataPtr === 0) return -1;
|
|
288
|
+
// Create DataView AFTER alloc() -- alloc may trigger WASM
|
|
289
|
+
// memory growth, which detaches the old ArrayBuffer and
|
|
290
|
+
// replaces it with a new one. A DataView captured before
|
|
291
|
+
// growth would reference the detached buffer and throw.
|
|
292
|
+
const dv = new DataView(getBuffer());
|
|
278
293
|
new Uint8Array(getBuffer(), dataPtr, encoded.length).set(
|
|
279
294
|
encoded,
|
|
280
295
|
);
|
|
@@ -28,6 +28,8 @@ export function createIpcServer(
|
|
|
28
28
|
const server = createServer((socket) => {
|
|
29
29
|
let buffer = '';
|
|
30
30
|
|
|
31
|
+
let processingQueue = Promise.resolve();
|
|
32
|
+
|
|
31
33
|
socket.on('data', (chunk: Buffer) => {
|
|
32
34
|
buffer += chunk.toString();
|
|
33
35
|
const lines = buffer.split('\n');
|
|
@@ -35,7 +37,9 @@ export function createIpcServer(
|
|
|
35
37
|
|
|
36
38
|
for (const line of lines) {
|
|
37
39
|
if (!line.trim()) continue;
|
|
38
|
-
|
|
40
|
+
processingQueue = processingQueue.then(() =>
|
|
41
|
+
handleLine(line, handler, socket),
|
|
42
|
+
);
|
|
39
43
|
}
|
|
40
44
|
});
|
|
41
45
|
|
|
@@ -5,7 +5,10 @@ declare namespace wasm_bindgen {
|
|
|
5
5
|
/**
|
|
6
6
|
* Adds a section. `ptr`/`len` point to an encoded `AddSectionParams`.
|
|
7
7
|
*
|
|
8
|
-
* On success: writes encoded
|
|
8
|
+
* On success: writes an encoded `AddSectionResult` to the return slot and
|
|
9
|
+
* returns 0. When `params.replace` is true and a section with the same
|
|
10
|
+
* `source_key` exists, the superseded section's index entries are removed
|
|
11
|
+
* and the returned `AddSectionResult.superseded` is set to its kbid.
|
|
9
12
|
* On error: writes error message to return slot, returns -1.
|
|
10
13
|
*/
|
|
11
14
|
export function db_add_section(ptr: number, len: number): number;
|
|
@@ -42,6 +45,23 @@ declare namespace wasm_bindgen {
|
|
|
42
45
|
*/
|
|
43
46
|
export function db_check_version(ptr: number, len: number): number;
|
|
44
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Exports the database to a target directory.
|
|
50
|
+
*
|
|
51
|
+
* `ptr`/`len` point to a UTF-8 string containing the target path.
|
|
52
|
+
*
|
|
53
|
+
* Copies `sections/`, `documents/`, and `catalog.toml` to the target.
|
|
54
|
+
* Indexes are not copied because they can be rebuilt from the data.
|
|
55
|
+
*
|
|
56
|
+
* The target path may be absolute or relative. It is an external output
|
|
57
|
+
* destination, not a database-internal path, so no path validation is
|
|
58
|
+
* applied to it.
|
|
59
|
+
*
|
|
60
|
+
* On success a summary string is placed in the return slot:
|
|
61
|
+
* `path={target}\nsections_exported={N}\ndocuments_exported={M}`
|
|
62
|
+
*/
|
|
63
|
+
export function db_export(ptr: number, len: number): number;
|
|
64
|
+
|
|
45
65
|
/**
|
|
46
66
|
* Runs garbage collection. Returns 0 on success.
|
|
47
67
|
* On success a summary string is placed in the return slot.
|
|
@@ -200,6 +220,7 @@ declare interface InitOutput {
|
|
|
200
220
|
readonly db_add_to_index: (a: number, b: number) => number;
|
|
201
221
|
readonly db_check_integrity: (a: number, b: number) => number;
|
|
202
222
|
readonly db_check_version: (a: number, b: number) => number;
|
|
223
|
+
readonly db_export: (a: number, b: number) => number;
|
|
203
224
|
readonly db_gc: (a: number, b: number) => number;
|
|
204
225
|
readonly db_get_back_refs: (a: number, b: number) => number;
|
|
205
226
|
readonly db_get_forward_refs: (a: number, b: number) => number;
|
|
@@ -7,7 +7,10 @@ let wasm_bindgen = (function(exports) {
|
|
|
7
7
|
/**
|
|
8
8
|
* Adds a section. `ptr`/`len` point to an encoded `AddSectionParams`.
|
|
9
9
|
*
|
|
10
|
-
* On success: writes encoded
|
|
10
|
+
* On success: writes an encoded `AddSectionResult` to the return slot and
|
|
11
|
+
* returns 0. When `params.replace` is true and a section with the same
|
|
12
|
+
* `source_key` exists, the superseded section's index entries are removed
|
|
13
|
+
* and the returned `AddSectionResult.superseded` is set to its kbid.
|
|
11
14
|
* On error: writes error message to return slot, returns -1.
|
|
12
15
|
* @param {number} ptr
|
|
13
16
|
* @param {number} len
|
|
@@ -72,6 +75,30 @@ let wasm_bindgen = (function(exports) {
|
|
|
72
75
|
}
|
|
73
76
|
exports.db_check_version = db_check_version;
|
|
74
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Exports the database to a target directory.
|
|
80
|
+
*
|
|
81
|
+
* `ptr`/`len` point to a UTF-8 string containing the target path.
|
|
82
|
+
*
|
|
83
|
+
* Copies `sections/`, `documents/`, and `catalog.toml` to the target.
|
|
84
|
+
* Indexes are not copied because they can be rebuilt from the data.
|
|
85
|
+
*
|
|
86
|
+
* The target path may be absolute or relative. It is an external output
|
|
87
|
+
* destination, not a database-internal path, so no path validation is
|
|
88
|
+
* applied to it.
|
|
89
|
+
*
|
|
90
|
+
* On success a summary string is placed in the return slot:
|
|
91
|
+
* `path={target}\nsections_exported={N}\ndocuments_exported={M}`
|
|
92
|
+
* @param {number} ptr
|
|
93
|
+
* @param {number} len
|
|
94
|
+
* @returns {number}
|
|
95
|
+
*/
|
|
96
|
+
function db_export(ptr, len) {
|
|
97
|
+
const ret = wasm.db_export(ptr, len);
|
|
98
|
+
return ret;
|
|
99
|
+
}
|
|
100
|
+
exports.db_export = db_export;
|
|
101
|
+
|
|
75
102
|
/**
|
|
76
103
|
* Runs garbage collection. Returns 0 on success.
|
|
77
104
|
* On success a summary string is placed in the return slot.
|
|
Binary file
|
|
@@ -7,6 +7,7 @@ export const db_add_section: (a: number, b: number) => number;
|
|
|
7
7
|
export const db_add_to_index: (a: number, b: number) => number;
|
|
8
8
|
export const db_check_integrity: (a: number, b: number) => number;
|
|
9
9
|
export const db_check_version: (a: number, b: number) => number;
|
|
10
|
+
export const db_export: (a: number, b: number) => number;
|
|
10
11
|
export const db_gc: (a: number, b: number) => number;
|
|
11
12
|
export const db_get_back_refs: (a: number, b: number) => number;
|
|
12
13
|
export const db_get_forward_refs: (a: number, b: number) => number;
|