xindex 1.0.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 (160) hide show
  1. package/.ai/research/2026-04-10-file-watching.md +79 -0
  2. package/.ai/research/2026-04-10-mcp-output-format.md +129 -0
  3. package/.ai/task/INDEX.md +12 -0
  4. package/.ai/task/done/INDEX.md +3 -0
  5. package/.ai/task/done/task.2026-04-09-local-ai-research-protos.log.md +98 -0
  6. package/.ai/task/done/task.2026-04-09-local-ai-research-protos.md +102 -0
  7. package/.ai/task/task.2026-04-10-cluster-config.log.md +19 -0
  8. package/.ai/task/task.2026-04-10-cluster-config.md +118 -0
  9. package/.ai/task/task.2026-04-10-dir-indexing.log.md +8 -0
  10. package/.ai/task/task.2026-04-10-dir-indexing.md +92 -0
  11. package/.ai/task/task.2026-04-10-line-clustering.log.md +50 -0
  12. package/.ai/task/task.2026-04-10-line-clustering.md +176 -0
  13. package/.ai/task/task.2026-04-10-object-store.log.md +7 -0
  14. package/.ai/task/task.2026-04-10-object-store.md +81 -0
  15. package/.ai/task/task.2026-04-10-search-config.log.md +46 -0
  16. package/.ai/task/task.2026-04-10-search-config.md +274 -0
  17. package/.ai/task/task.2026-04-10-watch-indexing.log.md +32 -0
  18. package/.ai/task/task.2026-04-10-watch-indexing.md +101 -0
  19. package/.ai/task/task.2026-04-10-xindex-mcp.log.md +5 -0
  20. package/.ai/task/task.2026-04-10-xindex-mcp.md +92 -0
  21. package/.ai/task/task.2026-04-10-xindex-mcp.report.md +113 -0
  22. package/.claude/settings.local.json +73 -0
  23. package/.claude/skills/make-hof/SKILL.md +8 -0
  24. package/.claude/skills/make-hof/playbook.md +38 -0
  25. package/.cursor/mcp.json +8 -0
  26. package/.mcp.json +8 -0
  27. package/.xindex.json +22 -0
  28. package/CLAUDE.md +54 -0
  29. package/README.md +206 -0
  30. package/apps/indexApp.ts +31 -0
  31. package/apps/mcpApp.ts +119 -0
  32. package/apps/run.index.ts +19 -0
  33. package/apps/run.mcp.ts +49 -0
  34. package/apps/run.reset.ts +10 -0
  35. package/apps/run.search.ts +21 -0
  36. package/apps/run.watch.ts +44 -0
  37. package/apps/searchApp.ts +9 -0
  38. package/apps/watchApp.ts +53 -0
  39. package/apps/watchFileEventsApp.ts +39 -0
  40. package/bin/xindex-index +2 -0
  41. package/bin/xindex-mcp +2 -0
  42. package/bin/xindex-reset +2 -0
  43. package/bin/xindex-search +2 -0
  44. package/bin/xindex-watch +2 -0
  45. package/componets/IType.ts +1 -0
  46. package/componets/appId.ts +3 -0
  47. package/componets/buildComponents.ts +27 -0
  48. package/componets/config/loadConfig.ts +43 -0
  49. package/componets/config/xindexConfig.ts +4 -0
  50. package/componets/index/contentIndexDriver.ts +39 -0
  51. package/componets/index/formatSearchResults.ts +18 -0
  52. package/componets/index/getIndexStats.ts +11 -0
  53. package/componets/index/handleFileEvent.ts +25 -0
  54. package/componets/index/indexApi.ts +45 -0
  55. package/componets/index/vectraIndex.ts +11 -0
  56. package/componets/index/watcherLock.ts +107 -0
  57. package/componets/keywords/cleanUpKeywords.ts +38 -0
  58. package/componets/keywords/extractKeywords.ts +14 -0
  59. package/componets/keywords/refineKeywords.ts +16 -0
  60. package/componets/llm/embed.ts +18 -0
  61. package/componets/llm/queryLLM.ts +20 -0
  62. package/componets/logger.ts +34 -0
  63. package/componets/walkFiles.ts +51 -0
  64. package/componets/watchFiles.ts +106 -0
  65. package/features/indexContent.ts +16 -0
  66. package/features/removeContent.ts +9 -0
  67. package/features/resetIndex.ts +9 -0
  68. package/features/searchIndex.ts +33 -0
  69. package/package.json +32 -0
  70. package/packages/fun/src/IType.ts +5 -0
  71. package/packages/fun/src/array-finder.ts +55 -0
  72. package/packages/fun/src/array-index.ts +35 -0
  73. package/packages/fun/src/array.ts +112 -0
  74. package/packages/fun/src/assert.ts +5 -0
  75. package/packages/fun/src/asyncRequest.ts +35 -0
  76. package/packages/fun/src/callsites.ts +18 -0
  77. package/packages/fun/src/case-never.ts +9 -0
  78. package/packages/fun/src/casting.ts +41 -0
  79. package/packages/fun/src/collect.ts +13 -0
  80. package/packages/fun/src/concurrency.ts +186 -0
  81. package/packages/fun/src/container.ts +86 -0
  82. package/packages/fun/src/counter.ts +45 -0
  83. package/packages/fun/src/create-map.ts +2 -0
  84. package/packages/fun/src/dedupe.ts +2 -0
  85. package/packages/fun/src/defer.ts +55 -0
  86. package/packages/fun/src/delay.ts +5 -0
  87. package/packages/fun/src/discriminate.ts +34 -0
  88. package/packages/fun/src/enum-values.ts +12 -0
  89. package/packages/fun/src/exponential-backoff.ts +20 -0
  90. package/packages/fun/src/flatten.ts +11 -0
  91. package/packages/fun/src/hash.ts +67 -0
  92. package/packages/fun/src/hash128.ts +6 -0
  93. package/packages/fun/src/hash256.ts +6 -0
  94. package/packages/fun/src/hub.ts +53 -0
  95. package/packages/fun/src/id.ts +10 -0
  96. package/packages/fun/src/interval.ts +76 -0
  97. package/packages/fun/src/is-non-nullable.ts +2 -0
  98. package/packages/fun/src/isIterable.ts +3 -0
  99. package/packages/fun/src/mailbox.ts +13 -0
  100. package/packages/fun/src/map-record.ts +19 -0
  101. package/packages/fun/src/match-collections.ts +57 -0
  102. package/packages/fun/src/match-left-and-right-arrays.ts +78 -0
  103. package/packages/fun/src/mem.ts +26 -0
  104. package/packages/fun/src/memos.ts +28 -0
  105. package/packages/fun/src/normalizeError.ts +25 -0
  106. package/packages/fun/src/nothing.ts +3 -0
  107. package/packages/fun/src/pipe.ts +18 -0
  108. package/packages/fun/src/prettyJson.ts +3 -0
  109. package/packages/fun/src/project.ts +8 -0
  110. package/packages/fun/src/promise.ts +27 -0
  111. package/packages/fun/src/pubsub.ts +128 -0
  112. package/packages/fun/src/randomId.ts +14 -0
  113. package/packages/fun/src/regexp-escape.ts +13 -0
  114. package/packages/fun/src/retry.ts +15 -0
  115. package/packages/fun/src/serial.test.ts +107 -0
  116. package/packages/fun/src/serial.ts +17 -0
  117. package/packages/fun/src/sleep.ts +3 -0
  118. package/packages/fun/src/sort-object.ts +46 -0
  119. package/packages/fun/src/speed-test.ts +56 -0
  120. package/packages/fun/src/tick.ts +37 -0
  121. package/packages/fun/src/time-behavior.ts +50 -0
  122. package/packages/fun/src/time.ts +22 -0
  123. package/packages/fun/src/timedFallback.ts +37 -0
  124. package/packages/fun/src/timer.ts +30 -0
  125. package/packages/fun/src/value.ts +33 -0
  126. package/packages/fun/src/waitForCounter.ts +15 -0
  127. package/packages/streamx/src/batch.ts +23 -0
  128. package/packages/streamx/src/batchTimed.ts +113 -0
  129. package/packages/streamx/src/buffer.ts +72 -0
  130. package/packages/streamx/src/concatenate.ts +33 -0
  131. package/packages/streamx/src/filter.ts +14 -0
  132. package/packages/streamx/src/flat.ts +19 -0
  133. package/packages/streamx/src/flatMap.ts +9 -0
  134. package/packages/streamx/src/from.ts +30 -0
  135. package/packages/streamx/src/index.ts +49 -0
  136. package/packages/streamx/src/interval.ts +58 -0
  137. package/packages/streamx/src/loop.ts +8 -0
  138. package/packages/streamx/src/map.ts +12 -0
  139. package/packages/streamx/src/merge.ts +89 -0
  140. package/packages/streamx/src/nodeReadable.ts +6 -0
  141. package/packages/streamx/src/nodeTransform.ts +9 -0
  142. package/packages/streamx/src/nodeWritable.ts +38 -0
  143. package/packages/streamx/src/objectReader.ts +16 -0
  144. package/packages/streamx/src/polyfill.ts +20 -0
  145. package/packages/streamx/src/reader.ts +38 -0
  146. package/packages/streamx/src/reduce.ts +15 -0
  147. package/packages/streamx/src/scale.ts +93 -0
  148. package/packages/streamx/src/scaleSync.ts +13 -0
  149. package/packages/streamx/src/sequence.ts +7 -0
  150. package/packages/streamx/src/tap.ts +9 -0
  151. package/packages/streamx/src/toArray.ts +9 -0
  152. package/packages/streamx/src/writer.ts +96 -0
  153. package/rnd/hf.ts +14 -0
  154. package/rnd/keywords-compromise.ts +18 -0
  155. package/rnd/keywords-pipeline.ts +79 -0
  156. package/rnd/keywords.ts +38 -0
  157. package/rnd/test-vectra-memory.ts +63 -0
  158. package/rnd/vectra-keywords.ts +95 -0
  159. package/rnd/vectra.ts +50 -0
  160. package/tsconfig.json +14 -0
