mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
chore(deps): swap unmaintained/legacy deps per e18e module-replacements (#2447)
- npm-run-all → npm-run-all2 (dev script uses run-p) - lint-staged → nano-staged (config block + husky hook) - lodash.debounce → local src/utils/debounce.ts with cancel/flush/leading/trailing - dotenv → process.loadEnvFile in integration test bootstrap - eslint-plugin-react → @eslint-react/eslint-plugin (flat-config migration) - js-yaml → yaml (mock + jest CJS moduleNameMapper for jsdom) - crypto-js → pure-JS md5/sha256 in src/utils/hash.ts producing byte-identical digests so all on-disk cache keys (PDF, file, project, search index) remain valid across the upgrade — no migration or re-parse cost Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8239443814
commit
d6255374cb
24 changed files with 1108 additions and 1088 deletions
|
|
@ -1 +1 @@
|
||||||
npx lint-staged
|
npx nano-staged
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// __mocks__/obsidian.js
|
// __mocks__/obsidian.js
|
||||||
import yaml from "js-yaml";
|
import { parse as parseYamlString } from "yaml";
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Reason: normalizePath is used by projectPaths.ts; identity function is sufficient for tests
|
// Reason: normalizePath is used by projectPaths.ts; identity function is sufficient for tests
|
||||||
|
|
@ -32,7 +32,7 @@ module.exports = {
|
||||||
isDesktop: true,
|
isDesktop: true,
|
||||||
},
|
},
|
||||||
parseYaml: jest.fn().mockImplementation((content) => {
|
parseYaml: jest.fn().mockImplementation((content) => {
|
||||||
return yaml.load(content);
|
return parseYamlString(content);
|
||||||
}),
|
}),
|
||||||
Modal: class Modal {
|
Modal: class Modal {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||||
import reactPlugin from "eslint-plugin-react";
|
import eslintReact from "@eslint-react/eslint-plugin";
|
||||||
import reactHooks from "eslint-plugin-react-hooks";
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
import tailwind from "eslint-plugin-tailwindcss";
|
import tailwind from "eslint-plugin-tailwindcss";
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
|
|
@ -26,10 +26,10 @@ export default [
|
||||||
|
|
||||||
// React + tailwind plugins ship flat configs with no `files` filter, so
|
// React + tailwind plugins ship flat configs with no `files` filter, so
|
||||||
// they'd cascade onto package.json (which uses the JSON parser) and crash.
|
// they'd cascade onto package.json (which uses the JSON parser) and crash.
|
||||||
// Constrain them to JS/TS sources.
|
// Constrain them to JSX/TSX sources where React/JSX rules actually apply.
|
||||||
{
|
{
|
||||||
files: ["**/*.{js,jsx,mjs,cjs,ts,tsx}"],
|
files: ["**/*.{jsx,tsx}"],
|
||||||
...reactPlugin.configs.flat.recommended,
|
...eslintReact.configs.recommended,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["**/*.{js,jsx,mjs,cjs,ts,tsx}"],
|
files: ["**/*.{js,jsx,mjs,cjs,ts,tsx}"],
|
||||||
|
|
@ -53,7 +53,7 @@ export default [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: { version: "detect" },
|
"react-x": { version: "detect" },
|
||||||
tailwindcss: {
|
tailwindcss: {
|
||||||
callees: ["classnames", "clsx", "ctl", "cn", "cva"],
|
callees: ["classnames", "clsx", "ctl", "cn", "cva"],
|
||||||
config: "./tailwind.config.js",
|
config: "./tailwind.config.js",
|
||||||
|
|
@ -63,7 +63,6 @@ export default [
|
||||||
rules: {
|
rules: {
|
||||||
// Carry-over from legacy .eslintrc
|
// Carry-over from legacy .eslintrc
|
||||||
"no-prototype-builtins": "off",
|
"no-prototype-builtins": "off",
|
||||||
"react/prop-types": "off",
|
|
||||||
"tailwindcss/classnames-order": "error",
|
"tailwindcss/classnames-order": "error",
|
||||||
"tailwindcss/enforces-negative-arbitrary-values": "error",
|
"tailwindcss/enforces-negative-arbitrary-values": "error",
|
||||||
"tailwindcss/enforces-shorthand": "error",
|
"tailwindcss/enforces-shorthand": "error",
|
||||||
|
|
@ -268,15 +267,7 @@ export default [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
presets: ["native", "microutilities", "preferred"],
|
presets: ["native", "microutilities", "preferred"],
|
||||||
allowed: [
|
allowed: [],
|
||||||
"crypto-js",
|
|
||||||
"dotenv",
|
|
||||||
"js-yaml",
|
|
||||||
"lodash.debounce",
|
|
||||||
"eslint-plugin-react",
|
|
||||||
"lint-staged",
|
|
||||||
"npm-run-all",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ module.exports = {
|
||||||
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
||||||
"^@/(.*)$": "<rootDir>/src/$1",
|
"^@/(.*)$": "<rootDir>/src/$1",
|
||||||
"^obsidian$": "<rootDir>/__mocks__/obsidian.js",
|
"^obsidian$": "<rootDir>/__mocks__/obsidian.js",
|
||||||
|
// The yaml package's "exports" field defaults to a browser ESM entry under
|
||||||
|
// jsdom; Jest can't parse ESM without extra config, so point at the CJS
|
||||||
|
// build it ships under dist/.
|
||||||
|
"^yaml$": "<rootDir>/node_modules/yaml/dist/index.js",
|
||||||
},
|
},
|
||||||
testRegex: ".*\\.test\\.(jsx?|tsx?)$",
|
testRegex: ".*\\.test\\.(jsx?|tsx?)$",
|
||||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
||||||
|
|
|
||||||
1596
package-lock.json
generated
1596
package-lock.json
generated
File diff suppressed because it is too large
Load diff
19
package.json
19
package.json
|
|
@ -4,7 +4,7 @@
|
||||||
"description": "Your AI Copilot: Chat with Your Second Brain, Learn Faster, Work Smarter.",
|
"description": "Your AI Copilot: Chat with Your Second Brain, Learn Faster, Work Smarter.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm-run-all --parallel dev:*",
|
"dev": "run-p dev:*",
|
||||||
"dev:tailwind": "npx tailwindcss -i src/styles/tailwind.css -o styles.css --watch",
|
"dev:tailwind": "npx tailwindcss -i src/styles/tailwind.css -o styles.css --watch",
|
||||||
"dev:esbuild": "node esbuild.config.mjs",
|
"dev:esbuild": "node esbuild.config.mjs",
|
||||||
"build": "npm run build:tailwind && npm run build:esbuild || exit 1",
|
"build": "npm run build:tailwind && npm run build:esbuild || exit 1",
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Logan Yang",
|
"author": "Logan Yang",
|
||||||
"lint-staged": {
|
"nano-staged": {
|
||||||
"*.{js,jsx,ts,tsx,json,css,md}": [
|
"*.{js,jsx,ts,tsx,json,css,md}": [
|
||||||
"prettier --write"
|
"prettier --write"
|
||||||
],
|
],
|
||||||
|
|
@ -32,18 +32,17 @@
|
||||||
},
|
},
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint-react/eslint-plugin": "^1.38.4",
|
||||||
"@jest/globals": "^29.7.0",
|
"@jest/globals": "^29.7.0",
|
||||||
"@langchain/ollama": "^1.2.2",
|
"@langchain/ollama": "^1.2.2",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@types/crypto-js": "^4.1.1",
|
|
||||||
"@types/diff": "^7.0.1",
|
"@types/diff": "^7.0.1",
|
||||||
"@types/events": "^3.0.0",
|
"@types/events": "^3.0.0",
|
||||||
"@types/jest": "^29.5.11",
|
"@types/jest": "^29.5.11",
|
||||||
"@types/koa": "^2.13.7",
|
"@types/koa": "^2.13.7",
|
||||||
"@types/koa__cors": "^4.0.0",
|
"@types/koa__cors": "^4.0.0",
|
||||||
"@types/lodash.debounce": "^4.0.9",
|
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^16.11.6",
|
||||||
"@types/react": "^18.0.33",
|
"@types/react": "^18.0.33",
|
||||||
|
|
@ -53,27 +52,25 @@
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
||||||
"@typescript-eslint/parser": "^8.19.1",
|
"@typescript-eslint/parser": "^8.19.1",
|
||||||
"dotenv": "^16.4.5",
|
|
||||||
"electron": "^27.3.2",
|
"electron": "^27.3.2",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||||
"eslint-plugin-react": "^7.37.3",
|
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||||
"husky": "^9.1.5",
|
"husky": "^9.1.5",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.5.0",
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
"js-yaml": "^4.1.0",
|
"nano-staged": "^0.8.0",
|
||||||
"lint-staged": "^15.2.9",
|
"npm-run-all2": "^7.0.2",
|
||||||
"npm-run-all": "^4.1.5",
|
|
||||||
"obsidian": "^1.2.5",
|
"obsidian": "^1.2.5",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"tailwindcss": "^3.4.15",
|
"tailwindcss": "^3.4.15",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"tslib": "2.4.0",
|
"tslib": "2.4.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"web-streams-polyfill": "^3.3.2"
|
"web-streams-polyfill": "^3.3.2",
|
||||||
|
"yaml": "^2.6.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
|
|
@ -121,7 +118,6 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"codemirror-companion-extension": "^0.0.11",
|
"codemirror-companion-extension": "^0.0.11",
|
||||||
"crypto-js": "^4.1.1",
|
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"esbuild-plugin-svg": "^0.1.0",
|
"esbuild-plugin-svg": "^0.1.0",
|
||||||
"eventsource-parser": "^1.0.0",
|
"eventsource-parser": "^1.0.0",
|
||||||
|
|
@ -131,7 +127,6 @@
|
||||||
"koa-proxies": "^0.12.3",
|
"koa-proxies": "^0.12.3",
|
||||||
"langchain": "^1.2.28",
|
"langchain": "^1.2.28",
|
||||||
"lexical": "^0.34.0",
|
"lexical": "^0.34.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"minisearch": "^7.2.0",
|
"minisearch": "^7.2.0",
|
||||||
|
|
|
||||||
4
src/cache/fileCache.ts
vendored
4
src/cache/fileCache.ts
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
import { logError, logInfo } from "@/logger";
|
import { logError, logInfo } from "@/logger";
|
||||||
import { MD5 } from "crypto-js";
|
import { md5 } from "@/utils/hash";
|
||||||
import { TFile } from "obsidian";
|
import { TFile } from "obsidian";
|
||||||
|
|
||||||
export interface FileCacheEntry<T> {
|
export interface FileCacheEntry<T> {
|
||||||
|
|
@ -33,7 +33,7 @@ export class FileCache<T> {
|
||||||
getCacheKey(file: TFile, additionalContext?: string): string {
|
getCacheKey(file: TFile, additionalContext?: string): string {
|
||||||
// Use file path, size and mtime for a unique but efficient cache key
|
// Use file path, size and mtime for a unique but efficient cache key
|
||||||
const metadata = `${file.path}:${file.stat.size}:${file.stat.mtime}${additionalContext ? `:${additionalContext}` : ""}`;
|
const metadata = `${file.path}:${file.stat.size}:${file.stat.mtime}${additionalContext ? `:${additionalContext}` : ""}`;
|
||||||
return MD5(metadata).toString();
|
return md5(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCachePath(cacheKey: string): string {
|
private getCachePath(cacheKey: string): string {
|
||||||
|
|
|
||||||
4
src/cache/pdfCache.ts
vendored
4
src/cache/pdfCache.ts
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
import { Pdf4llmResponse } from "@/LLMProviders/brevilabsClient";
|
import { Pdf4llmResponse } from "@/LLMProviders/brevilabsClient";
|
||||||
import { logError, logInfo } from "@/logger";
|
import { logError, logInfo } from "@/logger";
|
||||||
import { MD5 } from "crypto-js";
|
import { md5 } from "@/utils/hash";
|
||||||
import { TFile } from "obsidian";
|
import { TFile } from "obsidian";
|
||||||
|
|
||||||
export class PDFCache {
|
export class PDFCache {
|
||||||
|
|
@ -26,7 +26,7 @@ export class PDFCache {
|
||||||
private getCacheKey(file: TFile): string {
|
private getCacheKey(file: TFile): string {
|
||||||
// Use file path, size and mtime for a unique but efficient cache key
|
// Use file path, size and mtime for a unique but efficient cache key
|
||||||
const metadata = `${file.path}:${file.stat.size}:${file.stat.mtime}`;
|
const metadata = `${file.path}:${file.stat.size}:${file.stat.mtime}`;
|
||||||
const key = MD5(metadata).toString();
|
const key = md5(metadata);
|
||||||
logInfo("Generated cache key for PDF:", { path: file.path, key });
|
logInfo("Generated cache key for PDF:", { path: file.path, key });
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6
src/cache/projectContextCache.ts
vendored
6
src/cache/projectContextCache.ts
vendored
|
|
@ -3,9 +3,9 @@ import { FileCache } from "@/cache/fileCache";
|
||||||
import { logError, logInfo, logWarn } from "@/logger";
|
import { logError, logInfo, logWarn } from "@/logger";
|
||||||
import { getMatchingPatterns, shouldIndexFile } from "@/search/searchUtils";
|
import { getMatchingPatterns, shouldIndexFile } from "@/search/searchUtils";
|
||||||
import { getCachedProjects } from "@/projects/state";
|
import { getCachedProjects } from "@/projects/state";
|
||||||
import { MD5 } from "crypto-js";
|
import { md5 } from "@/utils/hash";
|
||||||
import { TAbstractFile, TFile, Vault } from "obsidian";
|
import { TAbstractFile, TFile, Vault } from "obsidian";
|
||||||
import debounce from "lodash.debounce";
|
import { debounce } from "@/utils/debounce";
|
||||||
import { Mutex } from "async-mutex";
|
import { Mutex } from "async-mutex";
|
||||||
|
|
||||||
export interface ContextCache {
|
export interface ContextCache {
|
||||||
|
|
@ -173,7 +173,7 @@ export class ProjectContextCache {
|
||||||
|
|
||||||
private getCacheKey(project: ProjectConfig): string {
|
private getCacheKey(project: ProjectConfig): string {
|
||||||
// Use project ID as cache key
|
// Use project ID as cache key
|
||||||
return MD5(project.id).toString();
|
return md5(project.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCachePath(cacheKey: string): string {
|
private getCachePath(cacheKey: string): string {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
} from "@/commands/customCommandUtils";
|
} from "@/commands/customCommandUtils";
|
||||||
import { Editor, Plugin, TFile, Vault } from "obsidian";
|
import { Editor, Plugin, TFile, Vault } from "obsidian";
|
||||||
import { CustomCommandChatModal } from "@/commands/CustomCommandChatModal";
|
import { CustomCommandChatModal } from "@/commands/CustomCommandChatModal";
|
||||||
import debounce from "lodash.debounce";
|
import { debounce } from "@/utils/debounce";
|
||||||
import { CustomCommand } from "@/commands/type";
|
import { CustomCommand } from "@/commands/type";
|
||||||
import {
|
import {
|
||||||
deleteCachedCommand,
|
deleteCachedCommand,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { SettingSwitch } from "@/components/ui/setting-switch";
|
||||||
import { ModelParametersEditor } from "@/components/ui/ModelParametersEditor";
|
import { ModelParametersEditor } from "@/components/ui/ModelParametersEditor";
|
||||||
import { CustomModel, getModelKey } from "@/aiParams";
|
import { CustomModel, getModelKey } from "@/aiParams";
|
||||||
import { getSettings, updateSetting } from "@/settings/model";
|
import { getSettings, updateSetting } from "@/settings/model";
|
||||||
import debounce from "lodash.debounce";
|
import { debounce } from "@/utils/debounce";
|
||||||
import {
|
import {
|
||||||
getDefaultSystemPromptTitle,
|
getDefaultSystemPromptTitle,
|
||||||
getDisableBuiltinSystemPrompt,
|
getDisableBuiltinSystemPrompt,
|
||||||
|
|
@ -83,8 +83,8 @@ export function ChatSettingsPopover() {
|
||||||
}
|
}
|
||||||
}, [showConfirmation]);
|
}, [showConfirmation]);
|
||||||
|
|
||||||
// Debounced save function - must be defined before handleOpenChange
|
// Debounced save function - must be defined before handleOpenChange.
|
||||||
// Reference: Command module uses lodash.debounce which has cancel() method
|
// Uses debounce.cancel() / debounce.flush() to manage pending writes on unmount.
|
||||||
const debouncedSave = useMemo(
|
const debouncedSave = useMemo(
|
||||||
() =>
|
() =>
|
||||||
debounce((updatedModel: CustomModel) => {
|
debounce((updatedModel: CustomModel) => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { SHA256 } from "crypto-js";
|
import { sha256 } from "@/utils/hash";
|
||||||
import { logInfo } from "@/logger";
|
import { logInfo } from "@/logger";
|
||||||
import {
|
import {
|
||||||
PROMPT_LAYER_LABELS,
|
PROMPT_LAYER_LABELS,
|
||||||
|
|
@ -125,11 +125,11 @@ export class PromptContextEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute a SHA-256 hash for the supplied value.
|
* Compute a SHA-256 hash for the supplied value. Pure-JS so it works on
|
||||||
* Uses crypto-js for mobile compatibility (no Node.js crypto).
|
* Obsidian mobile (no node:crypto, no sync Web Crypto).
|
||||||
*/
|
*/
|
||||||
private hash(value: string): string {
|
private hash(value: string): string {
|
||||||
return SHA256(value || "").toString();
|
return sha256(value || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { DEFAULT_SYSTEM_PROMPT, COMPOSER_OUTPUT_INSTRUCTIONS } from "../constants";
|
import { DEFAULT_SYSTEM_PROMPT, COMPOSER_OUTPUT_INSTRUCTIONS } from "../constants";
|
||||||
import * as dotenv from "dotenv";
|
|
||||||
import { jest } from "@jest/globals";
|
import { jest } from "@jest/globals";
|
||||||
import {
|
import {
|
||||||
GoogleGenerativeAI,
|
GoogleGenerativeAI,
|
||||||
|
|
@ -8,8 +7,13 @@ import {
|
||||||
GenerativeModel,
|
GenerativeModel,
|
||||||
} from "@google/generative-ai";
|
} from "@google/generative-ai";
|
||||||
|
|
||||||
// Load environment variables from .env.test
|
// Load environment variables from .env.test using Node's native env-file loader.
|
||||||
dotenv.config({ path: ".env.test" });
|
// `loadEnvFile` is available on Node >= 20.12 / 21.7 but @types/node here predates the typing.
|
||||||
|
try {
|
||||||
|
(process as unknown as { loadEnvFile: (path: string) => void }).loadEnvFile(".env.test");
|
||||||
|
} catch {
|
||||||
|
// .env.test is optional — skip silently when absent
|
||||||
|
}
|
||||||
|
|
||||||
// Test data
|
// Test data
|
||||||
const atom_note = `Atoms are the basic particles of the chemical elements. An atom consists of a nucleus of protons and generally neutrons, surrounded by an electromagnetically bound swarm of electrons. The chemical elements are distinguished from each other by the number of protons that are in their atoms. For example, any atom that contains 11 protons is sodium, and any atom that contains 29 protons is copper. Atoms with the same number of protons but a different number of neutrons are called isotopes of the same element.
|
const atom_note = `Atoms are the basic particles of the chemical elements. An atom consists of a nucleus of protons and generally neutrons, surrounded by an electromagnetically bound swarm of electrons. The chemical elements are distinguished from each other by the number of protons that are in their atoms. For example, any atom that contains 11 protons is sodium, and any atom that contains 29 protons is copper. Atoms with the same number of protons but a different number of neutrons are called isotopes of the same element.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
import { loadAllProjects } from "@/projects/projectUtils";
|
import { loadAllProjects } from "@/projects/projectUtils";
|
||||||
import { PROJECT_CONFIG_FILE_NAME, PROJECTS_UNSUPPORTED_FOLDER_NAME } from "@/projects/constants";
|
import { PROJECT_CONFIG_FILE_NAME, PROJECTS_UNSUPPORTED_FOLDER_NAME } from "@/projects/constants";
|
||||||
import { getSettings, subscribeToSettingsChange } from "@/settings/model";
|
import { getSettings, subscribeToSettingsChange } from "@/settings/model";
|
||||||
import debounce from "lodash.debounce";
|
import { debounce } from "@/utils/debounce";
|
||||||
import { App, Notice, TAbstractFile, Vault } from "obsidian";
|
import { App, Notice, TAbstractFile, Vault } from "obsidian";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { areEmbeddingModelsSame } from "@/utils";
|
||||||
import { Embeddings } from "@langchain/core/embeddings";
|
import { Embeddings } from "@langchain/core/embeddings";
|
||||||
import { create, insert, Orama, remove, removeMultiple, search } from "@orama/orama";
|
import { create, insert, Orama, remove, removeMultiple, search } from "@orama/orama";
|
||||||
import { Mutex } from "async-mutex";
|
import { Mutex } from "async-mutex";
|
||||||
import { MD5 } from "crypto-js";
|
import { md5 } from "@/utils/hash";
|
||||||
import { App, Notice, Platform } from "obsidian";
|
import { App, Notice, Platform } from "obsidian";
|
||||||
import { ChunkedStorage } from "./chunkedStorage";
|
import { ChunkedStorage } from "./chunkedStorage";
|
||||||
import { getMatchingPatterns, getVectorLength, shouldIndexFile } from "./searchUtils";
|
import { getMatchingPatterns, getVectorLength, shouldIndexFile } from "./searchUtils";
|
||||||
|
|
@ -260,7 +260,7 @@ export class DBOperations {
|
||||||
|
|
||||||
private getVaultIdentifier(): string {
|
private getVaultIdentifier(): string {
|
||||||
const vaultName = this.app.vault.getName();
|
const vaultName = this.app.vault.getName();
|
||||||
return MD5(vaultName).toString();
|
return md5(vaultName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public markUnsavedChanges() {
|
public markUnsavedChanges() {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { RateLimiter } from "@/rateLimiter";
|
||||||
import { ChunkManager, getSharedChunkManager } from "@/search/v3/chunks";
|
import { ChunkManager, getSharedChunkManager } from "@/search/v3/chunks";
|
||||||
import { getSettings, subscribeToSettingsChange } from "@/settings/model";
|
import { getSettings, subscribeToSettingsChange } from "@/settings/model";
|
||||||
import { formatDateTime } from "@/utils";
|
import { formatDateTime } from "@/utils";
|
||||||
import { MD5 } from "crypto-js";
|
import { md5 } from "@/utils/hash";
|
||||||
import { App, Notice, TFile } from "obsidian";
|
import { App, Notice, TFile } from "obsidian";
|
||||||
import type {
|
import type {
|
||||||
SemanticIndexBackend,
|
SemanticIndexBackend,
|
||||||
|
|
@ -379,7 +379,7 @@ export class IndexOperations {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDocHash(sourceDocument: string): string {
|
private getDocHash(sourceDocument: string): string {
|
||||||
return MD5(sourceDocument).toString();
|
return md5(sourceDocument);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getFilesToIndex(overwrite?: boolean): Promise<TFile[]> {
|
private async getFilesToIndex(overwrite?: boolean): Promise<TFile[]> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { TFile, TFolder, TAbstractFile } from "obsidian";
|
import { TFile, TFolder, TAbstractFile } from "obsidian";
|
||||||
import debounce from "lodash.debounce";
|
import { debounce } from "@/utils/debounce";
|
||||||
import { settingsStore } from "@/settings/model";
|
import { settingsStore } from "@/settings/model";
|
||||||
import { getTagsFromNote, isAllowedFileForNoteContext } from "@/utils";
|
import { getTagsFromNote, isAllowedFileForNoteContext } from "@/utils";
|
||||||
import { logInfo } from "@/logger";
|
import { logInfo } from "@/logger";
|
||||||
|
|
@ -161,7 +161,7 @@ export class VaultDataManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounced notes refresh - batches rapid file operations using lodash.debounce
|
* Debounced notes refresh - batches rapid file operations via debounce
|
||||||
*/
|
*/
|
||||||
private debouncedRefreshNotes = debounce(() => this.refreshNotes(), VAULT_DEBOUNCE_DELAY, {
|
private debouncedRefreshNotes = debounce(() => this.refreshNotes(), VAULT_DEBOUNCE_DELAY, {
|
||||||
leading: true,
|
leading: true,
|
||||||
|
|
@ -169,7 +169,7 @@ export class VaultDataManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounced folders refresh - batches rapid file operations using lodash.debounce
|
* Debounced folders refresh - batches rapid file operations via debounce
|
||||||
*/
|
*/
|
||||||
private debouncedRefreshFolders = debounce(() => this.refreshFolders(), VAULT_DEBOUNCE_DELAY, {
|
private debouncedRefreshFolders = debounce(() => this.refreshFolders(), VAULT_DEBOUNCE_DELAY, {
|
||||||
leading: true,
|
leading: true,
|
||||||
|
|
@ -177,7 +177,7 @@ export class VaultDataManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounced frontmatter tags refresh - batches rapid file operations using lodash.debounce
|
* Debounced frontmatter tags refresh - batches rapid file operations via debounce
|
||||||
*/
|
*/
|
||||||
private debouncedRefreshTagsFrontmatter = debounce(
|
private debouncedRefreshTagsFrontmatter = debounce(
|
||||||
() => this.refreshTagsFrontmatter(),
|
() => this.refreshTagsFrontmatter(),
|
||||||
|
|
@ -189,7 +189,7 @@ export class VaultDataManager {
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounced all tags refresh - batches rapid file operations using lodash.debounce
|
* Debounced all tags refresh - batches rapid file operations via debounce
|
||||||
*/
|
*/
|
||||||
private debouncedRefreshTagsAll = debounce(() => this.refreshTagsAll(), VAULT_DEBOUNCE_DELAY, {
|
private debouncedRefreshTagsAll = debounce(() => this.refreshTagsAll(), VAULT_DEBOUNCE_DELAY, {
|
||||||
leading: true,
|
leading: true,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
} from "@/system-prompts/state";
|
} from "@/system-prompts/state";
|
||||||
import { getSettings, subscribeToSettingsChange, updateSetting } from "@/settings/model";
|
import { getSettings, subscribeToSettingsChange, updateSetting } from "@/settings/model";
|
||||||
import { SystemPromptManager } from "@/system-prompts/systemPromptManager";
|
import { SystemPromptManager } from "@/system-prompts/systemPromptManager";
|
||||||
import debounce from "lodash.debounce";
|
import { debounce } from "@/utils/debounce";
|
||||||
import { logError, logInfo } from "@/logger";
|
import { logError, logInfo } from "@/logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,9 @@ jest.mock("@/search/searchUtils", () => ({
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock crypto-js
|
// Mock md5 so the cache key in tests is predictable
|
||||||
jest.mock("crypto-js", () => ({
|
jest.mock("@/utils/hash", () => ({
|
||||||
MD5: jest.fn().mockImplementation((str) => ({
|
md5: jest.fn().mockImplementation((str: string) => `mocked-hash-${str}`),
|
||||||
toString: () => `mocked-hash-${str}`,
|
|
||||||
})),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock plusUtils
|
// Mock plusUtils
|
||||||
|
|
|
||||||
11
src/utils.ts
11
src/utils.ts
|
|
@ -1112,16 +1112,7 @@ export async function insertIntoEditor(message: string, replace: boolean = false
|
||||||
finalizeInsertion();
|
finalizeInsertion();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debounce<T extends (...args: any[]) => void>(
|
export { debounce } from "@/utils/debounce";
|
||||||
func: T,
|
|
||||||
wait: number
|
|
||||||
): (...args: Parameters<T>) => void {
|
|
||||||
let timeout: number;
|
|
||||||
return (...args: Parameters<T>) => {
|
|
||||||
window.clearTimeout(timeout);
|
|
||||||
timeout = window.setTimeout(() => func(...args), wait);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two semantic version strings.
|
* Compare two semantic version strings.
|
||||||
|
|
|
||||||
86
src/utils/debounce.test.ts
Normal file
86
src/utils/debounce.test.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { debounce } from "./debounce";
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
describe("debounce", () => {
|
||||||
|
it("invokes once after the wait period with the latest args (trailing default)", () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
const d = debounce(fn, 100);
|
||||||
|
|
||||||
|
d("a");
|
||||||
|
d("b");
|
||||||
|
d("c");
|
||||||
|
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
jest.advanceTimersByTime(100);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fn).toHaveBeenCalledWith("c");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports leading-only mode", () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
const d = debounce(fn, 100, { leading: true, trailing: false });
|
||||||
|
|
||||||
|
d("a");
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fn).toHaveBeenCalledWith("a");
|
||||||
|
|
||||||
|
d("b");
|
||||||
|
d("c");
|
||||||
|
jest.advanceTimersByTime(100);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fires both edges when leading and trailing are true and there are multiple calls", () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
const d = debounce(fn, 100, { leading: true, trailing: true });
|
||||||
|
|
||||||
|
d("a");
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fn).toHaveBeenLastCalledWith("a");
|
||||||
|
|
||||||
|
d("b");
|
||||||
|
jest.advanceTimersByTime(100);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(2);
|
||||||
|
expect(fn).toHaveBeenLastCalledWith("b");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not fire trailing if leading already covered the single call", () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
const d = debounce(fn, 100, { leading: true, trailing: true });
|
||||||
|
|
||||||
|
d("only");
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
jest.advanceTimersByTime(100);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cancel prevents pending trailing invocation", () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
const d = debounce(fn, 100);
|
||||||
|
|
||||||
|
d("a");
|
||||||
|
d.cancel();
|
||||||
|
jest.advanceTimersByTime(100);
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flush invokes the pending call synchronously", () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
const d = debounce(fn, 100);
|
||||||
|
|
||||||
|
d("a");
|
||||||
|
d.flush();
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fn).toHaveBeenCalledWith("a");
|
||||||
|
jest.advanceTimersByTime(100);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flush is a no-op when nothing is pending", () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
const d = debounce(fn, 100);
|
||||||
|
expect(d.flush()).toBeUndefined();
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
89
src/utils/debounce.ts
Normal file
89
src/utils/debounce.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* Returned by {@link debounce}. Calling it schedules the underlying function,
|
||||||
|
* with `cancel` and `flush` controls modeled after `lodash.debounce`.
|
||||||
|
*/
|
||||||
|
export interface DebouncedFunction<T extends (...args: any[]) => any> {
|
||||||
|
(...args: Parameters<T>): ReturnType<T> | undefined;
|
||||||
|
cancel(): void;
|
||||||
|
flush(): ReturnType<T> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DebounceOptions {
|
||||||
|
leading?: boolean;
|
||||||
|
trailing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delay-invoking `func` until `wait` ms have elapsed since the last call.
|
||||||
|
* Supports the `leading` / `trailing` options and `cancel` / `flush` methods
|
||||||
|
* used by the `lodash.debounce` API the codebase relied on previously.
|
||||||
|
*/
|
||||||
|
export function debounce<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number,
|
||||||
|
options: DebounceOptions = {}
|
||||||
|
): DebouncedFunction<T> {
|
||||||
|
const leading = options.leading ?? false;
|
||||||
|
const trailing = options.trailing ?? true;
|
||||||
|
|
||||||
|
let timeoutId: number | undefined;
|
||||||
|
let lastArgs: Parameters<T> | undefined;
|
||||||
|
let lastResult: ReturnType<T> | undefined;
|
||||||
|
let hasPendingTrailingCall = false;
|
||||||
|
|
||||||
|
const invoke = (): ReturnType<T> | undefined => {
|
||||||
|
const args = lastArgs;
|
||||||
|
lastArgs = undefined;
|
||||||
|
hasPendingTrailingCall = false;
|
||||||
|
if (!args) return lastResult;
|
||||||
|
lastResult = func(...args) as ReturnType<T>;
|
||||||
|
return lastResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTimer = () => {
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
timeoutId = undefined;
|
||||||
|
if (trailing && hasPendingTrailingCall) {
|
||||||
|
invoke();
|
||||||
|
}
|
||||||
|
}, wait);
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounced = ((...args: Parameters<T>) => {
|
||||||
|
lastArgs = args;
|
||||||
|
hasPendingTrailingCall = true;
|
||||||
|
|
||||||
|
const isFirstCallInWindow = timeoutId === undefined;
|
||||||
|
if (timeoutId !== undefined) {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFirstCallInWindow && leading) {
|
||||||
|
invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
startTimer();
|
||||||
|
return lastResult;
|
||||||
|
}) as DebouncedFunction<T>;
|
||||||
|
|
||||||
|
debounced.cancel = () => {
|
||||||
|
if (timeoutId !== undefined) {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
timeoutId = undefined;
|
||||||
|
}
|
||||||
|
lastArgs = undefined;
|
||||||
|
hasPendingTrailingCall = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
debounced.flush = () => {
|
||||||
|
if (timeoutId === undefined) return lastResult;
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
timeoutId = undefined;
|
||||||
|
if (hasPendingTrailingCall) {
|
||||||
|
return invoke();
|
||||||
|
}
|
||||||
|
return lastResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
return debounced;
|
||||||
|
}
|
||||||
80
src/utils/hash.test.ts
Normal file
80
src/utils/hash.test.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { md5, sha256 } from "./hash";
|
||||||
|
|
||||||
|
// Test vectors below are standard RFC 1321 / FIPS 180-4 reference values and
|
||||||
|
// match what `crypto-js`'s `MD5(str).toString()` / `SHA256(str).toString()`
|
||||||
|
// produced for the same UTF-8-encoded inputs. Preserving these byte-for-byte
|
||||||
|
// keeps existing on-disk cache keys (PDF cache, file cache, project cache,
|
||||||
|
// search index doc hashes) valid across the crypto-js removal.
|
||||||
|
|
||||||
|
describe("md5", () => {
|
||||||
|
it.each([
|
||||||
|
["", "d41d8cd98f00b204e9800998ecf8427e"],
|
||||||
|
["a", "0cc175b9c0f1b6a831c399e269772661"],
|
||||||
|
["abc", "900150983cd24fb0d6963f7d28e17f72"],
|
||||||
|
["message digest", "f96b697d7cb7938d525a2f31aaf161d0"],
|
||||||
|
["abcdefghijklmnopqrstuvwxyz", "c3fcd3d76192e4007dfb496cca67e13b"],
|
||||||
|
[
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
|
||||||
|
"d174ab98d277d9f5a5611c2c9f419d9f",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"12345678901234567890123456789012345678901234567890123456789012345678901234567890",
|
||||||
|
"57edf4a22be3c955ac49da2e2107b67a",
|
||||||
|
],
|
||||||
|
["The quick brown fox jumps over the lazy dog", "9e107d9d372bb6826bd81d3542a419d6"],
|
||||||
|
])("md5(%j) = %s", (input, expected) => {
|
||||||
|
expect(md5(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles UTF-8 multibyte input", () => {
|
||||||
|
// crypto-js encodes JS strings as UTF-8 before hashing; "café" → 63 61 66 c3 a9.
|
||||||
|
expect(md5("café")).toBe("07117fe4a1ebd544965dc19573183da2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles inputs across block boundaries (55, 56, 64, 119, 120 bytes)", () => {
|
||||||
|
expect(md5("a".repeat(55))).toBe("ef1772b6dff9a122358552954ad0df65");
|
||||||
|
expect(md5("a".repeat(56))).toBe("3b0c8ac703f828b04c6c197006d17218");
|
||||||
|
expect(md5("a".repeat(64))).toBe("014842d480b571495a4a0363793f7367");
|
||||||
|
expect(md5("a".repeat(119))).toBe("8a7bd0732ed6a28ce75f6dabc90e1613");
|
||||||
|
expect(md5("a".repeat(120))).toBe("5f61c0ccad4cac44c75ff505e1f1e537");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sha256", () => {
|
||||||
|
it.each([
|
||||||
|
["", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
|
||||||
|
["abc", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"],
|
||||||
|
[
|
||||||
|
"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
|
||||||
|
"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"The quick brown fox jumps over the lazy dog",
|
||||||
|
"d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592",
|
||||||
|
],
|
||||||
|
])("sha256(%j) = %s", (input, expected) => {
|
||||||
|
expect(sha256(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles UTF-8 multibyte input", () => {
|
||||||
|
expect(sha256("café")).toBe("850f7dc43910ff890f8879c0ed26fe697c93a067ad93a7d50f466a7028a9bf4e");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles inputs across block boundaries (55, 56, 64, 119, 120 bytes)", () => {
|
||||||
|
expect(sha256("a".repeat(55))).toBe(
|
||||||
|
"9f4390f8d30c2dd92ec9f095b65e2b9ae9b0a925a5258e241c9f1e910f734318"
|
||||||
|
);
|
||||||
|
expect(sha256("a".repeat(56))).toBe(
|
||||||
|
"b35439a4ac6f0948b6d6f9e3c6af0f5f590ce20f1bde7090ef7970686ec6738a"
|
||||||
|
);
|
||||||
|
expect(sha256("a".repeat(64))).toBe(
|
||||||
|
"ffe054fe7ae0cb6dc65c3af9b61d5209f439851db43d0ba5997337df154668eb"
|
||||||
|
);
|
||||||
|
expect(sha256("a".repeat(119))).toBe(
|
||||||
|
"31eba51c313a5c08226adf18d4a359cfdfd8d2e816b13f4af952f7ea6584dcfb"
|
||||||
|
);
|
||||||
|
expect(sha256("a".repeat(120))).toBe(
|
||||||
|
"2f3d335432c70b580af0e8e1b3674a7c020d683aa5f73aaaedfdc55af904c21c"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
214
src/utils/hash.ts
Normal file
214
src/utils/hash.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
/**
|
||||||
|
* Pure-JS MD5 and SHA-256 producing lowercase-hex digests that match what
|
||||||
|
* `crypto-js`'s `MD5(str).toString()` / `SHA256(str).toString()` produced.
|
||||||
|
*
|
||||||
|
* Used for cache keys and content fingerprints — not for security. Pure JS so
|
||||||
|
* it stays available across Obsidian's desktop (Electron) and mobile (WebView)
|
||||||
|
* runtimes, where `node:crypto` is unavailable and `crypto.subtle.digest` is
|
||||||
|
* async-only.
|
||||||
|
*
|
||||||
|
* Inputs are interpreted as UTF-8 (matching crypto-js's default `Utf8.parse`).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HEX = "0123456789abcdef";
|
||||||
|
|
||||||
|
function bytesToHex(bytes: Uint8Array): string {
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
out += HEX[bytes[i] >>> 4] + HEX[bytes[i] & 0xf];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- MD5 (RFC 1321) ----------
|
||||||
|
|
||||||
|
const MD5_T = new Int32Array([
|
||||||
|
-0x28955b88, -0x173848aa, 0x242070db, -0x3e423112, -0x0a83f051, 0x4787c62a, -0x57cfb9ed,
|
||||||
|
-0x02b96aff, 0x698098d8, -0x74bb0851, -0x0000a44f, -0x76a32842, 0x6b901122, -0x02678e6d,
|
||||||
|
-0x5986bc72, 0x49b40821, -0x09e1da9e, -0x3fbf4cc0, 0x265e5a51, -0x16493856, -0x29d0efa3,
|
||||||
|
0x02441453, -0x275e197f, -0x182c0438, 0x21e1cde6, -0x3cc8f82a, -0x0b2af279, 0x455a14ed,
|
||||||
|
-0x561c16fb, -0x03105c08, 0x676f02d9, -0x72d5b376, -0x0005c6be, -0x788e097f, 0x6d9d6122,
|
||||||
|
-0x021ac7f4, -0x5b4115bc, 0x4bdecfa9, -0x0944b4a0, -0x41404390, 0x289b7ec6, -0x155ed806,
|
||||||
|
-0x2b10cf7b, 0x04881d05, -0x262b2fc7, -0x1924661b, 0x1fa27cf8, -0x3b53a99b, -0x0bd6ddbc,
|
||||||
|
0x432aff97, -0x546bdc59, -0x036c5fc7, 0x655b59c3, -0x70f3336e, -0x00100b83, -0x7a7ba22f,
|
||||||
|
0x6fa87e4f, -0x01d31920, -0x5cfebcec, 0x4e0811a1, -0x08ac817e, -0x42c50dcb, 0x2ad7d2bb,
|
||||||
|
-0x14792c6f,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const MD5_S = [
|
||||||
|
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14,
|
||||||
|
20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6,
|
||||||
|
10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
|
||||||
|
];
|
||||||
|
|
||||||
|
function rotl32(x: number, n: number): number {
|
||||||
|
return (x << n) | (x >>> (32 - n));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function md5(input: string): string {
|
||||||
|
const msg = new TextEncoder().encode(input);
|
||||||
|
const len = msg.length;
|
||||||
|
// Pad to a multiple of 64 with at least 9 trailing bytes (0x80 + 8-byte length).
|
||||||
|
const paddedLen = (((len + 8) >>> 6) + 1) << 6;
|
||||||
|
const padded = new Uint8Array(paddedLen);
|
||||||
|
padded.set(msg);
|
||||||
|
padded[len] = 0x80;
|
||||||
|
const dv = new DataView(padded.buffer);
|
||||||
|
// Length in bits, little-endian, 64-bit. JS numbers handle len * 8 up to ~2^50.
|
||||||
|
const bitLenLow = (len * 8) >>> 0;
|
||||||
|
const bitLenHigh = Math.floor(len / 0x20000000);
|
||||||
|
dv.setUint32(paddedLen - 8, bitLenLow, true);
|
||||||
|
dv.setUint32(paddedLen - 4, bitLenHigh, true);
|
||||||
|
|
||||||
|
let a0 = 0x67452301 | 0;
|
||||||
|
let b0 = 0xefcdab89 | 0;
|
||||||
|
let c0 = 0x98badcfe | 0;
|
||||||
|
let d0 = 0x10325476 | 0;
|
||||||
|
|
||||||
|
const M = new Int32Array(16);
|
||||||
|
for (let off = 0; off < paddedLen; off += 64) {
|
||||||
|
for (let j = 0; j < 16; j++) M[j] = dv.getInt32(off + j * 4, true);
|
||||||
|
|
||||||
|
let a = a0;
|
||||||
|
let b = b0;
|
||||||
|
let c = c0;
|
||||||
|
let d = d0;
|
||||||
|
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
let f: number;
|
||||||
|
let g: number;
|
||||||
|
if (i < 16) {
|
||||||
|
f = (b & c) | (~b & d);
|
||||||
|
g = i;
|
||||||
|
} else if (i < 32) {
|
||||||
|
f = (d & b) | (~d & c);
|
||||||
|
g = (5 * i + 1) & 0xf;
|
||||||
|
} else if (i < 48) {
|
||||||
|
f = b ^ c ^ d;
|
||||||
|
g = (3 * i + 5) & 0xf;
|
||||||
|
} else {
|
||||||
|
f = c ^ (b | ~d);
|
||||||
|
g = (7 * i) & 0xf;
|
||||||
|
}
|
||||||
|
const tmp = d;
|
||||||
|
d = c;
|
||||||
|
c = b;
|
||||||
|
b = (b + rotl32((a + f + MD5_T[i] + M[g]) | 0, MD5_S[i])) | 0;
|
||||||
|
a = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
a0 = (a0 + a) | 0;
|
||||||
|
b0 = (b0 + b) | 0;
|
||||||
|
c0 = (c0 + c) | 0;
|
||||||
|
d0 = (d0 + d) | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MD5 digest: state words in little-endian byte order.
|
||||||
|
const out = new Uint8Array(16);
|
||||||
|
const outDv = new DataView(out.buffer);
|
||||||
|
outDv.setInt32(0, a0, true);
|
||||||
|
outDv.setInt32(4, b0, true);
|
||||||
|
outDv.setInt32(8, c0, true);
|
||||||
|
outDv.setInt32(12, d0, true);
|
||||||
|
return bytesToHex(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- SHA-256 (FIPS 180-4) ----------
|
||||||
|
|
||||||
|
const SHA256_K = new Uint32Array([
|
||||||
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||||
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||||
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||||
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||||
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||||
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||||
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||||
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function rotr32(x: number, n: number): number {
|
||||||
|
return (x >>> n) | (x << (32 - n));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sha256(input: string): string {
|
||||||
|
const msg = new TextEncoder().encode(input);
|
||||||
|
const len = msg.length;
|
||||||
|
const paddedLen = (((len + 8) >>> 6) + 1) << 6;
|
||||||
|
const padded = new Uint8Array(paddedLen);
|
||||||
|
padded.set(msg);
|
||||||
|
padded[len] = 0x80;
|
||||||
|
const dv = new DataView(padded.buffer);
|
||||||
|
// Length in bits, big-endian, 64-bit. JS numbers handle len * 8 up to ~2^50.
|
||||||
|
const bitLenLow = (len * 8) >>> 0;
|
||||||
|
const bitLenHigh = Math.floor(len / 0x20000000);
|
||||||
|
dv.setUint32(paddedLen - 8, bitLenHigh, false);
|
||||||
|
dv.setUint32(paddedLen - 4, bitLenLow, false);
|
||||||
|
|
||||||
|
let h0 = 0x6a09e667 | 0;
|
||||||
|
let h1 = 0xbb67ae85 | 0;
|
||||||
|
let h2 = 0x3c6ef372 | 0;
|
||||||
|
let h3 = 0xa54ff53a | 0;
|
||||||
|
let h4 = 0x510e527f | 0;
|
||||||
|
let h5 = 0x9b05688c | 0;
|
||||||
|
let h6 = 0x1f83d9ab | 0;
|
||||||
|
let h7 = 0x5be0cd19 | 0;
|
||||||
|
|
||||||
|
const W = new Int32Array(64);
|
||||||
|
for (let off = 0; off < paddedLen; off += 64) {
|
||||||
|
for (let i = 0; i < 16; i++) W[i] = dv.getInt32(off + i * 4, false);
|
||||||
|
for (let i = 16; i < 64; i++) {
|
||||||
|
const w15 = W[i - 15];
|
||||||
|
const w2 = W[i - 2];
|
||||||
|
const s0 = rotr32(w15, 7) ^ rotr32(w15, 18) ^ (w15 >>> 3);
|
||||||
|
const s1 = rotr32(w2, 17) ^ rotr32(w2, 19) ^ (w2 >>> 10);
|
||||||
|
W[i] = (W[i - 16] + s0 + W[i - 7] + s1) | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let a = h0;
|
||||||
|
let b = h1;
|
||||||
|
let c = h2;
|
||||||
|
let d = h3;
|
||||||
|
let e = h4;
|
||||||
|
let f = h5;
|
||||||
|
let g = h6;
|
||||||
|
let h = h7;
|
||||||
|
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
const S1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25);
|
||||||
|
const ch = (e & f) ^ (~e & g);
|
||||||
|
const t1 = (h + S1 + ch + SHA256_K[i] + W[i]) | 0;
|
||||||
|
const S0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22);
|
||||||
|
const maj = (a & b) ^ (a & c) ^ (b & c);
|
||||||
|
const t2 = (S0 + maj) | 0;
|
||||||
|
h = g;
|
||||||
|
g = f;
|
||||||
|
f = e;
|
||||||
|
e = (d + t1) | 0;
|
||||||
|
d = c;
|
||||||
|
c = b;
|
||||||
|
b = a;
|
||||||
|
a = (t1 + t2) | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h0 = (h0 + a) | 0;
|
||||||
|
h1 = (h1 + b) | 0;
|
||||||
|
h2 = (h2 + c) | 0;
|
||||||
|
h3 = (h3 + d) | 0;
|
||||||
|
h4 = (h4 + e) | 0;
|
||||||
|
h5 = (h5 + f) | 0;
|
||||||
|
h6 = (h6 + g) | 0;
|
||||||
|
h7 = (h7 + h) | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = new Uint8Array(32);
|
||||||
|
const outDv = new DataView(out.buffer);
|
||||||
|
outDv.setInt32(0, h0, false);
|
||||||
|
outDv.setInt32(4, h1, false);
|
||||||
|
outDv.setInt32(8, h2, false);
|
||||||
|
outDv.setInt32(12, h3, false);
|
||||||
|
outDv.setInt32(16, h4, false);
|
||||||
|
outDv.setInt32(20, h5, false);
|
||||||
|
outDv.setInt32(24, h6, false);
|
||||||
|
outDv.setInt32(28, h7, false);
|
||||||
|
return bytesToHex(out);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue