grepmind-core 0.1.0-alpha
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/LICENSE +190 -0
- package/dist/config/types.d.ts +174 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +137 -0
- package/dist/config/types.js.map +1 -0
- package/dist/git.d.ts +98 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +298 -0
- package/dist/git.js.map +1 -0
- package/dist/git.test.d.ts +7 -0
- package/dist/git.test.d.ts.map +1 -0
- package/dist/git.test.js +242 -0
- package/dist/git.test.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/branch.d.ts +121 -0
- package/dist/indexer/branch.d.ts.map +1 -0
- package/dist/indexer/branch.js +451 -0
- package/dist/indexer/branch.js.map +1 -0
- package/dist/indexer/chunker.d.ts +9 -0
- package/dist/indexer/chunker.d.ts.map +1 -0
- package/dist/indexer/chunker.js +70 -0
- package/dist/indexer/chunker.js.map +1 -0
- package/dist/indexer/chunker.test.d.ts +2 -0
- package/dist/indexer/chunker.test.d.ts.map +1 -0
- package/dist/indexer/chunker.test.js +180 -0
- package/dist/indexer/chunker.test.js.map +1 -0
- package/dist/indexer/code/branch.d.ts +155 -0
- package/dist/indexer/code/branch.d.ts.map +1 -0
- package/dist/indexer/code/branch.js +550 -0
- package/dist/indexer/code/branch.js.map +1 -0
- package/dist/indexer/code/branch.test.d.ts +7 -0
- package/dist/indexer/code/branch.test.d.ts.map +1 -0
- package/dist/indexer/code/branch.test.js +241 -0
- package/dist/indexer/code/branch.test.js.map +1 -0
- package/dist/indexer/code/chunker.d.ts +61 -0
- package/dist/indexer/code/chunker.d.ts.map +1 -0
- package/dist/indexer/code/chunker.js +311 -0
- package/dist/indexer/code/chunker.js.map +1 -0
- package/dist/indexer/code/chunker.test.d.ts +2 -0
- package/dist/indexer/code/chunker.test.d.ts.map +1 -0
- package/dist/indexer/code/chunker.test.js +552 -0
- package/dist/indexer/code/chunker.test.js.map +1 -0
- package/dist/indexer/code/fts.test.d.ts +2 -0
- package/dist/indexer/code/fts.test.d.ts.map +1 -0
- package/dist/indexer/code/fts.test.js +14 -0
- package/dist/indexer/code/fts.test.js.map +1 -0
- package/dist/indexer/code/graph/embedded.d.ts +11 -0
- package/dist/indexer/code/graph/embedded.d.ts.map +1 -0
- package/dist/indexer/code/graph/embedded.js +152 -0
- package/dist/indexer/code/graph/embedded.js.map +1 -0
- package/dist/indexer/code/graph/embedded.test.d.ts +2 -0
- package/dist/indexer/code/graph/embedded.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/embedded.test.js +105 -0
- package/dist/indexer/code/graph/embedded.test.js.map +1 -0
- package/dist/indexer/code/graph/facts.d.ts +11 -0
- package/dist/indexer/code/graph/facts.d.ts.map +1 -0
- package/dist/indexer/code/graph/facts.js +456 -0
- package/dist/indexer/code/graph/facts.js.map +1 -0
- package/dist/indexer/code/graph/facts.test.d.ts +2 -0
- package/dist/indexer/code/graph/facts.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/facts.test.js +181 -0
- package/dist/indexer/code/graph/facts.test.js.map +1 -0
- package/dist/indexer/code/graph/id.d.ts +14 -0
- package/dist/indexer/code/graph/id.d.ts.map +1 -0
- package/dist/indexer/code/graph/id.js +40 -0
- package/dist/indexer/code/graph/id.js.map +1 -0
- package/dist/indexer/code/graph/id.test.d.ts +2 -0
- package/dist/indexer/code/graph/id.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/id.test.js +86 -0
- package/dist/indexer/code/graph/id.test.js.map +1 -0
- package/dist/indexer/code/graph/index.d.ts +133 -0
- package/dist/indexer/code/graph/index.d.ts.map +1 -0
- package/dist/indexer/code/graph/index.js +1876 -0
- package/dist/indexer/code/graph/index.js.map +1 -0
- package/dist/indexer/code/graph/index.test.d.ts +2 -0
- package/dist/indexer/code/graph/index.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/index.test.js +210 -0
- package/dist/indexer/code/graph/index.test.js.map +1 -0
- package/dist/indexer/code/graph/queries.d.ts +22 -0
- package/dist/indexer/code/graph/queries.d.ts.map +1 -0
- package/dist/indexer/code/graph/queries.js +79 -0
- package/dist/indexer/code/graph/queries.js.map +1 -0
- package/dist/indexer/code/graph/queries.test.d.ts +2 -0
- package/dist/indexer/code/graph/queries.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/queries.test.js +108 -0
- package/dist/indexer/code/graph/queries.test.js.map +1 -0
- package/dist/indexer/code/graph/resolver.d.ts +136 -0
- package/dist/indexer/code/graph/resolver.d.ts.map +1 -0
- package/dist/indexer/code/graph/resolver.js +839 -0
- package/dist/indexer/code/graph/resolver.js.map +1 -0
- package/dist/indexer/code/graph/resolver.test.d.ts +2 -0
- package/dist/indexer/code/graph/resolver.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/resolver.test.js +482 -0
- package/dist/indexer/code/graph/resolver.test.js.map +1 -0
- package/dist/indexer/code/graph/semantic.d.ts +33 -0
- package/dist/indexer/code/graph/semantic.d.ts.map +1 -0
- package/dist/indexer/code/graph/semantic.js +279 -0
- package/dist/indexer/code/graph/semantic.js.map +1 -0
- package/dist/indexer/code/graph/semantic.test.d.ts +2 -0
- package/dist/indexer/code/graph/semantic.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/semantic.test.js +127 -0
- package/dist/indexer/code/graph/semantic.test.js.map +1 -0
- package/dist/indexer/code/index.d.ts +404 -0
- package/dist/indexer/code/index.d.ts.map +1 -0
- package/dist/indexer/code/index.js +2070 -0
- package/dist/indexer/code/index.js.map +1 -0
- package/dist/indexer/code/languages/bash.d.ts +14 -0
- package/dist/indexer/code/languages/bash.d.ts.map +1 -0
- package/dist/indexer/code/languages/bash.js +125 -0
- package/dist/indexer/code/languages/bash.js.map +1 -0
- package/dist/indexer/code/languages/css.d.ts +16 -0
- package/dist/indexer/code/languages/css.d.ts.map +1 -0
- package/dist/indexer/code/languages/css.js +204 -0
- package/dist/indexer/code/languages/css.js.map +1 -0
- package/dist/indexer/code/languages/generic.d.ts +61 -0
- package/dist/indexer/code/languages/generic.d.ts.map +1 -0
- package/dist/indexer/code/languages/generic.js +150 -0
- package/dist/indexer/code/languages/generic.js.map +1 -0
- package/dist/indexer/code/languages/graphql.d.ts +13 -0
- package/dist/indexer/code/languages/graphql.d.ts.map +1 -0
- package/dist/indexer/code/languages/graphql.js +180 -0
- package/dist/indexer/code/languages/graphql.js.map +1 -0
- package/dist/indexer/code/languages/html.d.ts +16 -0
- package/dist/indexer/code/languages/html.d.ts.map +1 -0
- package/dist/indexer/code/languages/html.js +138 -0
- package/dist/indexer/code/languages/html.js.map +1 -0
- package/dist/indexer/code/languages/index.d.ts +9 -0
- package/dist/indexer/code/languages/index.d.ts.map +1 -0
- package/dist/indexer/code/languages/index.js +12 -0
- package/dist/indexer/code/languages/index.js.map +1 -0
- package/dist/indexer/code/languages/json.d.ts +12 -0
- package/dist/indexer/code/languages/json.d.ts.map +1 -0
- package/dist/indexer/code/languages/json.js +66 -0
- package/dist/indexer/code/languages/json.js.map +1 -0
- package/dist/indexer/code/languages/registry.d.ts +78 -0
- package/dist/indexer/code/languages/registry.d.ts.map +1 -0
- package/dist/indexer/code/languages/registry.js +72 -0
- package/dist/indexer/code/languages/registry.js.map +1 -0
- package/dist/indexer/code/languages/typescript.d.ts +39 -0
- package/dist/indexer/code/languages/typescript.d.ts.map +1 -0
- package/dist/indexer/code/languages/typescript.js +300 -0
- package/dist/indexer/code/languages/typescript.js.map +1 -0
- package/dist/indexer/code/languages/yaml.d.ts +13 -0
- package/dist/indexer/code/languages/yaml.d.ts.map +1 -0
- package/dist/indexer/code/languages/yaml.js +90 -0
- package/dist/indexer/code/languages/yaml.js.map +1 -0
- package/dist/indexer/code/parser.d.ts +26 -0
- package/dist/indexer/code/parser.d.ts.map +1 -0
- package/dist/indexer/code/parser.js +332 -0
- package/dist/indexer/code/parser.js.map +1 -0
- package/dist/indexer/code/retry.d.ts +58 -0
- package/dist/indexer/code/retry.d.ts.map +1 -0
- package/dist/indexer/code/retry.js +192 -0
- package/dist/indexer/code/retry.js.map +1 -0
- package/dist/indexer/code/tree/builder.d.ts +30 -0
- package/dist/indexer/code/tree/builder.d.ts.map +1 -0
- package/dist/indexer/code/tree/builder.js +132 -0
- package/dist/indexer/code/tree/builder.js.map +1 -0
- package/dist/indexer/code/tree/builder.test.d.ts +2 -0
- package/dist/indexer/code/tree/builder.test.d.ts.map +1 -0
- package/dist/indexer/code/tree/builder.test.js +31 -0
- package/dist/indexer/code/tree/builder.test.js.map +1 -0
- package/dist/indexer/code/tree/cache.d.ts +22 -0
- package/dist/indexer/code/tree/cache.d.ts.map +1 -0
- package/dist/indexer/code/tree/cache.js +85 -0
- package/dist/indexer/code/tree/cache.js.map +1 -0
- package/dist/indexer/code/tree/context.d.ts +32 -0
- package/dist/indexer/code/tree/context.d.ts.map +1 -0
- package/dist/indexer/code/tree/context.js +78 -0
- package/dist/indexer/code/tree/context.js.map +1 -0
- package/dist/indexer/code/tree/embedding.d.ts +9 -0
- package/dist/indexer/code/tree/embedding.d.ts.map +1 -0
- package/dist/indexer/code/tree/embedding.js +53 -0
- package/dist/indexer/code/tree/embedding.js.map +1 -0
- package/dist/indexer/code/tree/embedding.test.d.ts +2 -0
- package/dist/indexer/code/tree/embedding.test.d.ts.map +1 -0
- package/dist/indexer/code/tree/embedding.test.js +57 -0
- package/dist/indexer/code/tree/embedding.test.js.map +1 -0
- package/dist/indexer/code/tree/id.d.ts +3 -0
- package/dist/indexer/code/tree/id.d.ts.map +1 -0
- package/dist/indexer/code/tree/id.js +8 -0
- package/dist/indexer/code/tree/id.js.map +1 -0
- package/dist/indexer/code/tree/index.d.ts +113 -0
- package/dist/indexer/code/tree/index.d.ts.map +1 -0
- package/dist/indexer/code/tree/index.js +1146 -0
- package/dist/indexer/code/tree/index.js.map +1 -0
- package/dist/indexer/code/tree/rename.d.ts +13 -0
- package/dist/indexer/code/tree/rename.d.ts.map +1 -0
- package/dist/indexer/code/tree/rename.js +46 -0
- package/dist/indexer/code/tree/rename.js.map +1 -0
- package/dist/indexer/code/tree/repomap.d.ts +29 -0
- package/dist/indexer/code/tree/repomap.d.ts.map +1 -0
- package/dist/indexer/code/tree/repomap.js +95 -0
- package/dist/indexer/code/tree/repomap.js.map +1 -0
- package/dist/indexer/code/tree/repomap.test.d.ts +2 -0
- package/dist/indexer/code/tree/repomap.test.d.ts.map +1 -0
- package/dist/indexer/code/tree/repomap.test.js +93 -0
- package/dist/indexer/code/tree/repomap.test.js.map +1 -0
- package/dist/indexer/code/tree/stats.d.ts +26 -0
- package/dist/indexer/code/tree/stats.d.ts.map +1 -0
- package/dist/indexer/code/tree/stats.js +49 -0
- package/dist/indexer/code/tree/stats.js.map +1 -0
- package/dist/indexer/code/tree/types.d.ts +186 -0
- package/dist/indexer/code/tree/types.d.ts.map +1 -0
- package/dist/indexer/code/tree/types.js +10 -0
- package/dist/indexer/code/tree/types.js.map +1 -0
- package/dist/indexer/code/wal.d.ts +144 -0
- package/dist/indexer/code/wal.d.ts.map +1 -0
- package/dist/indexer/code/wal.js +283 -0
- package/dist/indexer/code/wal.js.map +1 -0
- package/dist/indexer/embeddings.d.ts +113 -0
- package/dist/indexer/embeddings.d.ts.map +1 -0
- package/dist/indexer/embeddings.js +477 -0
- package/dist/indexer/embeddings.js.map +1 -0
- package/dist/indexer/git-sync.d.ts +117 -0
- package/dist/indexer/git-sync.d.ts.map +1 -0
- package/dist/indexer/git-sync.js +398 -0
- package/dist/indexer/git-sync.js.map +1 -0
- package/dist/indexer/index.d.ts +175 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +1096 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/mocks/mock-reranker.d.ts +12 -0
- package/dist/indexer/mocks/mock-reranker.d.ts.map +1 -0
- package/dist/indexer/mocks/mock-reranker.js +26 -0
- package/dist/indexer/mocks/mock-reranker.js.map +1 -0
- package/dist/indexer/parser.d.ts +8 -0
- package/dist/indexer/parser.d.ts.map +1 -0
- package/dist/indexer/parser.js +44 -0
- package/dist/indexer/parser.js.map +1 -0
- package/dist/indexer/parser.test.d.ts +2 -0
- package/dist/indexer/parser.test.d.ts.map +1 -0
- package/dist/indexer/parser.test.js +197 -0
- package/dist/indexer/parser.test.js.map +1 -0
- package/dist/indexer/reranking.d.ts +71 -0
- package/dist/indexer/reranking.d.ts.map +1 -0
- package/dist/indexer/reranking.integration.test.d.ts +2 -0
- package/dist/indexer/reranking.integration.test.d.ts.map +1 -0
- package/dist/indexer/reranking.integration.test.js +104 -0
- package/dist/indexer/reranking.integration.test.js.map +1 -0
- package/dist/indexer/reranking.js +256 -0
- package/dist/indexer/reranking.js.map +1 -0
- package/dist/indexer/reranking.test.d.ts +2 -0
- package/dist/indexer/reranking.test.d.ts.map +1 -0
- package/dist/indexer/reranking.test.js +130 -0
- package/dist/indexer/reranking.test.js.map +1 -0
- package/dist/indexer/wal/file-storage.d.ts +60 -0
- package/dist/indexer/wal/file-storage.d.ts.map +1 -0
- package/dist/indexer/wal/file-storage.js +277 -0
- package/dist/indexer/wal/file-storage.js.map +1 -0
- package/dist/indexer/wal/file-storage.test.d.ts +8 -0
- package/dist/indexer/wal/file-storage.test.d.ts.map +1 -0
- package/dist/indexer/wal/file-storage.test.js +444 -0
- package/dist/indexer/wal/file-storage.test.js.map +1 -0
- package/dist/indexer/wal/index.d.ts +41 -0
- package/dist/indexer/wal/index.d.ts.map +1 -0
- package/dist/indexer/wal/index.js +61 -0
- package/dist/indexer/wal/index.js.map +1 -0
- package/dist/indexer/wal/integration.test.d.ts +11 -0
- package/dist/indexer/wal/integration.test.d.ts.map +1 -0
- package/dist/indexer/wal/integration.test.js +378 -0
- package/dist/indexer/wal/integration.test.js.map +1 -0
- package/dist/indexer/wal/lancedb-storage.d.ts +72 -0
- package/dist/indexer/wal/lancedb-storage.d.ts.map +1 -0
- package/dist/indexer/wal/lancedb-storage.js +462 -0
- package/dist/indexer/wal/lancedb-storage.js.map +1 -0
- package/dist/indexer/wal/lancedb-storage.test.d.ts +8 -0
- package/dist/indexer/wal/lancedb-storage.test.d.ts.map +1 -0
- package/dist/indexer/wal/lancedb-storage.test.js +415 -0
- package/dist/indexer/wal/lancedb-storage.test.js.map +1 -0
- package/dist/indexer/wal/sync-wal.d.ts +144 -0
- package/dist/indexer/wal/sync-wal.d.ts.map +1 -0
- package/dist/indexer/wal/sync-wal.js +863 -0
- package/dist/indexer/wal/sync-wal.js.map +1 -0
- package/dist/indexer/wal/sync-wal.test.d.ts +8 -0
- package/dist/indexer/wal/sync-wal.test.d.ts.map +1 -0
- package/dist/indexer/wal/sync-wal.test.js +752 -0
- package/dist/indexer/wal/sync-wal.test.js.map +1 -0
- package/dist/indexer/wal/types.d.ts +167 -0
- package/dist/indexer/wal/types.d.ts.map +1 -0
- package/dist/indexer/wal/types.js +12 -0
- package/dist/indexer/wal/types.js.map +1 -0
- package/dist/indexer/watcher.d.ts +36 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +110 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/search/explore.d.ts +62 -0
- package/dist/search/explore.d.ts.map +1 -0
- package/dist/search/explore.js +111 -0
- package/dist/search/explore.js.map +1 -0
- package/dist/search/fts.d.ts +23 -0
- package/dist/search/fts.d.ts.map +1 -0
- package/dist/search/fts.js +64 -0
- package/dist/search/fts.js.map +1 -0
- package/dist/search/fts.test.d.ts +2 -0
- package/dist/search/fts.test.d.ts.map +1 -0
- package/dist/search/fts.test.js +27 -0
- package/dist/search/fts.test.js.map +1 -0
- package/dist/search/grep.d.ts +75 -0
- package/dist/search/grep.d.ts.map +1 -0
- package/dist/search/grep.js +96 -0
- package/dist/search/grep.js.map +1 -0
- package/dist/search/grep.test.d.ts +2 -0
- package/dist/search/grep.test.d.ts.map +1 -0
- package/dist/search/grep.test.js +178 -0
- package/dist/search/grep.test.js.map +1 -0
- package/dist/search/hybrid-grep.d.ts +43 -0
- package/dist/search/hybrid-grep.d.ts.map +1 -0
- package/dist/search/hybrid-grep.js +130 -0
- package/dist/search/hybrid-grep.js.map +1 -0
- package/dist/search/hybrid-grep.test.d.ts +2 -0
- package/dist/search/hybrid-grep.test.d.ts.map +1 -0
- package/dist/search/hybrid-grep.test.js +133 -0
- package/dist/search/hybrid-grep.test.js.map +1 -0
- package/dist/search/rg-executor.d.ts +63 -0
- package/dist/search/rg-executor.d.ts.map +1 -0
- package/dist/search/rg-executor.js +146 -0
- package/dist/search/rg-executor.js.map +1 -0
- package/dist/search/rg-executor.test.d.ts +2 -0
- package/dist/search/rg-executor.test.d.ts.map +1 -0
- package/dist/search/rg-executor.test.js +104 -0
- package/dist/search/rg-executor.test.js.map +1 -0
- package/dist/search/rg-parser/extractor.d.ts +14 -0
- package/dist/search/rg-parser/extractor.d.ts.map +1 -0
- package/dist/search/rg-parser/extractor.js +82 -0
- package/dist/search/rg-parser/extractor.js.map +1 -0
- package/dist/search/rg-parser/extractor.test.d.ts +2 -0
- package/dist/search/rg-parser/extractor.test.d.ts.map +1 -0
- package/dist/search/rg-parser/extractor.test.js +35 -0
- package/dist/search/rg-parser/extractor.test.js.map +1 -0
- package/dist/search/rg-parser/fts-builder.d.ts +7 -0
- package/dist/search/rg-parser/fts-builder.d.ts.map +1 -0
- package/dist/search/rg-parser/fts-builder.js +18 -0
- package/dist/search/rg-parser/fts-builder.js.map +1 -0
- package/dist/search/rg-parser/fts-builder.test.d.ts +2 -0
- package/dist/search/rg-parser/fts-builder.test.d.ts.map +1 -0
- package/dist/search/rg-parser/fts-builder.test.js +26 -0
- package/dist/search/rg-parser/fts-builder.test.js.map +1 -0
- package/dist/search/rg-parser/index.d.ts +36 -0
- package/dist/search/rg-parser/index.d.ts.map +1 -0
- package/dist/search/rg-parser/index.js +83 -0
- package/dist/search/rg-parser/index.js.map +1 -0
- package/dist/search/rg-parser/index.test.d.ts +2 -0
- package/dist/search/rg-parser/index.test.d.ts.map +1 -0
- package/dist/search/rg-parser/index.test.js +34 -0
- package/dist/search/rg-parser/index.test.js.map +1 -0
- package/dist/search/rg-parser/strategy.d.ts +14 -0
- package/dist/search/rg-parser/strategy.d.ts.map +1 -0
- package/dist/search/rg-parser/strategy.js +31 -0
- package/dist/search/rg-parser/strategy.js.map +1 -0
- package/dist/search/rg-parser/strategy.test.d.ts +2 -0
- package/dist/search/rg-parser/strategy.test.d.ts.map +1 -0
- package/dist/search/rg-parser/strategy.test.js +29 -0
- package/dist/search/rg-parser/strategy.test.js.map +1 -0
- package/dist/types.d.ts +345 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/vault.d.ts +84 -0
- package/dist/utils/vault.d.ts.map +1 -0
- package/dist/utils/vault.js +138 -0
- package/dist/utils/vault.js.map +1 -0
- package/dist/utils/vault.test.d.ts +2 -0
- package/dist/utils/vault.test.d.ts.map +1 -0
- package/dist/utils/vault.test.js +153 -0
- package/dist/utils/vault.test.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyncWal - Unified Write-Ahead Log for Sync Tasks
|
|
3
|
+
*
|
|
4
|
+
* PostgreSQL-inspired WAL with:
|
|
5
|
+
* - LSN (Log Sequence Number) for recovery positioning
|
|
6
|
+
* - Checkpoint + truncate for bounded WAL size
|
|
7
|
+
* - Offset-based recovery (no paths stored)
|
|
8
|
+
* - Global lock with heartbeat for stale detection
|
|
9
|
+
* - Cancellation support
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { FileWalStorage } from './file-storage.js';
|
|
13
|
+
import { DEFAULT_SYNC_WAL_CONFIG } from './types.js';
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
15
|
+
// SYNC WAL CLASS
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
+
export class SyncWal {
|
|
18
|
+
storage;
|
|
19
|
+
config;
|
|
20
|
+
// Current state
|
|
21
|
+
currentLsn = 0;
|
|
22
|
+
currentTaskId = null;
|
|
23
|
+
currentTaskType = null;
|
|
24
|
+
totalFiles = 0;
|
|
25
|
+
manifestHash = null;
|
|
26
|
+
startedAt = 0;
|
|
27
|
+
phase = 'analyzing';
|
|
28
|
+
currentFile = null;
|
|
29
|
+
// Memtable (in-memory state between checkpoints)
|
|
30
|
+
memtable = null;
|
|
31
|
+
// Checkpoint tracking
|
|
32
|
+
filesSinceCheckpoint = 0;
|
|
33
|
+
checkpointProcessedCount = 0;
|
|
34
|
+
lastCheckpointLsn = 0;
|
|
35
|
+
// Heartbeat
|
|
36
|
+
heartbeatInterval;
|
|
37
|
+
// Cancellation and pause
|
|
38
|
+
cancelRequested = new Set();
|
|
39
|
+
pauseRequested = new Set();
|
|
40
|
+
isPaused = false;
|
|
41
|
+
// Event callback
|
|
42
|
+
onProgressCallback;
|
|
43
|
+
onEmbeddingProgressCallback;
|
|
44
|
+
onSubPhaseProgressCallback;
|
|
45
|
+
constructor(storage, config) {
|
|
46
|
+
this.storage = storage;
|
|
47
|
+
this.config = { ...DEFAULT_SYNC_WAL_CONFIG, ...config };
|
|
48
|
+
}
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
50
|
+
// INITIALIZATION
|
|
51
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
52
|
+
/**
|
|
53
|
+
* Initialize SyncWal - call on server start
|
|
54
|
+
* Handles stale lock detection and recovery
|
|
55
|
+
*/
|
|
56
|
+
async initialize() {
|
|
57
|
+
// Check for stale locks
|
|
58
|
+
const lockStatus = await this.isLocked();
|
|
59
|
+
if (lockStatus.locked && lockStatus.stale && lockStatus.lock) {
|
|
60
|
+
console.warn('Detected stale lock, clearing...');
|
|
61
|
+
await this.forceClearStaleLock();
|
|
62
|
+
}
|
|
63
|
+
// Load manifest to restore LSN
|
|
64
|
+
const manifest = await this.storage.getManifest();
|
|
65
|
+
if (manifest) {
|
|
66
|
+
this.currentLsn = manifest.walLsn;
|
|
67
|
+
this.lastCheckpointLsn = manifest.lastCheckpointLsn;
|
|
68
|
+
}
|
|
69
|
+
// Check for recovery
|
|
70
|
+
return this.recover();
|
|
71
|
+
}
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
// WRITE PATH (hot path — optimized for speed)
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
75
|
+
/**
|
|
76
|
+
* Start a new sync task
|
|
77
|
+
*/
|
|
78
|
+
async startTask(type, totalFiles, manifestHash) {
|
|
79
|
+
const taskId = randomUUID();
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
// Acquire lock
|
|
82
|
+
const lock = {
|
|
83
|
+
id: taskId,
|
|
84
|
+
type,
|
|
85
|
+
pid: process.pid,
|
|
86
|
+
startedAt: now,
|
|
87
|
+
heartbeat: now,
|
|
88
|
+
};
|
|
89
|
+
const acquired = await this.acquireLock(lock);
|
|
90
|
+
if (!acquired) {
|
|
91
|
+
throw new Error(`Cannot start ${type} sync: another sync is already running`);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
// Initialize state
|
|
95
|
+
this.currentTaskId = taskId;
|
|
96
|
+
this.currentTaskType = type;
|
|
97
|
+
this.totalFiles = totalFiles;
|
|
98
|
+
this.manifestHash = manifestHash || null;
|
|
99
|
+
this.startedAt = now;
|
|
100
|
+
this.phase = 'indexing';
|
|
101
|
+
this.currentFile = null;
|
|
102
|
+
this.filesSinceCheckpoint = 0;
|
|
103
|
+
this.checkpointProcessedCount = 0;
|
|
104
|
+
// Initialize memtable
|
|
105
|
+
this.memtable = {
|
|
106
|
+
taskId,
|
|
107
|
+
processedCount: 0,
|
|
108
|
+
failedPaths: new Map(),
|
|
109
|
+
stats: { added: 0, updated: 0, deleted: 0 },
|
|
110
|
+
lastLsn: this.currentLsn,
|
|
111
|
+
};
|
|
112
|
+
// Append task:start to WAL
|
|
113
|
+
const entry = {
|
|
114
|
+
lsn: this.currentLsn++,
|
|
115
|
+
ts: now,
|
|
116
|
+
op: 'task:start',
|
|
117
|
+
taskId,
|
|
118
|
+
data: {
|
|
119
|
+
type,
|
|
120
|
+
totalFiles,
|
|
121
|
+
manifestHash,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
await this.storage.appendEntry(entry);
|
|
125
|
+
// Create/update manifest
|
|
126
|
+
const task = {
|
|
127
|
+
id: taskId,
|
|
128
|
+
type,
|
|
129
|
+
status: 'running',
|
|
130
|
+
startedAt: now,
|
|
131
|
+
updatedAt: now,
|
|
132
|
+
phase: 'indexing',
|
|
133
|
+
totalFiles,
|
|
134
|
+
processedFiles: 0,
|
|
135
|
+
added: 0,
|
|
136
|
+
updated: 0,
|
|
137
|
+
deleted: 0,
|
|
138
|
+
failedFiles: [],
|
|
139
|
+
manifestHash,
|
|
140
|
+
lastCheckpointLsn: this.lastCheckpointLsn,
|
|
141
|
+
};
|
|
142
|
+
const manifest = {
|
|
143
|
+
version: 1,
|
|
144
|
+
current: task,
|
|
145
|
+
lastCompleted: (await this.storage.getManifest())?.lastCompleted || null,
|
|
146
|
+
walLsn: this.currentLsn,
|
|
147
|
+
lastCheckpointLsn: this.lastCheckpointLsn,
|
|
148
|
+
};
|
|
149
|
+
await this.storage.writeManifest(manifest);
|
|
150
|
+
// Start heartbeat
|
|
151
|
+
this.startHeartbeat();
|
|
152
|
+
// Emit progress
|
|
153
|
+
this.emitProgress();
|
|
154
|
+
return taskId;
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
// Release lock on any error after acquisition
|
|
158
|
+
await this.storage.releaseLock(taskId);
|
|
159
|
+
this.currentTaskId = null;
|
|
160
|
+
this.currentTaskType = null;
|
|
161
|
+
this.memtable = null;
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Mark a file as successfully processed
|
|
167
|
+
*/
|
|
168
|
+
async fileProcessed(taskId, path, chunks, action = 'updated') {
|
|
169
|
+
if (taskId !== this.currentTaskId || !this.memtable) {
|
|
170
|
+
throw new Error('Task not active');
|
|
171
|
+
}
|
|
172
|
+
// Update current file
|
|
173
|
+
this.currentFile = path;
|
|
174
|
+
// 1. Append to WAL (O(1) write)
|
|
175
|
+
await this.storage.appendEntry({
|
|
176
|
+
lsn: this.currentLsn++,
|
|
177
|
+
ts: Date.now(),
|
|
178
|
+
op: 'file:done',
|
|
179
|
+
taskId,
|
|
180
|
+
data: { path, chunks },
|
|
181
|
+
});
|
|
182
|
+
// 2. Update memtable (O(1))
|
|
183
|
+
this.memtable.processedCount++;
|
|
184
|
+
this.memtable.lastLsn = this.currentLsn;
|
|
185
|
+
if (action === 'added') {
|
|
186
|
+
this.memtable.stats.added++;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
this.memtable.stats.updated++;
|
|
190
|
+
}
|
|
191
|
+
this.filesSinceCheckpoint++;
|
|
192
|
+
// 3. Maybe checkpoint (amortized O(1))
|
|
193
|
+
if (this.filesSinceCheckpoint >= this.config.checkpointInterval) {
|
|
194
|
+
await this.checkpoint();
|
|
195
|
+
}
|
|
196
|
+
// 4. Emit progress (every 10 files or on checkpoint)
|
|
197
|
+
if (this.filesSinceCheckpoint % 10 === 0) {
|
|
198
|
+
this.emitProgress();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Mark a file as failed
|
|
203
|
+
*/
|
|
204
|
+
async fileFailed(taskId, path, error, retries) {
|
|
205
|
+
if (taskId !== this.currentTaskId || !this.memtable) {
|
|
206
|
+
throw new Error('Task not active');
|
|
207
|
+
}
|
|
208
|
+
// Append to WAL
|
|
209
|
+
await this.storage.appendEntry({
|
|
210
|
+
lsn: this.currentLsn++,
|
|
211
|
+
ts: Date.now(),
|
|
212
|
+
op: 'file:fail',
|
|
213
|
+
taskId,
|
|
214
|
+
data: { path, error, retries },
|
|
215
|
+
});
|
|
216
|
+
// Update memtable
|
|
217
|
+
this.memtable.failedPaths.set(path, { error, retries });
|
|
218
|
+
this.memtable.processedCount++;
|
|
219
|
+
this.memtable.lastLsn = this.currentLsn;
|
|
220
|
+
this.filesSinceCheckpoint++;
|
|
221
|
+
// Maybe checkpoint
|
|
222
|
+
if (this.filesSinceCheckpoint >= this.config.checkpointInterval) {
|
|
223
|
+
await this.checkpoint();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Mark file as deleted
|
|
228
|
+
*/
|
|
229
|
+
async fileDeleted(taskId, path) {
|
|
230
|
+
if (taskId !== this.currentTaskId || !this.memtable) {
|
|
231
|
+
throw new Error('Task not active');
|
|
232
|
+
}
|
|
233
|
+
// Append to WAL
|
|
234
|
+
await this.storage.appendEntry({
|
|
235
|
+
lsn: this.currentLsn++,
|
|
236
|
+
ts: Date.now(),
|
|
237
|
+
op: 'file:done',
|
|
238
|
+
taskId,
|
|
239
|
+
data: { path, chunks: 0 },
|
|
240
|
+
});
|
|
241
|
+
// Update memtable
|
|
242
|
+
this.memtable.stats.deleted++;
|
|
243
|
+
this.memtable.processedCount++;
|
|
244
|
+
this.memtable.lastLsn = this.currentLsn;
|
|
245
|
+
this.filesSinceCheckpoint++;
|
|
246
|
+
// Maybe checkpoint
|
|
247
|
+
if (this.filesSinceCheckpoint >= this.config.checkpointInterval) {
|
|
248
|
+
await this.checkpoint();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Complete the sync task
|
|
253
|
+
*/
|
|
254
|
+
async completeTask(taskId) {
|
|
255
|
+
console.log(`[SyncWal.completeTask] completing task ${taskId}`);
|
|
256
|
+
if (taskId !== this.currentTaskId || !this.memtable) {
|
|
257
|
+
throw new Error('Task not active');
|
|
258
|
+
}
|
|
259
|
+
const now = Date.now();
|
|
260
|
+
// Get cumulative stats
|
|
261
|
+
const stats = this.getCumulativeStats();
|
|
262
|
+
// Append task:complete to WAL
|
|
263
|
+
await this.storage.appendEntry({
|
|
264
|
+
lsn: this.currentLsn++,
|
|
265
|
+
ts: now,
|
|
266
|
+
op: 'task:complete',
|
|
267
|
+
taskId,
|
|
268
|
+
data: { stats },
|
|
269
|
+
});
|
|
270
|
+
// Update manifest
|
|
271
|
+
const summary = {
|
|
272
|
+
id: taskId,
|
|
273
|
+
type: this.currentTaskType,
|
|
274
|
+
status: 'completed',
|
|
275
|
+
startedAt: this.startedAt,
|
|
276
|
+
completedAt: now,
|
|
277
|
+
totalFiles: this.totalFiles,
|
|
278
|
+
stats: {
|
|
279
|
+
added: stats.added,
|
|
280
|
+
updated: stats.updated,
|
|
281
|
+
deleted: stats.deleted,
|
|
282
|
+
},
|
|
283
|
+
failedCount: stats.failed.length,
|
|
284
|
+
};
|
|
285
|
+
const manifest = {
|
|
286
|
+
version: 1,
|
|
287
|
+
current: null,
|
|
288
|
+
lastCompleted: summary,
|
|
289
|
+
walLsn: this.currentLsn,
|
|
290
|
+
lastCheckpointLsn: this.lastCheckpointLsn,
|
|
291
|
+
};
|
|
292
|
+
await this.storage.writeManifest(manifest);
|
|
293
|
+
// Clear checkpoint (task is done)
|
|
294
|
+
await this.storage.writeCheckpoint({
|
|
295
|
+
version: 1,
|
|
296
|
+
lsn: this.currentLsn,
|
|
297
|
+
createdAt: now,
|
|
298
|
+
task: {
|
|
299
|
+
id: taskId,
|
|
300
|
+
type: this.currentTaskType,
|
|
301
|
+
status: 'completed',
|
|
302
|
+
startedAt: this.startedAt,
|
|
303
|
+
totalFiles: this.totalFiles,
|
|
304
|
+
processedCount: this.checkpointProcessedCount + this.memtable.processedCount,
|
|
305
|
+
manifestHash: this.manifestHash || undefined,
|
|
306
|
+
},
|
|
307
|
+
stats,
|
|
308
|
+
});
|
|
309
|
+
console.log(`[SyncWal.completeTask] checkpoint written with status=completed for task ${taskId}`);
|
|
310
|
+
// Update phase for final progress emit
|
|
311
|
+
this.phase = 'done';
|
|
312
|
+
this.emitProgress();
|
|
313
|
+
// Cleanup
|
|
314
|
+
await this.cleanup();
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Fail the sync task
|
|
318
|
+
*/
|
|
319
|
+
async failTask(taskId, error) {
|
|
320
|
+
if (taskId !== this.currentTaskId) {
|
|
321
|
+
throw new Error('Task not active');
|
|
322
|
+
}
|
|
323
|
+
const now = Date.now();
|
|
324
|
+
const stats = this.getCumulativeStats();
|
|
325
|
+
// Append task:fail to WAL
|
|
326
|
+
await this.storage.appendEntry({
|
|
327
|
+
lsn: this.currentLsn++,
|
|
328
|
+
ts: now,
|
|
329
|
+
op: 'task:fail',
|
|
330
|
+
taskId,
|
|
331
|
+
data: { stats, error },
|
|
332
|
+
});
|
|
333
|
+
// Update manifest
|
|
334
|
+
const summary = {
|
|
335
|
+
id: taskId,
|
|
336
|
+
type: this.currentTaskType,
|
|
337
|
+
status: 'failed',
|
|
338
|
+
startedAt: this.startedAt,
|
|
339
|
+
completedAt: now,
|
|
340
|
+
totalFiles: this.totalFiles,
|
|
341
|
+
stats: {
|
|
342
|
+
added: stats.added,
|
|
343
|
+
updated: stats.updated,
|
|
344
|
+
deleted: stats.deleted,
|
|
345
|
+
},
|
|
346
|
+
failedCount: stats.failed.length,
|
|
347
|
+
};
|
|
348
|
+
const manifest = {
|
|
349
|
+
version: 1,
|
|
350
|
+
current: null,
|
|
351
|
+
lastCompleted: summary,
|
|
352
|
+
walLsn: this.currentLsn,
|
|
353
|
+
lastCheckpointLsn: this.lastCheckpointLsn,
|
|
354
|
+
};
|
|
355
|
+
await this.storage.writeManifest(manifest);
|
|
356
|
+
// Cleanup
|
|
357
|
+
await this.cleanup();
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Cancel the sync task
|
|
361
|
+
*/
|
|
362
|
+
async cancelTask(taskId) {
|
|
363
|
+
if (taskId !== this.currentTaskId) {
|
|
364
|
+
throw new Error('Task not active');
|
|
365
|
+
}
|
|
366
|
+
const now = Date.now();
|
|
367
|
+
const stats = this.getCumulativeStats();
|
|
368
|
+
// Append task:cancel to WAL
|
|
369
|
+
await this.storage.appendEntry({
|
|
370
|
+
lsn: this.currentLsn++,
|
|
371
|
+
ts: now,
|
|
372
|
+
op: 'task:cancel',
|
|
373
|
+
taskId,
|
|
374
|
+
});
|
|
375
|
+
// Update manifest
|
|
376
|
+
const summary = {
|
|
377
|
+
id: taskId,
|
|
378
|
+
type: this.currentTaskType,
|
|
379
|
+
status: 'cancelled',
|
|
380
|
+
startedAt: this.startedAt,
|
|
381
|
+
completedAt: now,
|
|
382
|
+
totalFiles: this.totalFiles,
|
|
383
|
+
stats: {
|
|
384
|
+
added: stats.added,
|
|
385
|
+
updated: stats.updated,
|
|
386
|
+
deleted: stats.deleted,
|
|
387
|
+
},
|
|
388
|
+
failedCount: stats.failed.length,
|
|
389
|
+
};
|
|
390
|
+
const manifest = {
|
|
391
|
+
version: 1,
|
|
392
|
+
current: null,
|
|
393
|
+
lastCompleted: summary,
|
|
394
|
+
walLsn: this.currentLsn,
|
|
395
|
+
lastCheckpointLsn: this.lastCheckpointLsn,
|
|
396
|
+
};
|
|
397
|
+
await this.storage.writeManifest(manifest);
|
|
398
|
+
// Cleanup
|
|
399
|
+
await this.cleanup();
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Set task phase
|
|
403
|
+
*/
|
|
404
|
+
setPhase(phase) {
|
|
405
|
+
this.phase = phase;
|
|
406
|
+
this.emitProgress();
|
|
407
|
+
}
|
|
408
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
409
|
+
// CHECKPOINT (PostgreSQL-style)
|
|
410
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
411
|
+
async checkpoint() {
|
|
412
|
+
if (!this.memtable || !this.currentTaskId)
|
|
413
|
+
return;
|
|
414
|
+
const checkpointLsn = this.currentLsn;
|
|
415
|
+
const totalProcessed = this.checkpointProcessedCount + this.memtable.processedCount;
|
|
416
|
+
const now = Date.now();
|
|
417
|
+
// 1. Write checkpoint marker to WAL
|
|
418
|
+
await this.storage.appendEntry({
|
|
419
|
+
lsn: checkpointLsn,
|
|
420
|
+
ts: now,
|
|
421
|
+
op: 'checkpoint',
|
|
422
|
+
taskId: this.currentTaskId,
|
|
423
|
+
data: { processedCount: totalProcessed },
|
|
424
|
+
});
|
|
425
|
+
this.currentLsn++;
|
|
426
|
+
// 2. Get cumulative stats
|
|
427
|
+
const stats = this.getCumulativeStats();
|
|
428
|
+
// 3. Write checkpoint file
|
|
429
|
+
await this.storage.writeCheckpoint({
|
|
430
|
+
version: 1,
|
|
431
|
+
lsn: checkpointLsn,
|
|
432
|
+
createdAt: now,
|
|
433
|
+
task: {
|
|
434
|
+
id: this.currentTaskId,
|
|
435
|
+
type: this.currentTaskType,
|
|
436
|
+
status: 'running',
|
|
437
|
+
startedAt: this.startedAt,
|
|
438
|
+
totalFiles: this.totalFiles,
|
|
439
|
+
processedCount: totalProcessed,
|
|
440
|
+
manifestHash: this.manifestHash || undefined,
|
|
441
|
+
},
|
|
442
|
+
stats,
|
|
443
|
+
});
|
|
444
|
+
// 4. Truncate WAL before checkpoint LSN
|
|
445
|
+
await this.storage.truncateBeforeLsn(checkpointLsn);
|
|
446
|
+
// 5. Update manifest
|
|
447
|
+
const manifest = await this.storage.getManifest();
|
|
448
|
+
if (manifest) {
|
|
449
|
+
manifest.lastCheckpointLsn = checkpointLsn;
|
|
450
|
+
manifest.walLsn = this.currentLsn;
|
|
451
|
+
if (manifest.current) {
|
|
452
|
+
manifest.current.processedFiles = totalProcessed;
|
|
453
|
+
manifest.current.updatedAt = now;
|
|
454
|
+
manifest.current.lastCheckpointLsn = checkpointLsn;
|
|
455
|
+
}
|
|
456
|
+
await this.storage.writeManifest(manifest);
|
|
457
|
+
}
|
|
458
|
+
// 6. Update checkpoint baseline and clear memtable counts
|
|
459
|
+
this.checkpointProcessedCount = totalProcessed;
|
|
460
|
+
this.lastCheckpointLsn = checkpointLsn;
|
|
461
|
+
this.memtable.processedCount = 0;
|
|
462
|
+
this.filesSinceCheckpoint = 0;
|
|
463
|
+
// 7. Update heartbeat
|
|
464
|
+
await this.storage.updateHeartbeat(this.currentTaskId);
|
|
465
|
+
// 8. Emit progress
|
|
466
|
+
this.emitProgress();
|
|
467
|
+
}
|
|
468
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
469
|
+
// RECOVERY (checkpoint + WAL replay)
|
|
470
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
471
|
+
/**
|
|
472
|
+
* Check for incomplete task and return recovery plan
|
|
473
|
+
*/
|
|
474
|
+
async recover() {
|
|
475
|
+
// 1. Load checkpoint (O(1), small file ~1KB)
|
|
476
|
+
const checkpoint = await this.storage.getCheckpoint();
|
|
477
|
+
// Debug logging
|
|
478
|
+
if (checkpoint) {
|
|
479
|
+
console.log(`[SyncWal.recover] checkpoint found: taskId=${checkpoint.task.id}, status=${checkpoint.task.status}, type=${checkpoint.task.type}`);
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
console.log('[SyncWal.recover] no checkpoint found');
|
|
483
|
+
}
|
|
484
|
+
if (!checkpoint || checkpoint.task.status !== 'running') {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
// 2. Read WAL tail (only entries after last checkpoint)
|
|
488
|
+
const walEntries = await this.storage.getEntriesFromLsn(checkpoint.lsn);
|
|
489
|
+
// 3. Count file:done entries in WAL tail
|
|
490
|
+
let walProcessedCount = 0;
|
|
491
|
+
for (const entry of walEntries) {
|
|
492
|
+
if (entry.op === 'file:done') {
|
|
493
|
+
walProcessedCount++;
|
|
494
|
+
}
|
|
495
|
+
// Update LSN from WAL
|
|
496
|
+
if (entry.lsn >= this.currentLsn) {
|
|
497
|
+
this.currentLsn = entry.lsn + 1;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// 4. Total skip count = checkpoint.processedCount + WAL tail count
|
|
501
|
+
const skipCount = checkpoint.task.processedCount + walProcessedCount;
|
|
502
|
+
// 5. Return recovery plan
|
|
503
|
+
return {
|
|
504
|
+
taskId: checkpoint.task.id,
|
|
505
|
+
type: checkpoint.task.type,
|
|
506
|
+
totalFiles: checkpoint.task.totalFiles,
|
|
507
|
+
skipCount,
|
|
508
|
+
manifestHash: checkpoint.task.manifestHash || '',
|
|
509
|
+
stats: checkpoint.stats,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Resume a task from recovery plan
|
|
514
|
+
*/
|
|
515
|
+
async resumeTask(plan) {
|
|
516
|
+
const now = Date.now();
|
|
517
|
+
// Restore state from recovery plan
|
|
518
|
+
this.currentTaskId = plan.taskId;
|
|
519
|
+
this.currentTaskType = plan.type;
|
|
520
|
+
this.totalFiles = plan.totalFiles;
|
|
521
|
+
this.manifestHash = plan.manifestHash || null;
|
|
522
|
+
this.checkpointProcessedCount = plan.skipCount;
|
|
523
|
+
this.phase = 'indexing';
|
|
524
|
+
// Re-acquire lock
|
|
525
|
+
const lock = {
|
|
526
|
+
id: plan.taskId,
|
|
527
|
+
type: plan.type,
|
|
528
|
+
pid: process.pid,
|
|
529
|
+
startedAt: now,
|
|
530
|
+
heartbeat: now,
|
|
531
|
+
};
|
|
532
|
+
// Force acquire (we know there's no valid lock from recovery check)
|
|
533
|
+
if (this.storage instanceof FileWalStorage) {
|
|
534
|
+
await this.storage.forceClearLock();
|
|
535
|
+
}
|
|
536
|
+
const acquired = await this.storage.acquireLock(lock);
|
|
537
|
+
if (!acquired) {
|
|
538
|
+
throw new Error('Failed to re-acquire lock for recovery');
|
|
539
|
+
}
|
|
540
|
+
// Initialize memtable with restored stats
|
|
541
|
+
this.memtable = {
|
|
542
|
+
taskId: plan.taskId,
|
|
543
|
+
processedCount: 0,
|
|
544
|
+
failedPaths: new Map(plan.stats.failed.map((f) => [f.path, { error: f.error, retries: f.retries }])),
|
|
545
|
+
stats: {
|
|
546
|
+
added: plan.stats.added,
|
|
547
|
+
updated: plan.stats.updated,
|
|
548
|
+
deleted: plan.stats.deleted,
|
|
549
|
+
},
|
|
550
|
+
lastLsn: this.currentLsn,
|
|
551
|
+
};
|
|
552
|
+
// Update manifest
|
|
553
|
+
const manifest = await this.storage.getManifest();
|
|
554
|
+
if (manifest && manifest.current) {
|
|
555
|
+
manifest.current.updatedAt = now;
|
|
556
|
+
await this.storage.writeManifest(manifest);
|
|
557
|
+
}
|
|
558
|
+
// Start heartbeat
|
|
559
|
+
this.startHeartbeat();
|
|
560
|
+
}
|
|
561
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
562
|
+
// STATE QUERIES
|
|
563
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
564
|
+
async getManifest() {
|
|
565
|
+
return this.storage.getManifest();
|
|
566
|
+
}
|
|
567
|
+
async getCurrentTask() {
|
|
568
|
+
const manifest = await this.storage.getManifest();
|
|
569
|
+
return manifest?.current || null;
|
|
570
|
+
}
|
|
571
|
+
async getLastCompleted() {
|
|
572
|
+
const manifest = await this.storage.getManifest();
|
|
573
|
+
return manifest?.lastCompleted || null;
|
|
574
|
+
}
|
|
575
|
+
getProgress() {
|
|
576
|
+
if (!this.currentTaskId || !this.currentTaskType) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const stats = this.getCumulativeStats();
|
|
580
|
+
const processedFiles = this.checkpointProcessedCount + (this.memtable?.processedCount || 0);
|
|
581
|
+
const now = Date.now();
|
|
582
|
+
const elapsedMs = now - this.startedAt;
|
|
583
|
+
// Estimate remaining time
|
|
584
|
+
let estimatedRemainingMs;
|
|
585
|
+
if (processedFiles > 0) {
|
|
586
|
+
const msPerFile = elapsedMs / processedFiles;
|
|
587
|
+
const remainingFiles = this.totalFiles - processedFiles;
|
|
588
|
+
estimatedRemainingMs = Math.round(msPerFile * remainingFiles);
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
taskId: this.currentTaskId,
|
|
592
|
+
type: this.currentTaskType,
|
|
593
|
+
status: this.isPaused ? 'paused' : 'running',
|
|
594
|
+
phase: this.phase,
|
|
595
|
+
paused: this.isPaused,
|
|
596
|
+
totalFiles: this.totalFiles,
|
|
597
|
+
processedFiles,
|
|
598
|
+
currentFile: this.currentFile || undefined,
|
|
599
|
+
added: stats.added,
|
|
600
|
+
updated: stats.updated,
|
|
601
|
+
deleted: stats.deleted,
|
|
602
|
+
failedCount: stats.failed.length,
|
|
603
|
+
startedAt: this.startedAt,
|
|
604
|
+
elapsedMs,
|
|
605
|
+
estimatedRemainingMs,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
609
|
+
// LOCK MANAGEMENT
|
|
610
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
611
|
+
async acquireLock(lock) {
|
|
612
|
+
// First try to acquire directly
|
|
613
|
+
const acquired = await this.storage.acquireLock(lock);
|
|
614
|
+
if (acquired)
|
|
615
|
+
return true;
|
|
616
|
+
// Check if existing lock is stale
|
|
617
|
+
const status = await this.isLocked();
|
|
618
|
+
if (status.locked && status.stale && status.lock) {
|
|
619
|
+
// Atomic compare-and-clear: only clear if lock unchanged
|
|
620
|
+
if (this.storage instanceof FileWalStorage) {
|
|
621
|
+
const cleared = await this.storage.compareAndClearLock(status.lock);
|
|
622
|
+
if (cleared) {
|
|
623
|
+
// Retry acquire
|
|
624
|
+
return this.storage.acquireLock(lock);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
async releaseLock() {
|
|
631
|
+
if (this.currentTaskId) {
|
|
632
|
+
await this.storage.releaseLock(this.currentTaskId);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
async isLocked() {
|
|
636
|
+
const lock = await this.storage.getLock();
|
|
637
|
+
if (!lock) {
|
|
638
|
+
return { locked: false, stale: false };
|
|
639
|
+
}
|
|
640
|
+
// Check if stale
|
|
641
|
+
const now = Date.now();
|
|
642
|
+
const heartbeatAge = now - lock.heartbeat;
|
|
643
|
+
const isHeartbeatStale = heartbeatAge > this.config.staleLockTimeout;
|
|
644
|
+
const isPidDead = !FileWalStorage.isPidRunning(lock.pid);
|
|
645
|
+
const stale = isHeartbeatStale || isPidDead;
|
|
646
|
+
return { locked: true, stale, lock };
|
|
647
|
+
}
|
|
648
|
+
async forceClearStaleLock() {
|
|
649
|
+
const status = await this.isLocked();
|
|
650
|
+
if (!status.locked) {
|
|
651
|
+
return true; // No lock to clear
|
|
652
|
+
}
|
|
653
|
+
if (!status.stale) {
|
|
654
|
+
return false; // Lock is not stale, cannot force clear
|
|
655
|
+
}
|
|
656
|
+
if (this.storage instanceof FileWalStorage) {
|
|
657
|
+
await this.storage.forceClearLock();
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Clear all WAL state (lock, checkpoint, WAL entries, manifest).
|
|
664
|
+
* Use this to reset sync state when stuck in recovery loop.
|
|
665
|
+
* WARNING: This will lose any recovery information for incomplete syncs.
|
|
666
|
+
*/
|
|
667
|
+
async clear() {
|
|
668
|
+
// Stop any running task first
|
|
669
|
+
this.stopHeartbeat();
|
|
670
|
+
await this.releaseLock();
|
|
671
|
+
// Clear all storage
|
|
672
|
+
await this.storage.clear();
|
|
673
|
+
// Reset in-memory state
|
|
674
|
+
this.currentTaskId = null;
|
|
675
|
+
this.currentTaskType = null;
|
|
676
|
+
this.memtable = null;
|
|
677
|
+
this.totalFiles = 0;
|
|
678
|
+
this.manifestHash = null;
|
|
679
|
+
this.filesSinceCheckpoint = 0;
|
|
680
|
+
this.checkpointProcessedCount = 0;
|
|
681
|
+
this.currentFile = null;
|
|
682
|
+
this.phase = 'analyzing';
|
|
683
|
+
this.currentLsn = 0;
|
|
684
|
+
this.lastCheckpointLsn = 0;
|
|
685
|
+
this.startedAt = 0;
|
|
686
|
+
this.cancelRequested.clear();
|
|
687
|
+
this.pauseRequested.clear();
|
|
688
|
+
this.isPaused = false;
|
|
689
|
+
}
|
|
690
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
691
|
+
// HEARTBEAT
|
|
692
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
693
|
+
startHeartbeat() {
|
|
694
|
+
this.stopHeartbeat();
|
|
695
|
+
this.heartbeatInterval = setInterval(async () => {
|
|
696
|
+
if (this.currentTaskId) {
|
|
697
|
+
try {
|
|
698
|
+
await this.storage.updateHeartbeat(this.currentTaskId);
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
console.warn('Heartbeat update failed:', err);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}, this.config.heartbeatInterval);
|
|
705
|
+
}
|
|
706
|
+
stopHeartbeat() {
|
|
707
|
+
if (this.heartbeatInterval) {
|
|
708
|
+
clearInterval(this.heartbeatInterval);
|
|
709
|
+
this.heartbeatInterval = undefined;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
713
|
+
// CANCELLATION
|
|
714
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
715
|
+
async requestCancel(taskId) {
|
|
716
|
+
this.cancelRequested.add(taskId);
|
|
717
|
+
// If paused, also unpause so cancellation can proceed
|
|
718
|
+
this.pauseRequested.delete(taskId);
|
|
719
|
+
this.isPaused = false;
|
|
720
|
+
}
|
|
721
|
+
isCancelled(taskId) {
|
|
722
|
+
return this.cancelRequested.has(taskId);
|
|
723
|
+
}
|
|
724
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
725
|
+
// PAUSE / RESUME
|
|
726
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
727
|
+
/**
|
|
728
|
+
* Request pause for the current sync task
|
|
729
|
+
*/
|
|
730
|
+
requestPause(taskId) {
|
|
731
|
+
if (taskId === this.currentTaskId) {
|
|
732
|
+
this.pauseRequested.add(taskId);
|
|
733
|
+
this.isPaused = true;
|
|
734
|
+
this.emitProgress();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Resume a paused sync task
|
|
739
|
+
*/
|
|
740
|
+
requestResume(taskId) {
|
|
741
|
+
if (taskId === this.currentTaskId) {
|
|
742
|
+
this.pauseRequested.delete(taskId);
|
|
743
|
+
this.isPaused = false;
|
|
744
|
+
this.emitProgress();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Check if task should pause (call from indexer loop)
|
|
749
|
+
*/
|
|
750
|
+
shouldPause(taskId) {
|
|
751
|
+
return this.pauseRequested.has(taskId);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Wait while paused (call from indexer loop)
|
|
755
|
+
* Returns true if resumed, false if cancelled during pause
|
|
756
|
+
*/
|
|
757
|
+
async waitWhilePaused(taskId) {
|
|
758
|
+
while (this.pauseRequested.has(taskId)) {
|
|
759
|
+
if (this.cancelRequested.has(taskId)) {
|
|
760
|
+
return false; // Cancelled during pause
|
|
761
|
+
}
|
|
762
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
763
|
+
}
|
|
764
|
+
return true; // Resumed
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Check if currently paused
|
|
768
|
+
*/
|
|
769
|
+
isPausedNow() {
|
|
770
|
+
return this.isPaused;
|
|
771
|
+
}
|
|
772
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
773
|
+
// PROGRESS CALLBACK
|
|
774
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
775
|
+
onProgress(callback) {
|
|
776
|
+
this.onProgressCallback = callback;
|
|
777
|
+
}
|
|
778
|
+
onEmbeddingProgress(callback) {
|
|
779
|
+
this.onEmbeddingProgressCallback = callback;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Get callback for embedding progress (to pass to embedding provider)
|
|
783
|
+
*/
|
|
784
|
+
getEmbeddingProgressCallback() {
|
|
785
|
+
return this.onEmbeddingProgressCallback;
|
|
786
|
+
}
|
|
787
|
+
onSubPhaseProgress(callback) {
|
|
788
|
+
this.onSubPhaseProgressCallback = callback;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Emit sub-phase progress (for graph/tree building phases)
|
|
792
|
+
*/
|
|
793
|
+
emitSubPhaseProgress(current, total, subPhase) {
|
|
794
|
+
this.onSubPhaseProgressCallback?.(current, total, subPhase);
|
|
795
|
+
}
|
|
796
|
+
emitProgress() {
|
|
797
|
+
const progress = this.getProgress();
|
|
798
|
+
if (progress && this.onProgressCallback) {
|
|
799
|
+
this.onProgressCallback(progress);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
803
|
+
// GRACEFUL SHUTDOWN
|
|
804
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
805
|
+
async shutdown() {
|
|
806
|
+
this.stopHeartbeat();
|
|
807
|
+
if (this.currentTaskId) {
|
|
808
|
+
// Final checkpoint before shutdown
|
|
809
|
+
await this.checkpoint().catch(() => { });
|
|
810
|
+
}
|
|
811
|
+
await this.releaseLock();
|
|
812
|
+
}
|
|
813
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
814
|
+
// HELPERS
|
|
815
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
816
|
+
getCumulativeStats() {
|
|
817
|
+
const failed = [];
|
|
818
|
+
if (this.memtable) {
|
|
819
|
+
for (const [path, info] of this.memtable.failedPaths) {
|
|
820
|
+
failed.push({ path, error: info.error, retries: info.retries });
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
added: this.memtable?.stats.added || 0,
|
|
825
|
+
updated: this.memtable?.stats.updated || 0,
|
|
826
|
+
deleted: this.memtable?.stats.deleted || 0,
|
|
827
|
+
failed,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
async cleanup() {
|
|
831
|
+
this.stopHeartbeat();
|
|
832
|
+
await this.releaseLock();
|
|
833
|
+
// Clear cancel/pause requests before resetting taskId
|
|
834
|
+
if (this.currentTaskId) {
|
|
835
|
+
this.cancelRequested.delete(this.currentTaskId);
|
|
836
|
+
this.pauseRequested.delete(this.currentTaskId);
|
|
837
|
+
}
|
|
838
|
+
this.isPaused = false;
|
|
839
|
+
// Reset state
|
|
840
|
+
this.currentTaskId = null;
|
|
841
|
+
this.currentTaskType = null;
|
|
842
|
+
this.memtable = null;
|
|
843
|
+
this.totalFiles = 0;
|
|
844
|
+
this.manifestHash = null;
|
|
845
|
+
this.filesSinceCheckpoint = 0;
|
|
846
|
+
this.checkpointProcessedCount = 0;
|
|
847
|
+
this.currentFile = null;
|
|
848
|
+
this.phase = 'analyzing';
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Get current task ID (for external use)
|
|
852
|
+
*/
|
|
853
|
+
getCurrentTaskId() {
|
|
854
|
+
return this.currentTaskId;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Check if sync is currently running
|
|
858
|
+
*/
|
|
859
|
+
isRunning() {
|
|
860
|
+
return this.currentTaskId !== null;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
//# sourceMappingURL=sync-wal.js.map
|