@@ -0,0 +1,34 @@
1
+ export type ILogger = (...args: any[]) => void;
2
+
3
+ export function Logger(fn: (...args: any[]) => void): ILogger {
4
+ return function log(...args) {
5
+ fn(...args);
6
+ }
7
+ }
8
+
9
+ const DEFAULT_FLUSH_MS = 100;
10
+
11
+ export function BufferedLoggerToStdOut(flushMs = DEFAULT_FLUSH_MS): ILogger {
12
+ return BufferedLogger(s => process.stdout.write(s), flushMs);
13
+ }
14
+
15
+ export function BufferedLoggerToStdErr(flushMs = DEFAULT_FLUSH_MS): ILogger {
16
+ return BufferedLogger(s => process.stderr.write(s), flushMs);
17
+ }
18
+
19
+ export function BufferedLogger(write: (text: string) => void, flushMs = DEFAULT_FLUSH_MS): ILogger {
20
+ let buffer: string[] = [];
21
+ let timer: ReturnType<typeof setTimeout> | null = null;
22
+
23
+ function flush() {
24
+ if (buffer.length === 0) return;
25
+ write(buffer.join("\n") + "\n");
26
+ buffer = [];
27
+ timer = null;
28
+ }
29
+
30
+ return function log(...args) {
31
+ buffer.push(args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "));
32
+ if (!timer) timer = setTimeout(flush, flushMs);
33
+ }
34
+ }
@@ -0,0 +1,51 @@
1
+ import {readdir, stat, readFile} from "fs/promises";
2
+ import {join, relative} from "path";
3
+ import ignore from "ignore";
4
+ import {ILogger} from "./logger.js";
5
+
6
+ export type IWalkFiles = (inputs: string[]) => AsyncIterable<string>;
7
+
8
+ export function WalkFiles({cwd, log, ignoreFiles = []}: {cwd: string, log: ILogger, ignoreFiles?: string[]}): IWalkFiles {
9
+
10
+ async function tryReadGitignore(dir: string): Promise<string> {
11
+ try {
12
+ return await readFile(join(dir, ".gitignore"), "utf8");
13
+ } catch {
14
+ return "";
15
+ }
16
+ }
17
+
18
+ async function* walk(dir: string, parentRules: string[]): AsyncIterable<string> {
19
+ const localGitignore = await tryReadGitignore(dir);
20
+ const rules = localGitignore ? [...parentRules, localGitignore] : parentRules;
21
+ const ig = ignore();
22
+ for (const rule of rules) ig.add(rule);
23
+ for (const pattern of ignoreFiles) ig.add(pattern);
24
+
25
+ const entries = await readdir(dir, {withFileTypes: true});
26
+ for (const entry of entries) {
27
+ const abs = join(dir, entry.name);
28
+ const rel = relative(cwd, abs);
29
+
30
+ if (entry.isDirectory()) {
31
+ if (entry.name.startsWith(".")) continue;
32
+ if (ig.ignores(rel + "/")) continue;
33
+ yield* walk(abs, rules);
34
+ } else {
35
+ if (ig.ignores(rel)) continue;
36
+ yield rel;
37
+ }
38
+ }
39
+ }
40
+
41
+ return async function* walkFiles(inputs) {
42
+ for (const input of inputs) {
43
+ const s = await stat(input);
44
+ if (s.isDirectory()) {
45
+ yield* walk(input, []);
46
+ } else {
47
+ yield relative(cwd, input);
48
+ }
49
+ }
50
+ };
51
+ }
@@ -0,0 +1,106 @@
1
+ import {readFile, stat, watch} from "fs/promises";
2
+ import {join, relative} from "path";
3
+ import ignore from "ignore";
4
+ import {ILogger} from "./logger.js";
5
+ import {IType} from "./IType.js";
6
+
7
+ export enum FileEventType {
8
+ index = 'index',
9
+ remove = 'remove',
10
+ }
11
+
12
+ export type IFileEvent = IType<{ type: FileEventType.index, path: string }> | IType<{
13
+ type: FileEventType.remove,
14
+ path: string
15
+ }>;
16
+
17
+ export type IWatchFilesResult = { events: AsyncIterable<IFileEvent>, stop: () => void };
18
+ export type IWatchFiles = (inputs: string[]) => IWatchFilesResult;
19
+
20
+ export function WatchFiles({cwd, log, ignoreFiles = []}: { cwd: string, log: ILogger, ignoreFiles?: string[] }): IWatchFiles {
21
+
22
+ async function loadGitignore(dir: string): Promise<ReturnType<typeof ignore>> {
23
+ const ig = ignore();
24
+ ig.add(".*");
25
+ try {
26
+ const content = await readFile(join(dir, ".gitignore"), "utf8");
27
+ ig.add(content);
28
+ } catch {
29
+ }
30
+ for (const pattern of ignoreFiles) ig.add(pattern);
31
+ return ig;
32
+ }
33
+
34
+ return function watchFiles(inputs) {
35
+ const pending = new Map<string, IFileEvent>();
36
+ let notify: (() => void) | null = null;
37
+ let stopped = false;
38
+ const abortControllers: AbortController[] = [];
39
+
40
+ async function startWatching(dir: string) {
41
+ const ig = await loadGitignore(dir);
42
+ const ac = new AbortController();
43
+ abortControllers.push(ac);
44
+
45
+ try {
46
+ for await (const event of watch(dir, {recursive: true, signal: ac.signal})) {
47
+ if (stopped) break;
48
+ if (!event.filename) continue;
49
+
50
+ const rel = relative(cwd, join(dir, event.filename));
51
+ if (ig.ignores(rel)) continue;
52
+ if (rel.endsWith("~")) continue;
53
+
54
+ try {
55
+ await stat(join(dir, event.filename));
56
+ pending.set(rel, {type: FileEventType.index, path: rel});
57
+ } catch {
58
+ pending.set(rel, {type: FileEventType.remove, path: rel});
59
+ }
60
+
61
+ notify?.();
62
+ }
63
+ } catch (err: any) {
64
+ if (err?.name !== "AbortError" && !stopped) {
65
+ log(`watch error: ${err}`);
66
+ }
67
+ }
68
+ }
69
+
70
+ for (const input of inputs) {
71
+ startWatching(input);
72
+ }
73
+
74
+ async function* debounced(): AsyncIterable<IFileEvent> {
75
+ while (!stopped) {
76
+ // Wait for at least one event
77
+ if (pending.size === 0) {
78
+ await new Promise<void>(r => {
79
+ notify = r;
80
+ });
81
+ notify = null;
82
+ if (stopped) break;
83
+ }
84
+
85
+ // Debounce: wait 150ms for more events to accumulate
86
+ await new Promise(r => setTimeout(r, 150));
87
+
88
+ // Flush all pending events
89
+ const batch = [...pending.values()];
90
+ pending.clear();
91
+ for (const event of batch) {
92
+ yield event;
93
+ }
94
+ }
95
+ }
96
+
97
+ return {
98
+ events: debounced(),
99
+ stop: () => {
100
+ stopped = true;
101
+ for (const ac of abortControllers) ac.abort();
102
+ notify?.();
103
+ },
104
+ };
105
+ };
106
+ }
@@ -0,0 +1,16 @@
1
+ import {IndexCommandType, IIndexApi} from "../componets/index/indexApi.js";
2
+ import {IExtractKeywords} from "../componets/keywords/extractKeywords.js";
3
+ import {ICleanUpKeywords} from "../componets/keywords/cleanUpKeywords.js";
4
+
5
+ export type IIndexContent = (id: string, content: string) => Promise<void>;
6
+
7
+ export function IndexContent({extractKeywords, cleanUpKeywords, indexApi}: {
8
+ extractKeywords: IExtractKeywords,
9
+ cleanUpKeywords: ICleanUpKeywords,
10
+ indexApi: IIndexApi,
11
+ }): IIndexContent {
12
+ return async function indexContent(id, content) {
13
+ const keywords = cleanUpKeywords(extractKeywords(content)).join(", ");
14
+ await indexApi({type: IndexCommandType.index, id, content: keywords, keywords});
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ import {IndexCommandType, IIndexApi} from "../componets/index/indexApi.js";
2
+
3
+ export type IRemoveContent = (id: string) => Promise<void>;
4
+
5
+ export function RemoveContent({indexApi}: {indexApi: IIndexApi}): IRemoveContent {
6
+ return async function removeContent(id) {
7
+ await indexApi({type: IndexCommandType.delete, id});
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ import {IndexCommandType, IIndexApi} from "../componets/index/indexApi.js";
2
+
3
+ export type IResetIndex = () => Promise<void>;
4
+
5
+ export function ResetIndex({indexApi}: {indexApi: IIndexApi}): IResetIndex {
6
+ return async function resetIndex() {
7
+ await indexApi({type: IndexCommandType.reset});
8
+ }
9
+ }
@@ -0,0 +1,33 @@
1
+ import {LocalIndex} from "vectra";
2
+ import {IEmbed} from "../componets/llm/embed.js";
3
+ import {IExtractKeywords} from "../componets/keywords/extractKeywords.js";
4
+ import {ICleanUpKeywords} from "../componets/keywords/cleanUpKeywords.js";
5
+
6
+ export type IIndexRecord = { score: number; id: string; keywords: string };
7
+
8
+ export type ISearchIndex = (query: string, limit: number) => Promise<IIndexRecord[]>;
9
+
10
+ export function SearchIndex({extractKeywords, cleanUpKeywords, embed, index, scoreThreshold = 0.05}: {
11
+ extractKeywords: IExtractKeywords,
12
+ cleanUpKeywords: ICleanUpKeywords,
13
+ embed: IEmbed,
14
+ index: LocalIndex,
15
+ scoreThreshold?: number
16
+ }): ISearchIndex {
17
+ return async function searchContentIndex(query, limit) {
18
+ const keywords = cleanUpKeywords(extractKeywords(query));
19
+ const searchText = keywords.length > 0 ? keywords.join(", ") : query;
20
+ const vector = await embed(searchText);
21
+
22
+ const results = await index.queryItems(vector, searchText, limit);
23
+
24
+ return results
25
+ .filter(r => r.score >= scoreThreshold)
26
+ .sort((a, b) => b.score - a.score)
27
+ .map(r => ({
28
+ score: r.score,
29
+ id: r.item.id,
30
+ keywords: typeof r.item.metadata?.keywords === "string" ? r.item.metadata.keywords : "",
31
+ }));
32
+ }
33
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "xindex",
3
+ "version": "1.0.0",
4
+ "description": "Local semantic code search — index codebase, search by meaning or keywords",
5
+ "type": "module",
6
+ "main": "xindex.ts",
7
+ "bin": {
8
+ "xindex-index": "bin/xindex-index",
9
+ "xindex-search": "bin/xindex-search",
10
+ "xindex-mcp": "bin/xindex-mcp",
11
+ "xindex-reset": "bin/xindex-reset",
12
+ "xindex-watch": "bin/xindex-watch"
13
+ },
14
+ "scripts": {
15
+ "index": "tsx apps/run.index.ts",
16
+ "search": "tsx apps/run.search.ts",
17
+ "reset": "tsx apps/run.reset.ts",
18
+ "mcp": "tsx apps/run.mcp.ts",
19
+ "watch": "tsx apps/run.watch.ts"
20
+ },
21
+ "private": false,
22
+ "dependencies": {
23
+ "@huggingface/transformers": "^4.0.1",
24
+ "@modelcontextprotocol/sdk": "^1.29.0",
25
+ "compromise": "^14.15.0",
26
+ "ignore": "^7.0.5",
27
+ "keyword-extractor": "^0.0.28",
28
+ "tsx": "^4.21.0",
29
+ "vectra": "^0.14.0",
30
+ "zod": "^4.3.6"
31
+ }
32
+ }
@@ -0,0 +1,5 @@
1
+ export type IType<T extends { type: string } = { type: string }> = Readonly<T>;
2
+ export type IUseType<
3
+ Union extends IType,
4
+ Selector extends IType["type"],
5
+ > = Extract<Union, { type: Selector }>;
@@ -0,0 +1,55 @@
1
+ import { sortObject } from './sort-object';
2
+
3
+ export function ArrayFinder<Type, ID>(
4
+ items: Type[] | ReadonlyArray<Type>,
5
+ identity: (item: Type) => ID
6
+ ): (query: ID) => Type | undefined {
7
+ const map = new Map<string, Type>();
8
+
9
+ const serializeId = (id: ID): string => {
10
+ if (id instanceof Object) {
11
+ return JSON.stringify(sortObject(id));
12
+ } else {
13
+ return String(id);
14
+ }
15
+ };
16
+
17
+ let initialized = false;
18
+
19
+ const initialize = () => {
20
+ for (const item of items) {
21
+ const id = serializeId(identity(item));
22
+ map.set(id, item);
23
+ }
24
+ };
25
+
26
+ return (id: ID) => {
27
+ if (!initialized) {
28
+ initialize();
29
+ initialized = true;
30
+ }
31
+
32
+ return map.get(serializeId(id));
33
+ };
34
+ }
35
+
36
+ export function StrictArrayFinder<Type, ID>(
37
+ items: Type[] | ReadonlyArray<Type>,
38
+ identity: (item: Type) => ID,
39
+ error: (item: ID) => Error | string
40
+ ): (id: ID) => Type {
41
+ const finder = ArrayFinder(items, identity);
42
+ return (id: ID) => {
43
+ const found = finder(id);
44
+
45
+ if (!found) {
46
+ const _error = error(id);
47
+ if (typeof _error === 'string') {
48
+ throw new Error(_error);
49
+ }
50
+ throw _error;
51
+ }
52
+
53
+ return found;
54
+ };
55
+ }
@@ -0,0 +1,35 @@
1
+ import { sortObject } from './sort-object';
2
+
3
+ export function ArrayIndex<Type, ID>(
4
+ items: Type[] | ReadonlyArray<Type>,
5
+ identity: (item: Type) => ID
6
+ ): (query: ID) => Type[] {
7
+ const groups: Record<string, Type[]> = {};
8
+
9
+ const serializeId = (id: ID): string => {
10
+ if (id instanceof Object) {
11
+ return JSON.stringify(sortObject(id));
12
+ } else {
13
+ return String(id);
14
+ }
15
+ };
16
+
17
+ let initialized = false;
18
+
19
+ const initialize = () => {
20
+ for (const item of items) {
21
+ const id = serializeId(identity(item));
22
+ groups[id] = groups[id] || [];
23
+ groups[id].push(item);
24
+ }
25
+ };
26
+
27
+ return (id: ID) => {
28
+ if (!initialized) {
29
+ initialize();
30
+ initialized = true;
31
+ }
32
+
33
+ return groups[serializeId(id)] ?? [];
34
+ };
35
+ }
@@ -0,0 +1,112 @@
1
+ export function toArray<T>(value: T | Array<T> | ReadonlyArray<T>): T[] {
2
+ return (Array.isArray(value) ? value : [value]) as T[];
3
+ }
4
+
5
+ export function toNonEmptyArray<T>(value: T | Array<T> | ReadonlyArray<T> | null): T[] {
6
+ return toArray(value).filter(x => !!x) as T[];
7
+ }
8
+
9
+ export function Exclude(toExclude: string[]) {
10
+ return function (payload: object) {
11
+ const out = { ...payload };
12
+
13
+ toExclude.forEach(column => {
14
+ if (column in out) {
15
+ // @ts-ignore
16
+ delete out[column];
17
+ }
18
+ });
19
+
20
+ return out;
21
+ };
22
+ }
23
+
24
+ const PickNullByDefault = (_column: string): any => null;
25
+
26
+ export function Pick(
27
+ toPick: string[],
28
+ defaultMapper: null | typeof PickNullByDefault = PickNullByDefault
29
+ ) {
30
+ return function (payload: object) {
31
+ const out = {};
32
+
33
+ toPick.forEach(column => {
34
+ if (column in payload) {
35
+ // @ts-ignore
36
+ out[column] = payload[column];
37
+ } else if (defaultMapper instanceof Function) {
38
+ // @ts-ignore
39
+ out[column] = defaultMapper(column);
40
+ }
41
+ });
42
+
43
+ return out;
44
+ };
45
+ }
46
+
47
+ type IGroupByResult<T, KType extends keyof any> = Record<KType, T[]>;
48
+ type IGroupByIdentifier<T, KType extends keyof any = any> = (v: T) => KType;
49
+
50
+ export function groupBy<T, KType extends keyof any = any>(
51
+ values: ReadonlyArray<T>,
52
+ identify: IGroupByIdentifier<T, KType>
53
+ ): IGroupByResult<T, KType> {
54
+ return values.reduce(
55
+ (a, v) => {
56
+ const key = identify(v);
57
+ a[key] = a[key] || [];
58
+ a[key].push(v);
59
+ return a;
60
+ },
61
+ {} as IGroupByResult<T, KType>
62
+ );
63
+ }
64
+
65
+ type IIdentifyBy<T> = (item: T) => any;
66
+
67
+ export function hashMapBy<T extends object>(
68
+ values: T[],
69
+ identify: (item: T) => any
70
+ ): Partial<{ [P in any]: T }> {
71
+ return values.reduce(
72
+ (hashMap, item) => {
73
+ hashMap[identify(item)] = item;
74
+ return hashMap;
75
+ },
76
+ {} as Partial<{ [P in any]: T }>
77
+ );
78
+ }
79
+
80
+ export function uniqBy<T extends object>(values: T[], identify: IIdentifyBy<T>): T[] {
81
+ return Object.values(hashMapBy<T>(values, identify) as Record<any, any>);
82
+ }
83
+
84
+ export function orderBy<TObject, TProperty>(
85
+ predicate: (item: TObject) => TProperty,
86
+ direction: 'asc' | 'desc' = 'asc'
87
+ ) {
88
+ const factor = direction === 'asc' ? 1 : -1;
89
+ return (x: TObject, y: TObject): number => {
90
+ const a = predicate(x);
91
+ const b = predicate(y);
92
+ if (a > b) {
93
+ return 1 * factor;
94
+ } else if (a < b) {
95
+ return -1 * factor;
96
+ } else {
97
+ return 0;
98
+ }
99
+ };
100
+ }
101
+
102
+ export function filterValues<T>(values: (T | undefined | null)[]): T[] {
103
+ const filteredValues: T[] = [];
104
+
105
+ for (const value of values) {
106
+ if (value !== null && value !== undefined) {
107
+ filteredValues.push(value);
108
+ }
109
+ }
110
+
111
+ return filteredValues;
112
+ }
@@ -0,0 +1,5 @@
1
+ export function assert(value: unknown, message?: string): asserts value {
2
+ if (!value) {
3
+ throw new Error(message ?? 'Assertion failed');
4
+ }
5
+ }
@@ -0,0 +1,35 @@
1
+ import { Defer, IDefer } from './defer';
2
+
3
+ export type IAsyncRequest<Request = any, Response = void, Cancellation = void> = Omit<
4
+ IDefer<Response>,
5
+ 'promise'
6
+ > & {
7
+ request: Request;
8
+ response: Promise<Response>;
9
+ cancelled: boolean;
10
+ cancel: (_: Cancellation) => Promise<void> | void;
11
+ };
12
+
13
+ export function AsyncRequest<Request = void, Response = void, Cancellation = void>(
14
+ request: Request,
15
+ onCancel?: (cancellation: Cancellation) => Promise<void> | void
16
+ ): IAsyncRequest<Request, Response, Cancellation> {
17
+ const deferred = Defer<Response>();
18
+
19
+ const cancel = async (cancellation: Cancellation) => {
20
+ instance.cancelled = true;
21
+ if (onCancel) {
22
+ return await onCancel(cancellation);
23
+ }
24
+ };
25
+
26
+ const instance = {
27
+ ...deferred,
28
+ cancelled: false,
29
+ request,
30
+ response: deferred.promise,
31
+ cancel,
32
+ };
33
+
34
+ return instance;
35
+ }
@@ -0,0 +1,18 @@
1
+ // Ripped off https://github.com/sindresorhus/callsites/
2
+
3
+ export function callsites() {
4
+ const _prepareStackTrace = Error.prepareStackTrace;
5
+ try {
6
+ let result: NodeJS.CallSite[] = [];
7
+ Error.prepareStackTrace = (_, callSites) => {
8
+ const callSitesWithoutCurrent = callSites.slice(1);
9
+ result = callSitesWithoutCurrent;
10
+ return callSitesWithoutCurrent;
11
+ };
12
+
13
+ new Error().stack;
14
+ return result;
15
+ } finally {
16
+ Error.prepareStackTrace = _prepareStackTrace;
17
+ }
18
+ }
@@ -0,0 +1,9 @@
1
+ export const caseNever = (_: never): never => {
2
+ let value = 'UNKNOWN';
3
+
4
+ try {
5
+ value = JSON.stringify(_);
6
+ } catch {}
7
+
8
+ throw new Error(`caseNever: ${value}`);
9
+ };
@@ -0,0 +1,41 @@
1
+ export const castAsNumerical = (value: any): number | null => {
2
+ try {
3
+ const result = Number(value);
4
+ if (Number.isNaN(result)) {
5
+ return null;
6
+ }
7
+
8
+ return result;
9
+ } catch (error) {
10
+ return null;
11
+ }
12
+ };
13
+
14
+ export const castAsBoolean = (value: any): boolean | null => {
15
+ const stringValue = String(value).toLowerCase();
16
+ if (stringValue === 'true') {
17
+ return true;
18
+ }
19
+
20
+ if (stringValue === 'false') {
21
+ return false;
22
+ }
23
+
24
+ return null;
25
+ };
26
+
27
+ export const castAsTimestamp = (value: any): Date | null => {
28
+ try {
29
+ return new Date(value);
30
+ } catch (error) {
31
+ return null;
32
+ }
33
+ };
34
+
35
+ export const safeJsonParse = (value: string): any | null => {
36
+ try {
37
+ return JSON.parse(value);
38
+ } catch (error) {
39
+ return null;
40
+ }
41
+ };
@@ -0,0 +1,13 @@
1
+ export type ICollect<T> = (...args: T[]) => T[];
2
+
3
+ export function Collect<T>(): ICollect<T> {
4
+ const records: T[] = [];
5
+
6
+ return function (...args: T[]) {
7
+ if (args.length) {
8
+ records.push(args[0]!);
9
+ }
10
+
11
+ return records;
12
+ };
13
+ }