@64mb/freelens-power 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +4 -4
  2. package/out/main/common/resource-search.js +61 -0
  3. package/out/main/common/resource-search.js.map +1 -0
  4. package/out/main/common/store/monitoring-preferences-store.js +10 -0
  5. package/out/main/common/store/monitoring-preferences-store.js.map +1 -1
  6. package/out/main/index.js +2 -0
  7. package/out/main/index.js.map +1 -1
  8. package/out/main/resource-search-database.js +209 -0
  9. package/out/main/resource-search-database.js.map +1 -0
  10. package/out/main/resource-search-worker.js +56 -0
  11. package/out/main/resource-search-worker.js.map +1 -0
  12. package/out/main/resource-search.js +155 -0
  13. package/out/main/resource-search.js.map +1 -0
  14. package/out/renderer/appearance.js +1 -1
  15. package/out/renderer/appearance.js.map +1 -1
  16. package/out/renderer/details/resource-health-details.js +8 -4
  17. package/out/renderer/details/resource-health-details.js.map +1 -1
  18. package/out/renderer/diagnostics-state.js +7 -5
  19. package/out/renderer/diagnostics-state.js.map +1 -1
  20. package/out/renderer/diagnostics.css.js +2 -2
  21. package/out/renderer/diagnostics.css.js.map +1 -1
  22. package/out/renderer/diagnostics.js +127 -51
  23. package/out/renderer/diagnostics.js.map +1 -1
  24. package/out/renderer/gateway-relationships-client.js +107 -0
  25. package/out/renderer/gateway-relationships-client.js.map +1 -0
  26. package/out/renderer/index.js +2 -2
  27. package/out/renderer/index.js.map +1 -1
  28. package/out/renderer/monitoring.js +15 -0
  29. package/out/renderer/monitoring.js.map +1 -1
  30. package/out/renderer/preferences/monitoring-preferences.js +11 -1
  31. package/out/renderer/preferences/monitoring-preferences.js.map +1 -1
  32. package/out/renderer/preferences/preferences.js +16 -6
  33. package/out/renderer/preferences/preferences.js.map +1 -1
  34. package/out/renderer/resource-relationships.css.js +1 -1
  35. package/out/renderer/resource-relationships.css.js.map +1 -1
  36. package/out/renderer/resource-relationships.js +71 -18
  37. package/out/renderer/resource-relationships.js.map +1 -1
  38. package/out/renderer/resource-search-client.js +116 -0
  39. package/out/renderer/resource-search-client.js.map +1 -0
  40. package/out/renderer/resource-search-discovery.js +79 -0
  41. package/out/renderer/resource-search-discovery.js.map +1 -0
  42. package/out/renderer/resource-search-model.js +2 -1
  43. package/out/renderer/resource-search-model.js.map +1 -1
  44. package/out/renderer/resource-search-routing.js +78 -0
  45. package/out/renderer/resource-search-routing.js.map +1 -0
  46. package/out/renderer/resource-search-sync.js +199 -0
  47. package/out/renderer/resource-search-sync.js.map +1 -0
  48. package/out/renderer/resource-search.js +279 -54
  49. package/out/renderer/resource-search.js.map +1 -1
  50. package/out/renderer/src/common/gateway-relationships.js +122 -0
  51. package/out/renderer/src/common/gateway-relationships.js.map +1 -0
  52. package/out/renderer/src/common/resource-relationships.js +8 -0
  53. package/out/renderer/src/common/resource-relationships.js.map +1 -1
  54. package/out/renderer/src/common/resource-search.js +6 -0
  55. package/out/renderer/src/common/resource-search.js.map +1 -0
  56. package/out/renderer/src/common/store/monitoring-preferences-store.js +10 -0
  57. package/out/renderer/src/common/store/monitoring-preferences-store.js.map +1 -1
  58. package/package.json +6 -2
package/README.md CHANGED
@@ -11,9 +11,9 @@ Power tools and personal dashboards for Freelens. Built for Freelens 1.x and tes
11
11
  - **macOS Dock icon:** optionally follows your accent, with a separate toggle to restore the original
12
12
  - **Power Dashboard:** reorderable XS/S/M/L/XL website cards in a responsive grid, cached pages, loading skeletons, interactive element selection, expansion, pinned floating windows, shared website sessions, per-card 10-second refresh including pinned windows, and optional background refresh
13
13
  - **Monitoring:** optional CPU and memory trends, allocation rings, pinned tooltips, and node/pod charts using metrics-server and Kubernetes APIs
14
- - **Resource diagnostics:** CPU/memory consumers, grouped warning timelines, Pending evidence, Service/HPA checks, operational resource and namespace summaries, scoped AI handoff, and incident-summary copying
15
- - **Resource relationships:** a focused Service graph connects Ingress, selected Pods, published endpoints, owners and node references, with resource links and an optional menu switch
16
- - **Fast resource search:** double Shift opens an indexed cross-kind search over names, namespaces, labels and safe status fields, with name-part and typo matching, keyboard navigation, resource links and explicit AI attachment; inventory is bounded and excludes Secrets, ConfigMaps and custom resources
14
+ - **Resource diagnostics:** CPU/memory consumers, grouped warning timelines, Pending and Not ready triage, Service/HPA checks, operational resource and namespace summaries, scoped AI handoff, and incident-summary copying
15
+ - **Resource relationships:** a focused Service graph connects Ingress and installed Gateway API routes/listeners to selected Pods, published endpoints, owners and node references, with acceptance and cross-namespace reference evidence, resource links and saved upstream visibility
16
+ - **Fast resource search:** double Shift searches all discoverable resource types, including Secrets, ConfigMaps and custom resources, through a persistent local metadata index with gentle background refresh, type and namespace filters, name-part and typo matching, resource links and explicit AI attachment; search and background indexing can be disabled together in Monitoring settings
17
17
  - **Local metric history:** opt-in SQLite storage, up to seven days within a 100 MiB budget, with aggregated ranges and explicit collection gaps
18
18
  - **AI assistant:** native Codex or Claude CLI, per-cluster conversations, read-only API discovery and resource summaries, model/effort/speed selection, editable prompts, quick-start commands, Markdown, cancellable replies, context preview, optional terminal attachments, resource links, resizable pinned chat, and a switch to disable all AI features
19
19
  - **Read-only UI:** hides common mutation actions without changing Kubernetes permissions
@@ -56,7 +56,7 @@ Use `pnpm dev` to rebuild on changes, then reinstall the package.
56
56
 
57
57
  ## CI and releases
58
58
 
59
- Pull requests and pushes to `main` run lint, type checking, unit tests, and a build.
59
+ Pull requests and pushes to `main` run independent lint, type checking, unit tests, build and real Freelens UI checks against a disposable kind cluster. Scheduled checks add a light/narrow profile, restart and index refresh scenarios. See [UI testing](e2e/README.md) for fixtures, screenshots and coverage limits.
60
60
 
61
61
  Run **Make Release** in GitHub Actions to create a version bump pull request. Merging it publishes the package to npm and the installable `.tgz` archive in [GitHub Releases](https://github.com/64mb/freelens-power/releases). Use **Package Release** with the merged pull request number to retry a failed release.
62
62
 
@@ -0,0 +1,61 @@
1
+ //#region src/common/resource-search.ts
2
+ var RESOURCE_SEARCH_CHANNEL = "freelens-power:resource-search";
3
+ var identifier = (value, maximum = 253) => typeof value === "string" && value.length > 0 && value.length <= maximum && /^[a-zA-Z0-9_.:-]+$/.test(value);
4
+ var objectName = (value) => typeof value === "string" && value.length > 0 && value.length <= 1024 && value !== "." && value !== ".." && !/[/%\x00-\x1f\x7f]/.test(value);
5
+ function validSearchPartition(value) {
6
+ const request = value;
7
+ return !!request && identifier(request.clusterId, 128) && identifier(request.contextId, 256);
8
+ }
9
+ function projectSearchDocuments(input) {
10
+ if (!Array.isArray(input) || input.length > 500) throw new Error("Invalid search batch");
11
+ return input.map((document) => {
12
+ if (!document || !identifier(document.kind) || !objectName(document.name) || !identifier(document.version) || !identifier(document.resource) || !(document.group === "" || identifier(document.group)) || !(document.namespace === void 0 || identifier(document.namespace)) || !(document.uid === void 0 || identifier(document.uid, 128))) throw new Error("Invalid search identity");
13
+ const path = `${document.group ? `/apis/${encodeURIComponent(document.group)}` : "/api"}/${encodeURIComponent(document.version)}/${document.namespace ? `namespaces/${encodeURIComponent(document.namespace)}/` : ""}${encodeURIComponent(document.resource)}/${encodeURIComponent(document.name)}`;
14
+ if (document.path !== path) throw new Error("Invalid search path");
15
+ const labels = Object.fromEntries(Object.entries(document.labels ?? {}).slice(0, 64).filter(([key, value]) => key.length <= 253 && typeof value === "string" && value.length <= 256));
16
+ const fields = (/* @__PURE__ */ new Set([
17
+ "/pods",
18
+ "/nodes",
19
+ "/services",
20
+ "/namespaces",
21
+ "apps/deployments",
22
+ "apps/statefulsets",
23
+ "apps/daemonsets",
24
+ "apps/replicasets",
25
+ "batch/jobs",
26
+ "batch/cronjobs",
27
+ "networking.k8s.io/ingresses",
28
+ "autoscaling/horizontalpodautoscalers"
29
+ ])).has(`${document.group}/${document.resource}`) ? (document.fields ?? []).slice(0, 32).flatMap((field) => field && typeof field.label === "string" && typeof field.text === "string" ? [{
30
+ label: field.label.slice(0, 64),
31
+ text: field.text.slice(0, 512)
32
+ }] : []) : [];
33
+ let remaining = 4096;
34
+ return {
35
+ kind: document.kind,
36
+ name: document.name,
37
+ namespace: document.namespace,
38
+ labels,
39
+ path,
40
+ group: document.group,
41
+ version: document.version,
42
+ resource: document.resource,
43
+ uid: document.uid,
44
+ aliases: Array.isArray(document.aliases) ? document.aliases.filter((alias) => typeof alias === "string" && /^[a-zA-Z0-9_.:-]{1,128}$/.test(alias)).slice(0, 32) : [],
45
+ fields: fields.flatMap((field) => {
46
+ const text = field.text.slice(0, Math.max(0, remaining));
47
+ remaining -= text.length;
48
+ return text ? [{
49
+ label: field.label,
50
+ text
51
+ }] : [];
52
+ })
53
+ };
54
+ });
55
+ }
56
+ //#endregion
57
+ exports.RESOURCE_SEARCH_CHANNEL = RESOURCE_SEARCH_CHANNEL;
58
+ exports.projectSearchDocuments = projectSearchDocuments;
59
+ exports.validSearchPartition = validSearchPartition;
60
+
61
+ //# sourceMappingURL=resource-search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-search.js","names":[],"sources":["../../../src/common/resource-search.ts"],"sourcesContent":["export const RESOURCE_SEARCH_CHANNEL = \"freelens-power:resource-search\";\nexport interface SearchDocument {\n kind: string;\n name: string;\n namespace?: string;\n labels: Record<string, string>;\n path: string;\n group: string;\n version: string;\n resource: string;\n uid?: string;\n aliases?: string[];\n fields?: { label: string; text: string }[];\n}\nexport interface SearchQueryResult {\n documents: SearchDocument[];\n total: number;\n hasMore: boolean;\n}\nexport interface SearchIndexStatus {\n count: number;\n bytes: number;\n available: boolean;\n}\nexport type SearchIndexRequest = { clusterId: string; contextId: string } & (\n | { operation: \"begin\"; scope: string; scanId: string }\n | { operation: \"complete\"; scope: string; scanId: string }\n | { operation: \"append\"; scope: string; scanId: string; documents: SearchDocument[] }\n | { operation: \"invalidate\"; scope: string }\n | { operation: \"reconcile\"; resources: string[] }\n | { operation: \"permit\" }\n | { operation: \"query\"; query: string; limit?: number; kind?: string; namespace?: string }\n | { operation: \"status\" }\n | { operation: \"clear\" }\n);\n\nconst identifier = (value: unknown, maximum = 253): value is string =>\n typeof value === \"string\" && value.length > 0 && value.length <= maximum && /^[a-zA-Z0-9_.:-]+$/.test(value);\nconst objectName = (value: unknown): value is string =>\n typeof value === \"string\" &&\n value.length > 0 &&\n value.length <= 1024 &&\n value !== \".\" &&\n value !== \"..\" &&\n !/[/%\\x00-\\x1f\\x7f]/.test(value);\n\nexport function validSearchPartition(value: unknown): value is { clusterId: string; contextId: string } {\n const request = value as SearchIndexRequest;\n return !!request && identifier(request.clusterId, 128) && identifier(request.contextId, 256);\n}\n\nexport function projectSearchDocuments(input: unknown): SearchDocument[] {\n if (!Array.isArray(input) || input.length > 500) throw new Error(\"Invalid search batch\");\n return input.map((document: SearchDocument) => {\n if (\n !document ||\n !identifier(document.kind) ||\n !objectName(document.name) ||\n !identifier(document.version) ||\n !identifier(document.resource) ||\n !(document.group === \"\" || identifier(document.group)) ||\n !(document.namespace === undefined || identifier(document.namespace)) ||\n !(document.uid === undefined || identifier(document.uid, 128))\n )\n throw new Error(\"Invalid search identity\");\n const path = `${document.group ? `/apis/${encodeURIComponent(document.group)}` : \"/api\"}/${encodeURIComponent(document.version)}/${document.namespace ? `namespaces/${encodeURIComponent(document.namespace)}/` : \"\"}${encodeURIComponent(document.resource)}/${encodeURIComponent(document.name)}`;\n if (document.path !== path) throw new Error(\"Invalid search path\");\n const labels = Object.fromEntries(\n Object.entries(document.labels ?? {})\n .slice(0, 64)\n .filter(([key, value]) => key.length <= 253 && typeof value === \"string\" && value.length <= 256),\n );\n const reviewedCollections = new Set([\n \"/pods\",\n \"/nodes\",\n \"/services\",\n \"/namespaces\",\n \"apps/deployments\",\n \"apps/statefulsets\",\n \"apps/daemonsets\",\n \"apps/replicasets\",\n \"batch/jobs\",\n \"batch/cronjobs\",\n \"networking.k8s.io/ingresses\",\n \"autoscaling/horizontalpodautoscalers\",\n ]);\n const fields = reviewedCollections.has(`${document.group}/${document.resource}`)\n ? (document.fields ?? [])\n .slice(0, 32)\n .flatMap((field) =>\n field && typeof field.label === \"string\" && typeof field.text === \"string\"\n ? [{ label: field.label.slice(0, 64), text: field.text.slice(0, 512) }]\n : [],\n )\n : [];\n let remaining = 4096;\n return {\n kind: document.kind,\n name: document.name,\n namespace: document.namespace,\n labels,\n path,\n group: document.group,\n version: document.version,\n resource: document.resource,\n uid: document.uid,\n aliases: Array.isArray(document.aliases)\n ? document.aliases\n .filter((alias) => typeof alias === \"string\" && /^[a-zA-Z0-9_.:-]{1,128}$/.test(alias))\n .slice(0, 32)\n : [],\n fields: fields.flatMap((field) => {\n const text = field.text.slice(0, Math.max(0, remaining));\n remaining -= text.length;\n return text ? [{ label: field.label, text }] : [];\n }),\n };\n });\n}\n"],"mappings":";AAAA,IAAa,0BAA0B;AAoCvC,IAAM,cAAc,OAAgB,UAAU,QAC5C,OAAO,UAAU,YAAY,MAAM,SAAS,KAAK,MAAM,UAAU,WAAW,qBAAqB,KAAK,KAAK;AAC7G,IAAM,cAAc,UAClB,OAAO,UAAU,YACjB,MAAM,SAAS,KACf,MAAM,UAAU,QAChB,UAAU,OACV,UAAU,QACV,CAAC,oBAAoB,KAAK,KAAK;AAEjC,SAAgB,qBAAqB,OAAmE;CACtG,MAAM,UAAU;CAChB,OAAO,CAAC,CAAC,WAAW,WAAW,QAAQ,WAAW,GAAG,KAAK,WAAW,QAAQ,WAAW,GAAG;AAC7F;AAEA,SAAgB,uBAAuB,OAAkC;CACvE,IAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,SAAS,KAAK,MAAM,IAAI,MAAM,sBAAsB;CACvF,OAAO,MAAM,KAAK,aAA6B;EAC7C,IACE,CAAC,YACD,CAAC,WAAW,SAAS,IAAI,KACzB,CAAC,WAAW,SAAS,IAAI,KACzB,CAAC,WAAW,SAAS,OAAO,KAC5B,CAAC,WAAW,SAAS,QAAQ,KAC7B,EAAE,SAAS,UAAU,MAAM,WAAW,SAAS,KAAK,MACpD,EAAE,SAAS,cAAc,KAAA,KAAa,WAAW,SAAS,SAAS,MACnE,EAAE,SAAS,QAAQ,KAAA,KAAa,WAAW,SAAS,KAAK,GAAG,IAE5D,MAAM,IAAI,MAAM,yBAAyB;EAC3C,MAAM,OAAO,GAAG,SAAS,QAAQ,SAAS,mBAAmB,SAAS,KAAK,MAAM,OAAO,GAAG,mBAAmB,SAAS,OAAO,EAAE,GAAG,SAAS,YAAY,cAAc,mBAAmB,SAAS,SAAS,EAAE,KAAK,KAAK,mBAAmB,SAAS,QAAQ,EAAE,GAAG,mBAAmB,SAAS,IAAI;EAChS,IAAI,SAAS,SAAS,MAAM,MAAM,IAAI,MAAM,qBAAqB;EACjE,MAAM,SAAS,OAAO,YACpB,OAAO,QAAQ,SAAS,UAAU,CAAC,CAAC,CAAC,CAClC,MAAM,GAAG,EAAE,CAAC,CACZ,QAAQ,CAAC,KAAK,WAAW,IAAI,UAAU,OAAO,OAAO,UAAU,YAAY,MAAM,UAAU,GAAG,CACnG;EAeA,MAAM,0BAAS,IAdiB,IAAI;GAClC;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF,CACe,EAAA,CAAoB,IAAI,GAAG,SAAS,MAAM,GAAG,SAAS,UAAU,KAC1E,SAAS,UAAU,CAAC,EAAA,CAClB,MAAM,GAAG,EAAE,CAAC,CACZ,SAAS,UACR,SAAS,OAAO,MAAM,UAAU,YAAY,OAAO,MAAM,SAAS,WAC9D,CAAC;GAAE,OAAO,MAAM,MAAM,MAAM,GAAG,EAAE;GAAG,MAAM,MAAM,KAAK,MAAM,GAAG,GAAG;EAAE,CAAC,IACpE,CAAC,CACP,IACF,CAAC;EACL,IAAI,YAAY;EAChB,OAAO;GACL,MAAM,SAAS;GACf,MAAM,SAAS;GACf,WAAW,SAAS;GACpB;GACA;GACA,OAAO,SAAS;GAChB,SAAS,SAAS;GAClB,UAAU,SAAS;GACnB,KAAK,SAAS;GACd,SAAS,MAAM,QAAQ,SAAS,OAAO,IACnC,SAAS,QACN,QAAQ,UAAU,OAAO,UAAU,YAAY,2BAA2B,KAAK,KAAK,CAAC,CAAC,CACtF,MAAM,GAAG,EAAE,IACd,CAAC;GACL,QAAQ,OAAO,SAAS,UAAU;IAChC,MAAM,OAAO,MAAM,KAAK,MAAM,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC;IACvD,aAAa,KAAK;IAClB,OAAO,OAAO,CAAC;KAAE,OAAO,MAAM;KAAO;IAAK,CAAC,IAAI,CAAC;GAClD,CAAC;EACH;CACF,CAAC;AACH"}
@@ -6,6 +6,8 @@ var MonitoringPreferencesStore = class extends require_extensions.Common.Store.E
6
6
  enabled = false;
7
7
  diagnosticsVisible = true;
8
8
  relationshipsVisible = true;
9
+ resourceSearchEnabled = true;
10
+ hiddenUpstreamClusters = [];
9
11
  historyEnabled = false;
10
12
  trackedHistory = [];
11
13
  constructor() {
@@ -15,6 +17,8 @@ var MonitoringPreferencesStore = class extends require_extensions.Common.Store.E
15
17
  enabled: false,
16
18
  diagnosticsVisible: true,
17
19
  relationshipsVisible: true,
20
+ resourceSearchEnabled: true,
21
+ hiddenUpstreamClusters: [],
18
22
  historyEnabled: false,
19
23
  trackedHistory: []
20
24
  }
@@ -23,6 +27,8 @@ var MonitoringPreferencesStore = class extends require_extensions.Common.Store.E
23
27
  enabled: require__global_external_mobx.observable,
24
28
  diagnosticsVisible: require__global_external_mobx.observable,
25
29
  relationshipsVisible: require__global_external_mobx.observable,
30
+ resourceSearchEnabled: require__global_external_mobx.observable,
31
+ hiddenUpstreamClusters: require__global_external_mobx.observable,
26
32
  historyEnabled: require__global_external_mobx.observable,
27
33
  trackedHistory: require__global_external_mobx.observable,
28
34
  fromStore: require__global_external_mobx.action
@@ -32,6 +38,8 @@ var MonitoringPreferencesStore = class extends require_extensions.Common.Store.E
32
38
  this.enabled = data?.enabled === true;
33
39
  this.diagnosticsVisible = data?.diagnosticsVisible !== false;
34
40
  this.relationshipsVisible = data?.relationshipsVisible !== false;
41
+ this.resourceSearchEnabled = data?.resourceSearchEnabled !== false;
42
+ this.hiddenUpstreamClusters = Array.isArray(data?.hiddenUpstreamClusters) ? [...new Set(data.hiddenUpstreamClusters.filter((id) => typeof id === "string" && id.length > 0 && id.length <= 256))].slice(-100) : [];
35
43
  this.historyEnabled = data?.historyEnabled === true;
36
44
  this.trackedHistory = Array.isArray(data?.trackedHistory) ? data.trackedHistory.filter(require_monitoring_history.validHistoryIdentity).slice(0, 100).map(({ clusterId, scope, uid }) => ({
37
45
  clusterId,
@@ -44,6 +52,8 @@ var MonitoringPreferencesStore = class extends require_extensions.Common.Store.E
44
52
  enabled: this.enabled,
45
53
  diagnosticsVisible: this.diagnosticsVisible,
46
54
  relationshipsVisible: this.relationshipsVisible,
55
+ resourceSearchEnabled: this.resourceSearchEnabled,
56
+ hiddenUpstreamClusters: [...this.hiddenUpstreamClusters],
47
57
  historyEnabled: this.historyEnabled,
48
58
  trackedHistory: this.trackedHistory.map(({ clusterId, scope, uid }) => ({
49
59
  clusterId,
@@ -1 +1 @@
1
- {"version":3,"file":"monitoring-preferences-store.js","names":[],"sources":["../../../../src/common/store/monitoring-preferences-store.ts"],"sourcesContent":["import { Common } from \"@freelensapp/extensions\";\nimport { action, makeObservable, observable } from \"mobx\";\nimport { type HistoryIdentity, validHistoryIdentity } from \"../monitoring-history\";\n\nexport class MonitoringPreferencesStore extends Common.Store.ExtensionStore<{\n enabled: boolean;\n diagnosticsVisible: boolean;\n relationshipsVisible: boolean;\n historyEnabled: boolean;\n trackedHistory: HistoryIdentity[];\n}> {\n enabled = false;\n diagnosticsVisible = true;\n relationshipsVisible = true;\n historyEnabled = false;\n trackedHistory: HistoryIdentity[] = [];\n\n constructor() {\n super({\n configName: \"monitoring-preferences\",\n defaults: {\n enabled: false,\n diagnosticsVisible: true,\n relationshipsVisible: true,\n historyEnabled: false,\n trackedHistory: [],\n },\n });\n makeObservable(this, {\n enabled: observable,\n diagnosticsVisible: observable,\n relationshipsVisible: observable,\n historyEnabled: observable,\n trackedHistory: observable,\n fromStore: action,\n });\n }\n\n fromStore(data: {\n enabled?: boolean;\n diagnosticsVisible?: boolean;\n relationshipsVisible?: boolean;\n historyEnabled?: boolean;\n trackedHistory?: HistoryIdentity[];\n }): void {\n this.enabled = data?.enabled === true;\n this.diagnosticsVisible = data?.diagnosticsVisible !== false;\n this.relationshipsVisible = data?.relationshipsVisible !== false;\n this.historyEnabled = data?.historyEnabled === true;\n this.trackedHistory = Array.isArray(data?.trackedHistory)\n ? data.trackedHistory\n .filter(validHistoryIdentity)\n .slice(0, 100)\n .map(({ clusterId, scope, uid }) => ({ clusterId, scope, uid }))\n : [];\n }\n\n toJSON(): {\n enabled: boolean;\n diagnosticsVisible: boolean;\n relationshipsVisible: boolean;\n historyEnabled: boolean;\n trackedHistory: HistoryIdentity[];\n } {\n return {\n enabled: this.enabled,\n diagnosticsVisible: this.diagnosticsVisible,\n relationshipsVisible: this.relationshipsVisible,\n historyEnabled: this.historyEnabled,\n trackedHistory: this.trackedHistory.map(({ clusterId, scope, uid }) => ({ clusterId, scope, uid })),\n };\n }\n}\n"],"mappings":";;;;AAIA,IAAa,6BAAb,cAAgD,mBAAA,OAAO,MAAM,eAM1D;CACD,UAAU;CACV,qBAAqB;CACrB,uBAAuB;CACvB,iBAAiB;CACjB,iBAAoC,CAAC;CAErC,cAAc;EACZ,MAAM;GACJ,YAAY;GACZ,UAAU;IACR,SAAS;IACT,oBAAoB;IACpB,sBAAsB;IACtB,gBAAgB;IAChB,gBAAgB,CAAC;GACnB;EACF,CAAC;EACD,8BAAA,eAAe,MAAM;GACnB,SAAS,8BAAA;GACT,oBAAoB,8BAAA;GACpB,sBAAsB,8BAAA;GACtB,gBAAgB,8BAAA;GAChB,gBAAgB,8BAAA;GAChB,WAAW,8BAAA;EACb,CAAC;CACH;CAEA,UAAU,MAMD;EACP,KAAK,UAAU,MAAM,YAAY;EACjC,KAAK,qBAAqB,MAAM,uBAAuB;EACvD,KAAK,uBAAuB,MAAM,yBAAyB;EAC3D,KAAK,iBAAiB,MAAM,mBAAmB;EAC/C,KAAK,iBAAiB,MAAM,QAAQ,MAAM,cAAc,IACpD,KAAK,eACF,OAAO,2BAAA,oBAAoB,CAAC,CAC5B,MAAM,GAAG,GAAG,CAAC,CACb,KAAK,EAAE,WAAW,OAAO,WAAW;GAAE;GAAW;GAAO;EAAI,EAAE,IACjE,CAAC;CACP;CAEA,SAME;EACA,OAAO;GACL,SAAS,KAAK;GACd,oBAAoB,KAAK;GACzB,sBAAsB,KAAK;GAC3B,gBAAgB,KAAK;GACrB,gBAAgB,KAAK,eAAe,KAAK,EAAE,WAAW,OAAO,WAAW;IAAE;IAAW;IAAO;GAAI,EAAE;EACpG;CACF;AACF"}
1
+ {"version":3,"file":"monitoring-preferences-store.js","names":[],"sources":["../../../../src/common/store/monitoring-preferences-store.ts"],"sourcesContent":["import { Common } from \"@freelensapp/extensions\";\nimport { action, makeObservable, observable } from \"mobx\";\nimport { type HistoryIdentity, validHistoryIdentity } from \"../monitoring-history\";\n\nexport class MonitoringPreferencesStore extends Common.Store.ExtensionStore<{\n enabled: boolean;\n diagnosticsVisible: boolean;\n relationshipsVisible: boolean;\n resourceSearchEnabled: boolean;\n hiddenUpstreamClusters: string[];\n historyEnabled: boolean;\n trackedHistory: HistoryIdentity[];\n}> {\n enabled = false;\n diagnosticsVisible = true;\n relationshipsVisible = true;\n resourceSearchEnabled = true;\n hiddenUpstreamClusters: string[] = [];\n historyEnabled = false;\n trackedHistory: HistoryIdentity[] = [];\n\n constructor() {\n super({\n configName: \"monitoring-preferences\",\n defaults: {\n enabled: false,\n diagnosticsVisible: true,\n relationshipsVisible: true,\n resourceSearchEnabled: true,\n hiddenUpstreamClusters: [],\n historyEnabled: false,\n trackedHistory: [],\n },\n });\n makeObservable(this, {\n enabled: observable,\n diagnosticsVisible: observable,\n relationshipsVisible: observable,\n resourceSearchEnabled: observable,\n hiddenUpstreamClusters: observable,\n historyEnabled: observable,\n trackedHistory: observable,\n fromStore: action,\n });\n }\n\n fromStore(data: {\n enabled?: boolean;\n diagnosticsVisible?: boolean;\n relationshipsVisible?: boolean;\n resourceSearchEnabled?: boolean;\n hiddenUpstreamClusters?: string[];\n historyEnabled?: boolean;\n trackedHistory?: HistoryIdentity[];\n }): void {\n this.enabled = data?.enabled === true;\n this.diagnosticsVisible = data?.diagnosticsVisible !== false;\n this.relationshipsVisible = data?.relationshipsVisible !== false;\n this.resourceSearchEnabled = data?.resourceSearchEnabled !== false;\n this.hiddenUpstreamClusters = Array.isArray(data?.hiddenUpstreamClusters)\n ? [\n ...new Set(\n data.hiddenUpstreamClusters.filter((id) => typeof id === \"string\" && id.length > 0 && id.length <= 256),\n ),\n ].slice(-100)\n : [];\n this.historyEnabled = data?.historyEnabled === true;\n this.trackedHistory = Array.isArray(data?.trackedHistory)\n ? data.trackedHistory\n .filter(validHistoryIdentity)\n .slice(0, 100)\n .map(({ clusterId, scope, uid }) => ({ clusterId, scope, uid }))\n : [];\n }\n\n toJSON(): {\n enabled: boolean;\n diagnosticsVisible: boolean;\n relationshipsVisible: boolean;\n resourceSearchEnabled: boolean;\n hiddenUpstreamClusters: string[];\n historyEnabled: boolean;\n trackedHistory: HistoryIdentity[];\n } {\n return {\n enabled: this.enabled,\n diagnosticsVisible: this.diagnosticsVisible,\n relationshipsVisible: this.relationshipsVisible,\n resourceSearchEnabled: this.resourceSearchEnabled,\n hiddenUpstreamClusters: [...this.hiddenUpstreamClusters],\n historyEnabled: this.historyEnabled,\n trackedHistory: this.trackedHistory.map(({ clusterId, scope, uid }) => ({ clusterId, scope, uid })),\n };\n }\n}\n"],"mappings":";;;;AAIA,IAAa,6BAAb,cAAgD,mBAAA,OAAO,MAAM,eAQ1D;CACD,UAAU;CACV,qBAAqB;CACrB,uBAAuB;CACvB,wBAAwB;CACxB,yBAAmC,CAAC;CACpC,iBAAiB;CACjB,iBAAoC,CAAC;CAErC,cAAc;EACZ,MAAM;GACJ,YAAY;GACZ,UAAU;IACR,SAAS;IACT,oBAAoB;IACpB,sBAAsB;IACtB,uBAAuB;IACvB,wBAAwB,CAAC;IACzB,gBAAgB;IAChB,gBAAgB,CAAC;GACnB;EACF,CAAC;EACD,8BAAA,eAAe,MAAM;GACnB,SAAS,8BAAA;GACT,oBAAoB,8BAAA;GACpB,sBAAsB,8BAAA;GACtB,uBAAuB,8BAAA;GACvB,wBAAwB,8BAAA;GACxB,gBAAgB,8BAAA;GAChB,gBAAgB,8BAAA;GAChB,WAAW,8BAAA;EACb,CAAC;CACH;CAEA,UAAU,MAQD;EACP,KAAK,UAAU,MAAM,YAAY;EACjC,KAAK,qBAAqB,MAAM,uBAAuB;EACvD,KAAK,uBAAuB,MAAM,yBAAyB;EAC3D,KAAK,wBAAwB,MAAM,0BAA0B;EAC7D,KAAK,yBAAyB,MAAM,QAAQ,MAAM,sBAAsB,IACpE,CACE,GAAG,IAAI,IACL,KAAK,uBAAuB,QAAQ,OAAO,OAAO,OAAO,YAAY,GAAG,SAAS,KAAK,GAAG,UAAU,GAAG,CACxG,CACF,CAAC,CAAC,MAAM,IAAI,IACZ,CAAC;EACL,KAAK,iBAAiB,MAAM,mBAAmB;EAC/C,KAAK,iBAAiB,MAAM,QAAQ,MAAM,cAAc,IACpD,KAAK,eACF,OAAO,2BAAA,oBAAoB,CAAC,CAC5B,MAAM,GAAG,GAAG,CAAC,CACb,KAAK,EAAE,WAAW,OAAO,WAAW;GAAE;GAAW;GAAO;EAAI,EAAE,IACjE,CAAC;CACP;CAEA,SAQE;EACA,OAAO;GACL,SAAS,KAAK;GACd,oBAAoB,KAAK;GACzB,sBAAsB,KAAK;GAC3B,uBAAuB,KAAK;GAC5B,wBAAwB,CAAC,GAAG,KAAK,sBAAsB;GACvD,gBAAgB,KAAK;GACrB,gBAAgB,KAAK,eAAe,KAAK,EAAE,WAAW,OAAO,WAAW;IAAE;IAAW;IAAO;GAAI,EAAE;EACpG;CACF;AACF"}
package/out/main/index.js CHANGED
@@ -24,6 +24,7 @@ const require_dashboard_views = require("./dashboard-views.js");
24
24
  const require_diagnostics_menu_order = require("./diagnostics-menu-order.js");
25
25
  const require_dock_icon = require("./dock-icon.js");
26
26
  const require_monitoring_history = require("./monitoring-history.js");
27
+ const require_resource_search = require("./resource-search.js");
27
28
  const require_settings_export = require("./settings-export.js");
28
29
  const require_splash_accent = require("./splash-accent.js");
29
30
  //#region src/main/index.ts
@@ -56,6 +57,7 @@ var FreelensPowerMain = class extends require_extensions.Main.LensExtension {
56
57
  for (const dispose of disposers) dispose();
57
58
  disposers = enabled ? [
58
59
  require_monitoring_history.startMonitoringHistory(this),
60
+ require_resource_search.startResourceSearchIndex(),
59
61
  require_diagnostics_menu_order.startDiagnosticsMenuOrder(),
60
62
  require_ai_ipc.startAiIpc(),
61
63
  require_ai_models.startAiModels(),
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/main/index.ts"],"sourcesContent":["import { Main } from \"@freelensapp/extensions\";\nimport { reaction } from \"mobx\";\nimport { EXTENSION_DISPLAY_NAME, EXTENSION_ID } from \"../common/extension-info\";\nimport { AiPreferencesStore } from \"../common/store/ai-preferences-store\";\nimport { AiShortcutsStore } from \"../common/store/ai-shortcuts-store\";\nimport { AppearancePreferencesStore } from \"../common/store/appearance-preferences-store\";\nimport { ClusterIconsStore } from \"../common/store/cluster-icons-store\";\nimport { DashboardPreferencesStore } from \"../common/store/dashboard-preferences-store\";\nimport { MonitoringPreferencesStore } from \"../common/store/monitoring-preferences-store\";\nimport { NodeShellPreferencesStore } from \"../common/store/node-shell-preferences-store\";\nimport { PowerPreferencesStore } from \"../common/store/power-preferences-store\";\nimport { ReadOnlyPreferencesStore } from \"../common/store/read-only-preferences-store\";\nimport { startAiIpc } from \"./ai-ipc\";\nimport { startAiModels } from \"./ai-models\";\nimport { startClusterIcons } from \"./cluster-icons\";\nimport { registerClusterIconsCatalog } from \"./cluster-icons-catalog\";\nimport { registerDashboardCatalog } from \"./dashboard-catalog\";\nimport { startDashboardPicker } from \"./dashboard-picker\";\nimport { startDashboardViews } from \"./dashboard-views\";\nimport { startDiagnosticsMenuOrder } from \"./diagnostics-menu-order\";\nimport { startDockIconOverride } from \"./dock-icon\";\nimport { startMonitoringHistory } from \"./monitoring-history\";\nimport { startSettingsExport } from \"./settings-export\";\nimport { startSplashAccentOverride } from \"./splash-accent\";\n\nexport default class FreelensPowerMain extends Main.LensExtension {\n private stopFeatures?: () => void;\n\n get name() {\n return EXTENSION_DISPLAY_NAME;\n }\n get sanitizedExtensionId() {\n return EXTENSION_ID;\n }\n get storeName() {\n return this.manifest.storeName || EXTENSION_ID;\n }\n\n onActivate() {\n const power = PowerPreferencesStore.getInstanceOrCreate<PowerPreferencesStore>();\n power.loadExtension(this);\n AiPreferencesStore.getInstanceOrCreate().loadExtension(this);\n AiShortcutsStore.getInstanceOrCreate().loadExtension(this);\n ReadOnlyPreferencesStore.getInstanceOrCreate().loadExtension(this);\n NodeShellPreferencesStore.getInstanceOrCreate().loadExtension(this);\n MonitoringPreferencesStore.getInstanceOrCreate().loadExtension(this);\n AppearancePreferencesStore.getInstanceOrCreate().loadExtension(this);\n DashboardPreferencesStore.getInstanceOrCreate().loadExtension(this);\n ClusterIconsStore.getInstanceOrCreate().loadExtension(this);\n this.stopFeatures?.();\n const stopExport = startSettingsExport();\n let disposers: (() => void)[] = [];\n const stop = reaction(\n () => power.enabled,\n (enabled) => {\n for (const dispose of disposers) dispose();\n disposers = enabled\n ? [\n startMonitoringHistory(this),\n startDiagnosticsMenuOrder(),\n startAiIpc(),\n startAiModels(),\n startDockIconOverride(),\n startSplashAccentOverride(),\n startDashboardViews(),\n startDashboardPicker(),\n registerDashboardCatalog(this),\n registerClusterIconsCatalog(this),\n startClusterIcons(),\n ]\n : [];\n },\n { fireImmediately: true },\n );\n this.stopFeatures = () => {\n stop();\n stopExport();\n for (const dispose of disposers) dispose();\n disposers = [];\n };\n }\n\n onDeactivate() {\n this.stopFeatures?.();\n this.stopFeatures = undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,IAAqB,oBAArB,cAA+C,mBAAA,KAAK,cAAc;CAChE;CAEA,IAAI,OAAO;EACT,OAAO,uBAAA;CACT;CACA,IAAI,uBAAuB;EACzB,OAAO,uBAAA;CACT;CACA,IAAI,YAAY;EACd,OAAO,KAAK,SAAS,aAAA;CACvB;CAEA,aAAa;EACX,MAAM,QAAQ,gCAAA,sBAAsB,oBAA2C;EAC/E,MAAM,cAAc,IAAI;EACxB,6BAAA,mBAAmB,oBAAoB,CAAC,CAAC,cAAc,IAAI;EAC3D,2BAAA,iBAAiB,oBAAoB,CAAC,CAAC,cAAc,IAAI;EACzD,oCAAA,yBAAyB,oBAAoB,CAAC,CAAC,cAAc,IAAI;EACjE,qCAAA,0BAA0B,oBAAoB,CAAC,CAAC,cAAc,IAAI;EAClE,qCAAA,2BAA2B,oBAAoB,CAAC,CAAC,cAAc,IAAI;EACnE,qCAAA,2BAA2B,oBAAoB,CAAC,CAAC,cAAc,IAAI;EACnE,oCAAA,0BAA0B,oBAAoB,CAAC,CAAC,cAAc,IAAI;EAClE,4BAAA,kBAAkB,oBAAoB,CAAC,CAAC,cAAc,IAAI;EAC1D,KAAK,eAAe;EACpB,MAAM,aAAa,wBAAA,oBAAoB;EACvC,IAAI,YAA4B,CAAC;EACjC,MAAM,OAAO,8BAAA,eACL,MAAM,UACX,YAAY;GACX,KAAK,MAAM,WAAW,WAAW,QAAQ;GACzC,YAAY,UACR;IACE,2BAAA,uBAAuB,IAAI;IAC3B,+BAAA,0BAA0B;IAC1B,eAAA,WAAW;IACX,kBAAA,cAAc;IACd,kBAAA,sBAAsB;IACtB,sBAAA,0BAA0B;IAC1B,wBAAA,oBAAoB;IACpB,yBAAA,qBAAqB;IACrB,0BAAA,yBAAyB,IAAI;IAC7B,8BAAA,4BAA4B,IAAI;IAChC,sBAAA,kBAAkB;GACpB,IACA,CAAC;EACP,GACA,EAAE,iBAAiB,KAAK,CAC1B;EACA,KAAK,qBAAqB;GACxB,KAAK;GACL,WAAW;GACX,KAAK,MAAM,WAAW,WAAW,QAAQ;GACzC,YAAY,CAAC;EACf;CACF;CAEA,eAAe;EACb,KAAK,eAAe;EACpB,KAAK,eAAe,KAAA;CACtB;AACF"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/main/index.ts"],"sourcesContent":["import { Main } from \"@freelensapp/extensions\";\nimport { reaction } from \"mobx\";\nimport { EXTENSION_DISPLAY_NAME, EXTENSION_ID } from \"../common/extension-info\";\nimport { AiPreferencesStore } from \"../common/store/ai-preferences-store\";\nimport { AiShortcutsStore } from \"../common/store/ai-shortcuts-store\";\nimport { AppearancePreferencesStore } from \"../common/store/appearance-preferences-store\";\nimport { ClusterIconsStore } from \"../common/store/cluster-icons-store\";\nimport { DashboardPreferencesStore } from \"../common/store/dashboard-preferences-store\";\nimport { MonitoringPreferencesStore } from \"../common/store/monitoring-preferences-store\";\nimport { NodeShellPreferencesStore } from \"../common/store/node-shell-preferences-store\";\nimport { PowerPreferencesStore } from \"../common/store/power-preferences-store\";\nimport { ReadOnlyPreferencesStore } from \"../common/store/read-only-preferences-store\";\nimport { startAiIpc } from \"./ai-ipc\";\nimport { startAiModels } from \"./ai-models\";\nimport { startClusterIcons } from \"./cluster-icons\";\nimport { registerClusterIconsCatalog } from \"./cluster-icons-catalog\";\nimport { registerDashboardCatalog } from \"./dashboard-catalog\";\nimport { startDashboardPicker } from \"./dashboard-picker\";\nimport { startDashboardViews } from \"./dashboard-views\";\nimport { startDiagnosticsMenuOrder } from \"./diagnostics-menu-order\";\nimport { startDockIconOverride } from \"./dock-icon\";\nimport { startMonitoringHistory } from \"./monitoring-history\";\nimport { startResourceSearchIndex } from \"./resource-search\";\nimport { startSettingsExport } from \"./settings-export\";\nimport { startSplashAccentOverride } from \"./splash-accent\";\n\nexport default class FreelensPowerMain extends Main.LensExtension {\n private stopFeatures?: () => void;\n\n get name() {\n return EXTENSION_DISPLAY_NAME;\n }\n get sanitizedExtensionId() {\n return EXTENSION_ID;\n }\n get storeName() {\n return this.manifest.storeName || EXTENSION_ID;\n }\n\n onActivate() {\n const power = PowerPreferencesStore.getInstanceOrCreate<PowerPreferencesStore>();\n power.loadExtension(this);\n AiPreferencesStore.getInstanceOrCreate().loadExtension(this);\n AiShortcutsStore.getInstanceOrCreate().loadExtension(this);\n ReadOnlyPreferencesStore.getInstanceOrCreate().loadExtension(this);\n NodeShellPreferencesStore.getInstanceOrCreate().loadExtension(this);\n MonitoringPreferencesStore.getInstanceOrCreate().loadExtension(this);\n AppearancePreferencesStore.getInstanceOrCreate().loadExtension(this);\n DashboardPreferencesStore.getInstanceOrCreate().loadExtension(this);\n ClusterIconsStore.getInstanceOrCreate().loadExtension(this);\n this.stopFeatures?.();\n const stopExport = startSettingsExport();\n let disposers: (() => void)[] = [];\n const stop = reaction(\n () => power.enabled,\n (enabled) => {\n for (const dispose of disposers) dispose();\n disposers = enabled\n ? [\n startMonitoringHistory(this),\n startResourceSearchIndex(),\n startDiagnosticsMenuOrder(),\n startAiIpc(),\n startAiModels(),\n startDockIconOverride(),\n startSplashAccentOverride(),\n startDashboardViews(),\n startDashboardPicker(),\n registerDashboardCatalog(this),\n registerClusterIconsCatalog(this),\n startClusterIcons(),\n ]\n : [];\n },\n { fireImmediately: true },\n );\n this.stopFeatures = () => {\n stop();\n stopExport();\n for (const dispose of disposers) dispose();\n disposers = [];\n };\n }\n\n onDeactivate() {\n this.stopFeatures?.();\n this.stopFeatures = undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAqB,oBAArB,cAA+C,mBAAA,KAAK,cAAc;CAChE;CAEA,IAAI,OAAO;EACT,OAAO,uBAAA;CACT;CACA,IAAI,uBAAuB;EACzB,OAAO,uBAAA;CACT;CACA,IAAI,YAAY;EACd,OAAO,KAAK,SAAS,aAAA;CACvB;CAEA,aAAa;EACX,MAAM,QAAQ,gCAAA,sBAAsB,oBAA2C;EAC/E,MAAM,cAAc,IAAI;EACxB,6BAAA,mBAAmB,oBAAoB,CAAC,CAAC,cAAc,IAAI;EAC3D,2BAAA,iBAAiB,oBAAoB,CAAC,CAAC,cAAc,IAAI;EACzD,oCAAA,yBAAyB,oBAAoB,CAAC,CAAC,cAAc,IAAI;EACjE,qCAAA,0BAA0B,oBAAoB,CAAC,CAAC,cAAc,IAAI;EAClE,qCAAA,2BAA2B,oBAAoB,CAAC,CAAC,cAAc,IAAI;EACnE,qCAAA,2BAA2B,oBAAoB,CAAC,CAAC,cAAc,IAAI;EACnE,oCAAA,0BAA0B,oBAAoB,CAAC,CAAC,cAAc,IAAI;EAClE,4BAAA,kBAAkB,oBAAoB,CAAC,CAAC,cAAc,IAAI;EAC1D,KAAK,eAAe;EACpB,MAAM,aAAa,wBAAA,oBAAoB;EACvC,IAAI,YAA4B,CAAC;EACjC,MAAM,OAAO,8BAAA,eACL,MAAM,UACX,YAAY;GACX,KAAK,MAAM,WAAW,WAAW,QAAQ;GACzC,YAAY,UACR;IACE,2BAAA,uBAAuB,IAAI;IAC3B,wBAAA,yBAAyB;IACzB,+BAAA,0BAA0B;IAC1B,eAAA,WAAW;IACX,kBAAA,cAAc;IACd,kBAAA,sBAAsB;IACtB,sBAAA,0BAA0B;IAC1B,wBAAA,oBAAoB;IACpB,yBAAA,qBAAqB;IACrB,0BAAA,yBAAyB,IAAI;IAC7B,8BAAA,4BAA4B,IAAI;IAChC,sBAAA,kBAAkB;GACpB,IACA,CAAC;EACP,GACA,EAAE,iBAAiB,KAAK,CAC1B;EACA,KAAK,qBAAqB;GACxB,KAAK;GACL,WAAW;GACX,KAAK,MAAM,WAAW,WAAW,QAAQ;GACzC,YAAY,CAAC;EACf;CACF;CAEA,eAAe;EACb,KAAK,eAAe;EACpB,KAAK,eAAe,KAAA;CACtB;AACF"}
@@ -0,0 +1,209 @@
1
+ //#region src/main/resource-search-database.ts
2
+ var ResourceSearchDatabase = class {
3
+ path;
4
+ db;
5
+ constructor(path, maxBytes = 256 * 1024 * 1024) {
6
+ this.path = path;
7
+ const { DatabaseSync } = require("node:sqlite");
8
+ this.db = new DatabaseSync(path);
9
+ this.db.exec(`PRAGMA busy_timeout=2000; PRAGMA foreign_keys=ON; PRAGMA journal_mode=DELETE; PRAGMA cache_size=-4096;
10
+ CREATE TABLE IF NOT EXISTS documents (
11
+ id INTEGER PRIMARY KEY, cluster TEXT NOT NULL, context TEXT NOT NULL,
12
+ path TEXT NOT NULL, scope TEXT NOT NULL, scan TEXT NOT NULL,
13
+ name TEXT NOT NULL, kind TEXT NOT NULL, json TEXT NOT NULL, text TEXT NOT NULL,
14
+ UNIQUE(cluster, context, path)
15
+ );
16
+ CREATE INDEX IF NOT EXISTS document_scope ON documents(cluster,context,scope);
17
+ CREATE TABLE IF NOT EXISTS scans (cluster TEXT,context TEXT,scope TEXT,scan TEXT,
18
+ PRIMARY KEY(cluster,context,scope)) WITHOUT ROWID;
19
+ CREATE VIRTUAL TABLE IF NOT EXISTS search USING fts5(text,content=documents,content_rowid=id,tokenize=trigram);
20
+ CREATE TRIGGER IF NOT EXISTS document_insert AFTER INSERT ON documents BEGIN
21
+ INSERT INTO search(rowid,text) VALUES(new.id,new.text); END;
22
+ CREATE TRIGGER IF NOT EXISTS document_delete AFTER DELETE ON documents BEGIN
23
+ INSERT INTO search(search,rowid,text) VALUES('delete',old.id,old.text); END;
24
+ CREATE TRIGGER IF NOT EXISTS document_update AFTER UPDATE ON documents BEGIN
25
+ INSERT INTO search(search,rowid,text) VALUES('delete',old.id,old.text);
26
+ INSERT INTO search(rowid,text) VALUES(new.id,new.text); END;
27
+ CREATE TABLE IF NOT EXISTS name_terms (
28
+ document INTEGER REFERENCES documents(id) ON DELETE CASCADE,term TEXT,
29
+ PRIMARY KEY(document,term)) WITHOUT ROWID;
30
+ CREATE INDEX IF NOT EXISTS term_length ON name_terms(length(term));`);
31
+ const pageSize = Number(this.db.prepare("PRAGMA page_size").get()?.page_size ?? 4096);
32
+ this.db.exec(`PRAGMA max_page_count=${Math.max(64, Math.floor(maxBytes / 2 / pageSize))}`);
33
+ this.db.function("search_distance", { deterministic: true }, (left, right, limit) => this.distance(String(left), String(right), Number(limit)));
34
+ this.db.function("search_parts", { deterministic: true }, (name, token) => this.parts(String(name), String(token)));
35
+ try {
36
+ require("node:fs").chmodSync(path, 384);
37
+ } catch {}
38
+ }
39
+ distance(left, right, limit) {
40
+ if (left.length > 64 || right.length > 64 || Math.abs(left.length - right.length) > limit) return limit + 1;
41
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
42
+ let beforePrevious = previous;
43
+ for (let row = 1; row <= left.length; row++) {
44
+ const current = Array(right.length + 1).fill(limit + 1);
45
+ current[0] = row;
46
+ let minimum = limit + 1;
47
+ for (let column = Math.max(1, row - limit); column <= Math.min(right.length, row + limit); column++) {
48
+ current[column] = Math.min(previous[column] + 1, current[column - 1] + 1, previous[column - 1] + (left[row - 1] === right[column - 1] ? 0 : 1));
49
+ if (row > 1 && column > 1 && left[row - 1] === right[column - 2] && left[row - 2] === right[column - 1]) current[column] = Math.min(current[column], beforePrevious[column - 2] + 1);
50
+ minimum = Math.min(minimum, current[column]);
51
+ }
52
+ if (minimum > limit) return limit + 1;
53
+ beforePrevious = previous;
54
+ previous = current;
55
+ }
56
+ return previous[right.length];
57
+ }
58
+ parts(name, token) {
59
+ if (!/^[a-z0-9]+(?:[-._][a-z0-9]+)+$/.test(token)) return 0;
60
+ const wanted = token.split(/[-._]/);
61
+ if (wanted.length > 16) return 0;
62
+ let offset = 0;
63
+ for (const part of name.toLocaleLowerCase().split(/[-._]/).filter(Boolean).slice(0, 16)) if (part === wanted[offset]) offset++;
64
+ return offset === wanted.length ? 1 : 0;
65
+ }
66
+ run(request) {
67
+ const { clusterId, contextId } = request;
68
+ this.db.prepare("DELETE FROM documents WHERE cluster=? AND context<>?").run(clusterId, contextId);
69
+ this.db.prepare("DELETE FROM scans WHERE cluster=? AND context<>?").run(clusterId, contextId);
70
+ if (request.operation === "query") return this.query(clusterId, contextId, request.query, request.limit, request);
71
+ if (request.operation === "status") return {
72
+ available: true,
73
+ count: Number(this.db.prepare("SELECT count(*) AS count FROM documents WHERE cluster=? AND context=?").get(clusterId, contextId)?.count ?? 0),
74
+ bytes: this.bytes()
75
+ };
76
+ if (request.operation === "clear") {
77
+ this.db.prepare("DELETE FROM documents WHERE cluster=?").run(clusterId);
78
+ this.db.prepare("DELETE FROM scans WHERE cluster=?").run(clusterId);
79
+ this.db.exec("VACUUM");
80
+ return true;
81
+ }
82
+ if (request.operation === "permit") return true;
83
+ if (request.operation === "reconcile") {
84
+ const catalog = JSON.stringify(request.resources);
85
+ this.db.prepare(`DELETE FROM documents WHERE cluster=? AND context=? AND
86
+ json_extract(json,'$.group')||'/'||json_extract(json,'$.resource') NOT IN (SELECT value FROM json_each(?))`).run(clusterId, contextId, catalog);
87
+ this.db.prepare(`DELETE FROM scans WHERE cluster=? AND context=? AND NOT EXISTS
88
+ (SELECT 1 FROM json_each(?) WHERE substr(scope,1,length(value)+1)=value||'/')`).run(clusterId, contextId, catalog);
89
+ return true;
90
+ }
91
+ const { scope } = request;
92
+ const prefix = scope.endsWith("/*") ? scope.slice(0, -1) : scope;
93
+ const scopeCondition = scope.endsWith("/*") ? "substr(scope,1,length(?))=?" : "scope=?";
94
+ const scopeArgs = scope.endsWith("/*") ? [prefix, prefix] : [scope];
95
+ if (request.operation === "invalidate") {
96
+ this.db.prepare(`DELETE FROM documents WHERE cluster=? AND context=? AND ${scopeCondition}`).run(clusterId, contextId, ...scopeArgs);
97
+ this.db.prepare(`DELETE FROM scans WHERE cluster=? AND context=? AND ${scopeCondition}`).run(clusterId, contextId, ...scopeArgs);
98
+ return true;
99
+ }
100
+ if (request.operation === "begin") {
101
+ this.db.prepare("INSERT INTO scans VALUES(?,?,?,?) ON CONFLICT(cluster,context,scope) DO UPDATE SET scan=excluded.scan").run(clusterId, contextId, scope, request.scanId);
102
+ return true;
103
+ }
104
+ if (this.db.prepare("SELECT scan FROM scans WHERE cluster=? AND context=? AND scope=?").get(clusterId, contextId, scope)?.scan !== request.scanId) throw new Error("Search scan superseded");
105
+ if (request.operation === "complete") {
106
+ this.db.prepare(`DELETE FROM documents WHERE cluster=? AND context=? AND ${scopeCondition} AND scan<>?`).run(clusterId, contextId, ...scopeArgs, request.scanId);
107
+ this.db.prepare(`DELETE FROM scans WHERE cluster=? AND context=? AND ${scopeCondition}`).run(clusterId, contextId, ...scopeArgs);
108
+ return true;
109
+ }
110
+ this.db.exec("BEGIN");
111
+ try {
112
+ const insert = this.db.prepare(`INSERT INTO documents(cluster,context,path,scope,scan,name,kind,json,text)
113
+ VALUES(?,?,?,?,?,?,?,?,?) ON CONFLICT(cluster,context,path) DO UPDATE SET scope=excluded.scope,
114
+ scan=excluded.scan,name=excluded.name,kind=excluded.kind,json=excluded.json,text=excluded.text RETURNING id`);
115
+ const term = this.db.prepare("INSERT OR IGNORE INTO name_terms VALUES(?,?)");
116
+ for (const document of request.documents) {
117
+ const text = [
118
+ document.name,
119
+ document.kind,
120
+ document.namespace ?? "",
121
+ ...document.aliases ?? [],
122
+ ...Object.entries(document.labels).map(([key, value]) => `${key}=${value}`),
123
+ ...(document.fields ?? []).map((field) => field.text)
124
+ ].join(" ").toLocaleLowerCase();
125
+ const row = insert.get(clusterId, contextId, document.path, scope, request.scanId, document.name, document.kind, JSON.stringify(document), text);
126
+ this.db.prepare("DELETE FROM name_terms WHERE document=?").run(row.id);
127
+ for (const part of new Set([document.name, ...document.name.split(/[-._]/)].map((part) => part.toLocaleLowerCase()))) if (part.length >= 3 && part.length <= 64) term.run(row.id, part);
128
+ }
129
+ this.db.exec("COMMIT");
130
+ } catch (error) {
131
+ try {
132
+ this.db.exec("ROLLBACK");
133
+ } catch {}
134
+ throw error;
135
+ }
136
+ return true;
137
+ }
138
+ query(cluster, context, query, requestedLimit = 200, filters = {}) {
139
+ const limit = Math.max(1, Math.min(1e3, Math.floor(requestedLimit)));
140
+ const tokens = query.slice(0, 300).toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
141
+ const args = [cluster, context];
142
+ const filtersSql = [];
143
+ if (filters.kind !== void 0) {
144
+ filtersSql.push("LOWER(d.kind)=LOWER(?)");
145
+ args.push(filters.kind);
146
+ }
147
+ if (filters.namespace !== void 0) {
148
+ filtersSql.push("COALESCE(json_extract(d.json,'$.namespace'),'')=?");
149
+ args.push(filters.namespace);
150
+ }
151
+ const escapeLike = (text) => text.replace(/[\\%_]/g, "\\$&");
152
+ const conditions = tokens.map((token) => {
153
+ const clauses = [];
154
+ if ([...token].length >= 3) {
155
+ clauses.push("d.id IN (SELECT rowid FROM search WHERE search MATCH ?)");
156
+ args.push(`"${token.replace(/"/g, "\"\"")}"`);
157
+ } else {
158
+ clauses.push("d.text LIKE ? ESCAPE '\\'");
159
+ args.push(`%${escapeLike(token)}%`);
160
+ }
161
+ if (token.split(/[-._]/).filter(Boolean).length > 1) {
162
+ clauses.push("search_parts(d.name,?)=1");
163
+ args.push(token);
164
+ }
165
+ const distance = token.length >= 7 && token.length <= 64 ? 2 : token.length >= 4 && token.length <= 6 ? 1 : 0;
166
+ if (distance) {
167
+ clauses.push("EXISTS (SELECT 1 FROM name_terms t WHERE t.document=d.id AND length(t.term) BETWEEN ? AND ? AND search_distance(t.term,?,?)<=?)");
168
+ args.push(token.length - distance, token.length + distance, token, distance, distance);
169
+ }
170
+ return `(${clauses.join(" OR ")})`;
171
+ });
172
+ const rankingArgs = [];
173
+ const penalties = tokens.map((token) => {
174
+ const distance = token.length >= 7 && token.length <= 64 ? 2 : token.length >= 4 && token.length <= 6 ? 1 : 0;
175
+ rankingArgs.push(token, token);
176
+ const fallback = distance ? `(SELECT 10*min(search_distance(t.term,?,?)) FROM name_terms t WHERE t.document=d.id AND length(t.term) BETWEEN ? AND ?)` : "100";
177
+ if (distance) rankingArgs.push(token, distance, token.length - distance, token.length + distance);
178
+ return `CASE WHEN instr(d.text,?)>0 THEN 0 WHEN search_parts(d.name,?)=1 THEN 1 ELSE COALESCE(${fallback},100) END`;
179
+ });
180
+ const name = query.toLocaleLowerCase().trim();
181
+ const rows = this.db.prepare(`SELECT d.json,count(*) OVER() AS total FROM documents d
182
+ WHERE d.cluster=? AND d.context=? ${[...filtersSql, ...conditions].length ? `AND ${[...filtersSql, ...conditions].join(" AND ")}` : ""}
183
+ ORDER BY ${penalties.length ? penalties.map((penalty) => `(${penalty})`).join("+") : "NULL"},
184
+ CASE WHEN LOWER(d.name)=? THEN 0 WHEN LOWER(d.name) LIKE ? ESCAPE '\\' THEN 1
185
+ WHEN LOWER(d.kind)=? THEN 2 WHEN instr(LOWER(d.name),?)>0 THEN 3 ELSE 4 END,d.kind,d.name,d.path LIMIT ?`).all(...args, ...rankingArgs, name, `${escapeLike(name)}%`, name, name, limit);
186
+ const total = Number(rows[0]?.total ?? 0);
187
+ return {
188
+ documents: rows.map((row) => JSON.parse(String(row.json))),
189
+ total,
190
+ hasMore: total > rows.length
191
+ };
192
+ }
193
+ bytes() {
194
+ return [this.path, `${this.path}-journal`].reduce((sum, path) => {
195
+ try {
196
+ return sum + require("node:fs").statSync(path).size;
197
+ } catch {
198
+ return sum;
199
+ }
200
+ }, 0);
201
+ }
202
+ close() {
203
+ this.db.close();
204
+ }
205
+ };
206
+ //#endregion
207
+ exports.ResourceSearchDatabase = ResourceSearchDatabase;
208
+
209
+ //# sourceMappingURL=resource-search-database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-search-database.js","names":[],"sources":["../../src/main/resource-search-database.ts"],"sourcesContent":["import type { SearchDocument, SearchIndexRequest, SearchQueryResult } from \"../common/resource-search\";\n\nexport class ResourceSearchDatabase {\n private db: import(\"node:sqlite\").DatabaseSync;\n\n constructor(\n private path: string,\n maxBytes = 256 * 1024 * 1024,\n ) {\n const { DatabaseSync } = require(\"node:sqlite\") as typeof import(\"node:sqlite\");\n this.db = new DatabaseSync(path);\n this.db.exec(`PRAGMA busy_timeout=2000; PRAGMA foreign_keys=ON; PRAGMA journal_mode=DELETE; PRAGMA cache_size=-4096;\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY, cluster TEXT NOT NULL, context TEXT NOT NULL,\n path TEXT NOT NULL, scope TEXT NOT NULL, scan TEXT NOT NULL,\n name TEXT NOT NULL, kind TEXT NOT NULL, json TEXT NOT NULL, text TEXT NOT NULL,\n UNIQUE(cluster, context, path)\n );\n CREATE INDEX IF NOT EXISTS document_scope ON documents(cluster,context,scope);\n CREATE TABLE IF NOT EXISTS scans (cluster TEXT,context TEXT,scope TEXT,scan TEXT,\n PRIMARY KEY(cluster,context,scope)) WITHOUT ROWID;\n CREATE VIRTUAL TABLE IF NOT EXISTS search USING fts5(text,content=documents,content_rowid=id,tokenize=trigram);\n CREATE TRIGGER IF NOT EXISTS document_insert AFTER INSERT ON documents BEGIN\n INSERT INTO search(rowid,text) VALUES(new.id,new.text); END;\n CREATE TRIGGER IF NOT EXISTS document_delete AFTER DELETE ON documents BEGIN\n INSERT INTO search(search,rowid,text) VALUES('delete',old.id,old.text); END;\n CREATE TRIGGER IF NOT EXISTS document_update AFTER UPDATE ON documents BEGIN\n INSERT INTO search(search,rowid,text) VALUES('delete',old.id,old.text);\n INSERT INTO search(rowid,text) VALUES(new.id,new.text); END;\n CREATE TABLE IF NOT EXISTS name_terms (\n document INTEGER REFERENCES documents(id) ON DELETE CASCADE,term TEXT,\n PRIMARY KEY(document,term)) WITHOUT ROWID;\n CREATE INDEX IF NOT EXISTS term_length ON name_terms(length(term));`);\n const pageSize = Number(this.db.prepare(\"PRAGMA page_size\").get()?.page_size ?? 4096);\n this.db.exec(`PRAGMA max_page_count=${Math.max(64, Math.floor(maxBytes / 2 / pageSize))}`);\n this.db.function(\"search_distance\", { deterministic: true }, (left, right, limit) =>\n this.distance(String(left), String(right), Number(limit)),\n );\n this.db.function(\"search_parts\", { deterministic: true }, (name, token) => this.parts(String(name), String(token)));\n try {\n require(\"node:fs\").chmodSync(path, 0o600);\n } catch {\n /* in-memory databases have no file */\n }\n }\n\n private distance(left: string, right: string, limit: number): number {\n if (left.length > 64 || right.length > 64 || Math.abs(left.length - right.length) > limit) return limit + 1;\n let previous = Array.from({ length: right.length + 1 }, (_, index) => index);\n let beforePrevious = previous;\n for (let row = 1; row <= left.length; row++) {\n const current = Array<number>(right.length + 1).fill(limit + 1);\n current[0] = row;\n let minimum = limit + 1;\n for (let column = Math.max(1, row - limit); column <= Math.min(right.length, row + limit); column++) {\n current[column] = Math.min(\n previous[column] + 1,\n current[column - 1] + 1,\n previous[column - 1] + (left[row - 1] === right[column - 1] ? 0 : 1),\n );\n if (row > 1 && column > 1 && left[row - 1] === right[column - 2] && left[row - 2] === right[column - 1])\n current[column] = Math.min(current[column], beforePrevious[column - 2] + 1);\n minimum = Math.min(minimum, current[column]);\n }\n if (minimum > limit) return limit + 1;\n beforePrevious = previous;\n previous = current;\n }\n return previous[right.length];\n }\n\n private parts(name: string, token: string): number {\n if (!/^[a-z0-9]+(?:[-._][a-z0-9]+)+$/.test(token)) return 0;\n const wanted = token.split(/[-._]/);\n if (wanted.length > 16) return 0;\n let offset = 0;\n for (const part of name.toLocaleLowerCase().split(/[-._]/).filter(Boolean).slice(0, 16))\n if (part === wanted[offset]) offset++;\n return offset === wanted.length ? 1 : 0;\n }\n\n run(request: SearchIndexRequest): unknown {\n const { clusterId, contextId } = request;\n this.db.prepare(\"DELETE FROM documents WHERE cluster=? AND context<>?\").run(clusterId, contextId);\n this.db.prepare(\"DELETE FROM scans WHERE cluster=? AND context<>?\").run(clusterId, contextId);\n if (request.operation === \"query\") return this.query(clusterId, contextId, request.query, request.limit, request);\n if (request.operation === \"status\")\n return {\n available: true,\n count: Number(\n this.db\n .prepare(\"SELECT count(*) AS count FROM documents WHERE cluster=? AND context=?\")\n .get(clusterId, contextId)?.count ?? 0,\n ),\n bytes: this.bytes(),\n };\n if (request.operation === \"clear\") {\n this.db.prepare(\"DELETE FROM documents WHERE cluster=?\").run(clusterId);\n this.db.prepare(\"DELETE FROM scans WHERE cluster=?\").run(clusterId);\n this.db.exec(\"VACUUM\");\n return true;\n }\n if (request.operation === \"permit\") return true;\n if (request.operation === \"reconcile\") {\n const catalog = JSON.stringify(request.resources);\n this.db\n .prepare(`DELETE FROM documents WHERE cluster=? AND context=? AND\n json_extract(json,'$.group')||'/'||json_extract(json,'$.resource') NOT IN (SELECT value FROM json_each(?))`)\n .run(clusterId, contextId, catalog);\n this.db\n .prepare(`DELETE FROM scans WHERE cluster=? AND context=? AND NOT EXISTS\n (SELECT 1 FROM json_each(?) WHERE substr(scope,1,length(value)+1)=value||'/')`)\n .run(clusterId, contextId, catalog);\n return true;\n }\n const { scope } = request;\n const prefix = scope.endsWith(\"/*\") ? scope.slice(0, -1) : scope;\n const scopeCondition = scope.endsWith(\"/*\") ? \"substr(scope,1,length(?))=?\" : \"scope=?\";\n const scopeArgs = scope.endsWith(\"/*\") ? [prefix, prefix] : [scope];\n if (request.operation === \"invalidate\") {\n this.db\n .prepare(`DELETE FROM documents WHERE cluster=? AND context=? AND ${scopeCondition}`)\n .run(clusterId, contextId, ...scopeArgs);\n this.db\n .prepare(`DELETE FROM scans WHERE cluster=? AND context=? AND ${scopeCondition}`)\n .run(clusterId, contextId, ...scopeArgs);\n return true;\n }\n if (request.operation === \"begin\") {\n this.db\n .prepare(\n \"INSERT INTO scans VALUES(?,?,?,?) ON CONFLICT(cluster,context,scope) DO UPDATE SET scan=excluded.scan\",\n )\n .run(clusterId, contextId, scope, request.scanId);\n return true;\n }\n const scan = this.db\n .prepare(\"SELECT scan FROM scans WHERE cluster=? AND context=? AND scope=?\")\n .get(clusterId, contextId, scope)?.scan;\n if (scan !== request.scanId) throw new Error(\"Search scan superseded\");\n if (request.operation === \"complete\") {\n this.db\n .prepare(`DELETE FROM documents WHERE cluster=? AND context=? AND ${scopeCondition} AND scan<>?`)\n .run(clusterId, contextId, ...scopeArgs, request.scanId);\n this.db\n .prepare(`DELETE FROM scans WHERE cluster=? AND context=? AND ${scopeCondition}`)\n .run(clusterId, contextId, ...scopeArgs);\n return true;\n }\n this.db.exec(\"BEGIN\");\n try {\n const insert = this.db.prepare(`INSERT INTO documents(cluster,context,path,scope,scan,name,kind,json,text)\n VALUES(?,?,?,?,?,?,?,?,?) ON CONFLICT(cluster,context,path) DO UPDATE SET scope=excluded.scope,\n scan=excluded.scan,name=excluded.name,kind=excluded.kind,json=excluded.json,text=excluded.text RETURNING id`);\n const term = this.db.prepare(\"INSERT OR IGNORE INTO name_terms VALUES(?,?)\");\n for (const document of request.documents) {\n const text = [\n document.name,\n document.kind,\n document.namespace ?? \"\",\n ...(document.aliases ?? []),\n ...Object.entries(document.labels).map(([key, value]) => `${key}=${value}`),\n ...(document.fields ?? []).map((field) => field.text),\n ]\n .join(\" \")\n .toLocaleLowerCase();\n const row = insert.get(\n clusterId,\n contextId,\n document.path,\n scope,\n request.scanId,\n document.name,\n document.kind,\n JSON.stringify(document),\n text,\n );\n this.db.prepare(\"DELETE FROM name_terms WHERE document=?\").run(row!.id);\n for (const part of new Set(\n [document.name, ...document.name.split(/[-._]/)].map((part) => part.toLocaleLowerCase()),\n ))\n if (part.length >= 3 && part.length <= 64) term.run(row!.id, part);\n }\n this.db.exec(\"COMMIT\");\n } catch (error) {\n try {\n this.db.exec(\"ROLLBACK\");\n } catch {\n /* sqlite can roll back a full transaction automatically */\n }\n throw error;\n }\n return true;\n }\n\n query(\n cluster: string,\n context: string,\n query: string,\n requestedLimit = 200,\n filters: { kind?: string; namespace?: string } = {},\n ): SearchQueryResult {\n const limit = Math.max(1, Math.min(1000, Math.floor(requestedLimit)));\n const tokens = query.slice(0, 300).toLocaleLowerCase().trim().split(/\\s+/).filter(Boolean);\n const args: (string | number)[] = [cluster, context];\n const filtersSql: string[] = [];\n if (filters.kind !== undefined) {\n filtersSql.push(\"LOWER(d.kind)=LOWER(?)\");\n args.push(filters.kind);\n }\n if (filters.namespace !== undefined) {\n filtersSql.push(\"COALESCE(json_extract(d.json,'$.namespace'),'')=?\");\n args.push(filters.namespace);\n }\n const escapeLike = (text: string) => text.replace(/[\\\\%_]/g, \"\\\\$&\");\n const conditions = tokens.map((token) => {\n const clauses: string[] = [];\n if ([...token].length >= 3) {\n clauses.push(\"d.id IN (SELECT rowid FROM search WHERE search MATCH ?)\");\n args.push(`\"${token.replace(/\"/g, '\"\"')}\"`);\n } else {\n clauses.push(\"d.text LIKE ? ESCAPE '\\\\'\");\n args.push(`%${escapeLike(token)}%`);\n }\n const parts = token.split(/[-._]/).filter(Boolean);\n if (parts.length > 1) {\n clauses.push(\"search_parts(d.name,?)=1\");\n args.push(token);\n }\n const distance = token.length >= 7 && token.length <= 64 ? 2 : token.length >= 4 && token.length <= 6 ? 1 : 0;\n if (distance) {\n clauses.push(\n \"EXISTS (SELECT 1 FROM name_terms t WHERE t.document=d.id AND length(t.term) BETWEEN ? AND ? AND search_distance(t.term,?,?)<=?)\",\n );\n args.push(token.length - distance, token.length + distance, token, distance, distance);\n }\n return `(${clauses.join(\" OR \")})`;\n });\n const rankingArgs: (string | number)[] = [];\n const penalties = tokens.map((token) => {\n const distance = token.length >= 7 && token.length <= 64 ? 2 : token.length >= 4 && token.length <= 6 ? 1 : 0;\n rankingArgs.push(token, token);\n const fallback = distance\n ? `(SELECT 10*min(search_distance(t.term,?,?)) FROM name_terms t WHERE t.document=d.id AND length(t.term) BETWEEN ? AND ?)`\n : \"100\";\n if (distance) rankingArgs.push(token, distance, token.length - distance, token.length + distance);\n return `CASE WHEN instr(d.text,?)>0 THEN 0 WHEN search_parts(d.name,?)=1 THEN 1 ELSE COALESCE(${fallback},100) END`;\n });\n const name = query.toLocaleLowerCase().trim();\n const rows = this.db\n .prepare(`SELECT d.json,count(*) OVER() AS total FROM documents d\n WHERE d.cluster=? AND d.context=? ${[...filtersSql, ...conditions].length ? `AND ${[...filtersSql, ...conditions].join(\" AND \")}` : \"\"}\n ORDER BY ${penalties.length ? penalties.map((penalty) => `(${penalty})`).join(\"+\") : \"NULL\"},\n CASE WHEN LOWER(d.name)=? THEN 0 WHEN LOWER(d.name) LIKE ? ESCAPE '\\\\' THEN 1\n WHEN LOWER(d.kind)=? THEN 2 WHEN instr(LOWER(d.name),?)>0 THEN 3 ELSE 4 END,d.kind,d.name,d.path LIMIT ?`)\n .all(...args, ...rankingArgs, name, `${escapeLike(name)}%`, name, name, limit);\n const total = Number(rows[0]?.total ?? 0);\n return {\n documents: rows.map((row) => JSON.parse(String(row.json)) as SearchDocument),\n total,\n hasMore: total > rows.length,\n };\n }\n\n private bytes(): number {\n return [this.path, `${this.path}-journal`].reduce((sum, path) => {\n try {\n return sum + require(\"node:fs\").statSync(path).size;\n } catch {\n return sum;\n }\n }, 0);\n }\n\n close(): void {\n this.db.close();\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ const require_resource_search_database = require("./resource-search-database.js");
2
+ let node_worker_threads = require("node:worker_threads");
3
+ //#region src/main/resource-search-worker.ts
4
+ var ResourceSearchWorker = class {
5
+ worker;
6
+ sequence = 0;
7
+ pending = /* @__PURE__ */ new Map();
8
+ failure = false;
9
+ constructor(path) {
10
+ this.worker = new node_worker_threads.Worker(`const {parentPort,workerData}=require('node:worker_threads');
11
+ const Engine=${require_resource_search_database.ResourceSearchDatabase.toString()};let engine;
12
+ parentPort.on('message',({id,request})=>{
13
+ try { engine??=new Engine(workerData);parentPort.postMessage({id,result:engine.run(request)}); }
14
+ catch(error) { parentPort.postMessage({id,error:error?.errcode===13?'Search storage is full; clear the local search cache to continue':'Search index operation failed'}); }
15
+ });`, {
16
+ eval: true,
17
+ workerData: path
18
+ });
19
+ this.worker.on("message", ({ id, result, error }) => {
20
+ const pending = this.pending.get(id);
21
+ this.pending.delete(id);
22
+ if (error) pending?.reject(new Error(error));
23
+ else pending?.resolve(result);
24
+ });
25
+ this.worker.on("error", () => this.fail());
26
+ this.worker.on("exit", () => this.fail());
27
+ }
28
+ fail() {
29
+ this.failure = true;
30
+ for (const pending of this.pending.values()) pending.reject(/* @__PURE__ */ new Error("Local search index is unavailable"));
31
+ this.pending.clear();
32
+ }
33
+ call(request) {
34
+ if (this.failure) return Promise.reject(/* @__PURE__ */ new Error("Local search index is unavailable"));
35
+ if (this.pending.size >= 16) return Promise.reject(/* @__PURE__ */ new Error("Local search index is busy"));
36
+ return new Promise((resolve, reject) => {
37
+ const id = ++this.sequence;
38
+ this.pending.set(id, {
39
+ resolve,
40
+ reject
41
+ });
42
+ this.worker.postMessage({
43
+ id,
44
+ request
45
+ });
46
+ });
47
+ }
48
+ close() {
49
+ this.fail();
50
+ this.worker.terminate();
51
+ }
52
+ };
53
+ //#endregion
54
+ exports.ResourceSearchWorker = ResourceSearchWorker;
55
+
56
+ //# sourceMappingURL=resource-search-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-search-worker.js","names":[],"sources":["../../src/main/resource-search-worker.ts"],"sourcesContent":["import { Worker } from \"node:worker_threads\";\nimport { ResourceSearchDatabase } from \"./resource-search-database\";\n\nimport type { SearchIndexRequest } from \"../common/resource-search\";\n\nexport class ResourceSearchWorker {\n private worker: Worker;\n private sequence = 0;\n private pending = new Map<number, { resolve: (value: unknown) => void; reject: (error: Error) => void }>();\n private failure = false;\n\n constructor(path: string) {\n // the serialized engine remains self-contained in both bundled and preserved builds\n this.worker = new Worker(\n `const {parentPort,workerData}=require('node:worker_threads');\n const Engine=${ResourceSearchDatabase.toString()};let engine;\n parentPort.on('message',({id,request})=>{\n try { engine??=new Engine(workerData);parentPort.postMessage({id,result:engine.run(request)}); }\n catch(error) { parentPort.postMessage({id,error:error?.errcode===13?'Search storage is full; clear the local search cache to continue':'Search index operation failed'}); }\n });`,\n { eval: true, workerData: path },\n );\n this.worker.on(\"message\", ({ id, result, error }) => {\n const pending = this.pending.get(id);\n this.pending.delete(id);\n if (error) pending?.reject(new Error(error));\n else pending?.resolve(result);\n });\n this.worker.on(\"error\", () => this.fail());\n this.worker.on(\"exit\", () => this.fail());\n }\n\n private fail(): void {\n this.failure = true;\n for (const pending of this.pending.values()) pending.reject(new Error(\"Local search index is unavailable\"));\n this.pending.clear();\n }\n\n call(request: SearchIndexRequest): Promise<unknown> {\n if (this.failure) return Promise.reject(new Error(\"Local search index is unavailable\"));\n if (this.pending.size >= 16) return Promise.reject(new Error(\"Local search index is busy\"));\n return new Promise((resolve, reject) => {\n const id = ++this.sequence;\n this.pending.set(id, { resolve, reject });\n this.worker.postMessage({ id, request });\n });\n }\n\n close(): void {\n this.fail();\n void this.worker.terminate();\n }\n}\n"],"mappings":";;;AAKA,IAAa,uBAAb,MAAkC;CAChC;CACA,WAAmB;CACnB,0BAAkB,IAAI,IAAmF;CACzG,UAAkB;CAElB,YAAY,MAAc;EAExB,KAAK,SAAS,IAAI,oBAAA,OAChB;qBACe,iCAAA,uBAAuB,SAAS,EAAE;;;;YAKjD;GAAE,MAAM;GAAM,YAAY;EAAK,CACjC;EACA,KAAK,OAAO,GAAG,YAAY,EAAE,IAAI,QAAQ,YAAY;GACnD,MAAM,UAAU,KAAK,QAAQ,IAAI,EAAE;GACnC,KAAK,QAAQ,OAAO,EAAE;GACtB,IAAI,OAAO,SAAS,OAAO,IAAI,MAAM,KAAK,CAAC;QACtC,SAAS,QAAQ,MAAM;EAC9B,CAAC;EACD,KAAK,OAAO,GAAG,eAAe,KAAK,KAAK,CAAC;EACzC,KAAK,OAAO,GAAG,cAAc,KAAK,KAAK,CAAC;CAC1C;CAEA,OAAqB;EACnB,KAAK,UAAU;EACf,KAAK,MAAM,WAAW,KAAK,QAAQ,OAAO,GAAG,QAAQ,uBAAO,IAAI,MAAM,mCAAmC,CAAC;EAC1G,KAAK,QAAQ,MAAM;CACrB;CAEA,KAAK,SAA+C;EAClD,IAAI,KAAK,SAAS,OAAO,QAAQ,uBAAO,IAAI,MAAM,mCAAmC,CAAC;EACtF,IAAI,KAAK,QAAQ,QAAQ,IAAI,OAAO,QAAQ,uBAAO,IAAI,MAAM,4BAA4B,CAAC;EAC1F,OAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,KAAK,EAAE,KAAK;GAClB,KAAK,QAAQ,IAAI,IAAI;IAAE;IAAS;GAAO,CAAC;GACxC,KAAK,OAAO,YAAY;IAAE;IAAI;GAAQ,CAAC;EACzC,CAAC;CACH;CAEA,QAAc;EACZ,KAAK,KAAK;EACV,KAAU,OAAO,UAAU;CAC7B;AACF"}