@mirk/store 0.4.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -8
- package/dist/adapters/sqlite.d.ts +6 -3
- package/dist/adapters/sqlite.js +265 -69
- package/dist/chunk-77IIKHQW.js +120 -0
- package/dist/{chunk-ZI4JA6IU.js → chunk-DKCPULXT.js} +17 -0
- package/dist/chunk-DP4D7CJY.js +59 -0
- package/dist/chunk-KPMRRYTL.js +14 -0
- package/dist/chunk-N27Y55CA.js +67 -0
- package/dist/{chunk-EDVHBRXG.js → chunk-OUY3D4RT.js} +45 -1
- package/dist/filter-B9hP-TKF.d.ts +8 -0
- package/dist/graph.d.ts +98 -0
- package/dist/graph.js +161 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +19 -3
- package/dist/kv.d.ts +6 -4
- package/dist/kv.js +1 -1
- package/dist/search.d.ts +26 -0
- package/dist/search.js +16 -0
- package/dist/sql.d.ts +27 -0
- package/dist/sql.js +14 -0
- package/dist/{types-BqSZEMAB.d.ts → types-B0XrD10b.d.ts} +28 -2
- package/dist/types-BLZTMsQO.d.ts +69 -0
- package/dist/{types-DyQLNtxa.d.ts → types-DHLdLZ_W.d.ts} +12 -1
- package/dist/vector.d.ts +8 -2
- package/dist/vector.js +8 -2
- package/package.json +16 -4
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
// src/to-async.ts
|
|
2
|
+
function hasListWhereIn(store) {
|
|
3
|
+
return typeof store.listWhereIn === "function";
|
|
4
|
+
}
|
|
2
5
|
var AsyncStoreAdapter = class {
|
|
3
6
|
constructor(sync) {
|
|
4
7
|
this.sync = sync;
|
|
8
|
+
if (hasListWhereIn(sync)) {
|
|
9
|
+
this.listWhereIn = async (collection, field, values, filter) => sync.listWhereIn(collection, field, values, filter);
|
|
10
|
+
}
|
|
5
11
|
}
|
|
6
12
|
sync;
|
|
13
|
+
listWhereIn;
|
|
7
14
|
get meta() {
|
|
8
15
|
return this.sync.meta;
|
|
9
16
|
}
|
|
@@ -85,6 +92,16 @@ var InMemoryStore = class {
|
|
|
85
92
|
items = applyFilter(items, filter);
|
|
86
93
|
return items;
|
|
87
94
|
}
|
|
95
|
+
listWhereIn(collection, field, values, filter) {
|
|
96
|
+
if (values.length === 0) return [];
|
|
97
|
+
const set = new Set(values);
|
|
98
|
+
const col = this.ensureCollection(collection);
|
|
99
|
+
const items = [...col.values()].filter((item) => {
|
|
100
|
+
if (typeof item !== "object" || item === null) return false;
|
|
101
|
+
return set.has(item[field]);
|
|
102
|
+
});
|
|
103
|
+
return applyFilter(items, filter);
|
|
104
|
+
}
|
|
88
105
|
getById(collection, id) {
|
|
89
106
|
const col = this.ensureCollection(collection);
|
|
90
107
|
const item = col.get(id);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/sql.ts
|
|
2
|
+
function jsonPath(field) {
|
|
3
|
+
return `$."${field.replace(/"/g, '""')}"`;
|
|
4
|
+
}
|
|
5
|
+
function buildWhereClause(filter) {
|
|
6
|
+
if (!filter?.where || Object.keys(filter.where).length === 0) {
|
|
7
|
+
return { clause: "", params: [] };
|
|
8
|
+
}
|
|
9
|
+
const conditions = [];
|
|
10
|
+
const params = [];
|
|
11
|
+
for (const [key, value] of Object.entries(filter.where)) {
|
|
12
|
+
const path = jsonPath(key);
|
|
13
|
+
if (value === null) {
|
|
14
|
+
conditions.push(`json_type(data, ?) = 'null'`);
|
|
15
|
+
params.push(path);
|
|
16
|
+
} else {
|
|
17
|
+
const bound = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
18
|
+
conditions.push(`json_extract(data, ?) = ?`);
|
|
19
|
+
params.push(path, bound);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { clause: ` WHERE ${conditions.join(" AND ")}`, params };
|
|
23
|
+
}
|
|
24
|
+
function buildOrderBy(filter) {
|
|
25
|
+
if (!filter?.sortBy) return { clause: "", params: [] };
|
|
26
|
+
const dir = filter.sortDir === "desc" ? "DESC" : "ASC";
|
|
27
|
+
const path = jsonPath(filter.sortBy);
|
|
28
|
+
return {
|
|
29
|
+
clause: ` ORDER BY json_extract(data, ?) IS NULL, json_extract(data, ?) ${dir}`,
|
|
30
|
+
params: [path, path]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function buildLimitOffset(filter) {
|
|
34
|
+
let sql = "";
|
|
35
|
+
if (filter?.limit !== void 0) {
|
|
36
|
+
sql += ` LIMIT ${Math.max(0, Math.floor(filter.limit))}`;
|
|
37
|
+
}
|
|
38
|
+
if (filter?.offset !== void 0 && filter.offset > 0) {
|
|
39
|
+
if (!sql.includes("LIMIT")) sql += " LIMIT -1";
|
|
40
|
+
sql += ` OFFSET ${Math.max(0, Math.floor(filter.offset))}`;
|
|
41
|
+
}
|
|
42
|
+
return sql;
|
|
43
|
+
}
|
|
44
|
+
function hashName(s) {
|
|
45
|
+
let h = 2166136261;
|
|
46
|
+
for (let i = 0; i < s.length; i++) {
|
|
47
|
+
h ^= s.charCodeAt(i);
|
|
48
|
+
h = Math.imul(h, 16777619);
|
|
49
|
+
}
|
|
50
|
+
return (h >>> 0).toString(36);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
jsonPath,
|
|
55
|
+
buildWhereClause,
|
|
56
|
+
buildOrderBy,
|
|
57
|
+
buildLimitOffset,
|
|
58
|
+
hashName
|
|
59
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/vector/filter.ts
|
|
2
|
+
function matchesWhere(metadata, filter) {
|
|
3
|
+
if (!metadata) return false;
|
|
4
|
+
for (const [key, expected] of Object.entries(filter)) {
|
|
5
|
+
const actual = metadata[key];
|
|
6
|
+
if (actual === expected) continue;
|
|
7
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) return false;
|
|
8
|
+
}
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
matchesWhere
|
|
14
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/search/tokenize.ts
|
|
2
|
+
function tokenize(text) {
|
|
3
|
+
if (!text) return [];
|
|
4
|
+
return text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
5
|
+
}
|
|
6
|
+
function sanitizeFtsQuery(q) {
|
|
7
|
+
const tokens = tokenize(q);
|
|
8
|
+
if (tokens.length === 0) return "";
|
|
9
|
+
return tokens.map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" OR ");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/search/fields.ts
|
|
13
|
+
var DEFAULT_SEARCH_FIELD = "text";
|
|
14
|
+
function normalizeSearchDocument(doc) {
|
|
15
|
+
if (doc.text !== void 0 && doc.fields !== void 0) {
|
|
16
|
+
throw new Error("SearchDocument must provide either `text` or `fields`, not both.");
|
|
17
|
+
}
|
|
18
|
+
if (doc.text !== void 0) {
|
|
19
|
+
return { names: [DEFAULT_SEARCH_FIELD], values: { [DEFAULT_SEARCH_FIELD]: doc.text } };
|
|
20
|
+
}
|
|
21
|
+
if (doc.fields === void 0) {
|
|
22
|
+
throw new Error("SearchDocument must provide `text` or `fields`.");
|
|
23
|
+
}
|
|
24
|
+
const names = Object.keys(doc.fields).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
25
|
+
if (names.length === 0) throw new Error("SearchDocument.fields must contain at least one field.");
|
|
26
|
+
const values = {};
|
|
27
|
+
for (const name of names) {
|
|
28
|
+
const value = doc.fields[name];
|
|
29
|
+
if (typeof value !== "string") {
|
|
30
|
+
throw new Error(`SearchDocument field "${name}" must be a string.`);
|
|
31
|
+
}
|
|
32
|
+
values[name] = value;
|
|
33
|
+
}
|
|
34
|
+
return { names, values };
|
|
35
|
+
}
|
|
36
|
+
function assertSameSearchFields(existing, incoming, collection) {
|
|
37
|
+
if (existing.length !== incoming.length || existing.some((name, index) => name !== incoming[index])) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Search collection "${collection}" was initialized with fields [${existing.join(", ")}], got [${incoming.join(", ")}].`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function assertValidFieldWeightValues(weights) {
|
|
44
|
+
for (const [field, weight] of Object.entries(weights ?? {})) {
|
|
45
|
+
if (!Number.isFinite(weight) || weight < 0) {
|
|
46
|
+
throw new Error(`Search field weight for "${field}" must be a non-negative finite number.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function fieldWeightsFor(fields, weights) {
|
|
51
|
+
assertValidFieldWeightValues(weights);
|
|
52
|
+
const fieldSet = new Set(fields);
|
|
53
|
+
for (const field of Object.keys(weights ?? {})) {
|
|
54
|
+
if (!fieldSet.has(field)) throw new Error(`Unknown search field weight "${field}".`);
|
|
55
|
+
}
|
|
56
|
+
return fields.map((field) => weights?.[field] ?? 1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
tokenize,
|
|
61
|
+
sanitizeFtsQuery,
|
|
62
|
+
DEFAULT_SEARCH_FIELD,
|
|
63
|
+
normalizeSearchDocument,
|
|
64
|
+
assertSameSearchFields,
|
|
65
|
+
assertValidFieldWeightValues,
|
|
66
|
+
fieldWeightsFor
|
|
67
|
+
};
|
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
cosineSimilarity,
|
|
4
4
|
isUsableVector
|
|
5
5
|
} from "./chunk-BXM3YDOC.js";
|
|
6
|
+
import {
|
|
7
|
+
matchesWhere
|
|
8
|
+
} from "./chunk-KPMRRYTL.js";
|
|
6
9
|
|
|
7
10
|
// src/vector/memory.ts
|
|
8
11
|
var InMemoryVectorStore = class {
|
|
@@ -26,6 +29,9 @@ var InMemoryVectorStore = class {
|
|
|
26
29
|
const doc = this.collections.get(collection)?.get(id);
|
|
27
30
|
return doc ?? null;
|
|
28
31
|
}
|
|
32
|
+
has(collection, id) {
|
|
33
|
+
return this.collections.get(collection)?.has(id) ?? false;
|
|
34
|
+
}
|
|
29
35
|
remove(collection, id) {
|
|
30
36
|
return this.collections.get(collection)?.delete(id) ?? false;
|
|
31
37
|
}
|
|
@@ -40,6 +46,8 @@ var InMemoryVectorStore = class {
|
|
|
40
46
|
if (!coll) return [];
|
|
41
47
|
const scored = [];
|
|
42
48
|
for (const doc of coll.values()) {
|
|
49
|
+
if (opts?.where && !matchesWhere(doc.metadata, opts.where)) continue;
|
|
50
|
+
if (opts?.whereNot && matchesWhere(doc.metadata, opts.whereNot)) continue;
|
|
43
51
|
if (!isUsableVector(doc.vector)) continue;
|
|
44
52
|
const score = cosineSimilarity(query, doc.vector);
|
|
45
53
|
if (!Number.isFinite(score)) continue;
|
|
@@ -59,6 +67,42 @@ var InMemoryVectorStore = class {
|
|
|
59
67
|
}
|
|
60
68
|
};
|
|
61
69
|
|
|
70
|
+
// src/vector/to-async-vector.ts
|
|
71
|
+
var AsyncVectorStoreAdapter = class {
|
|
72
|
+
constructor(sync) {
|
|
73
|
+
this.sync = sync;
|
|
74
|
+
}
|
|
75
|
+
sync;
|
|
76
|
+
get meta() {
|
|
77
|
+
return this.sync.meta;
|
|
78
|
+
}
|
|
79
|
+
async upsert(collection, doc) {
|
|
80
|
+
this.sync.upsert(collection, doc);
|
|
81
|
+
}
|
|
82
|
+
async upsertMany(collection, docs) {
|
|
83
|
+
this.sync.upsertMany(collection, docs);
|
|
84
|
+
}
|
|
85
|
+
async get(collection, id) {
|
|
86
|
+
return this.sync.get(collection, id);
|
|
87
|
+
}
|
|
88
|
+
async has(collection, id) {
|
|
89
|
+
return this.sync.has(collection, id);
|
|
90
|
+
}
|
|
91
|
+
async remove(collection, id) {
|
|
92
|
+
return this.sync.remove(collection, id);
|
|
93
|
+
}
|
|
94
|
+
async count(collection) {
|
|
95
|
+
return this.sync.count(collection);
|
|
96
|
+
}
|
|
97
|
+
async search(collection, query, opts) {
|
|
98
|
+
return this.sync.search(collection, query, opts);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
function toAsyncVector(store) {
|
|
102
|
+
return new AsyncVectorStoreAdapter(store);
|
|
103
|
+
}
|
|
104
|
+
|
|
62
105
|
export {
|
|
63
|
-
InMemoryVectorStore
|
|
106
|
+
InMemoryVectorStore,
|
|
107
|
+
toAsyncVector
|
|
64
108
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Returns true when `metadata` satisfies ALL exact-match conditions in `filter`.
|
|
2
|
+
* A missing metadata object never satisfies a non-empty filter (no field can match).
|
|
3
|
+
* Comparison is deep-equal via JSON round-trip — the same fidelity as metadata
|
|
4
|
+
* persisted and re-parsed from disk; avoids reference-equality surprises for
|
|
5
|
+
* objects/arrays in the filter. */
|
|
6
|
+
declare function matchesWhere(metadata: Record<string, unknown> | undefined, filter: Record<string, unknown>): boolean;
|
|
7
|
+
|
|
8
|
+
export { matchesWhere as m };
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { A as AsyncStore, S as StoreFilter } from './types-DHLdLZ_W.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A directed edge stored as a flat collection record. `from`/`to` are node ids,
|
|
5
|
+
* `type` is the relation kind. Arbitrary metadata fields (e.g. `published`,
|
|
6
|
+
* `weight`) live alongside and are matchable via StoreFilter.where / edgeFilter.
|
|
7
|
+
*/
|
|
8
|
+
interface Edge {
|
|
9
|
+
id: string;
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
type: string;
|
|
13
|
+
[field: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
/** Which way to walk an edge. "both" treats every edge as bidirectional. */
|
|
16
|
+
type Direction = "out" | "in" | "both";
|
|
17
|
+
/**
|
|
18
|
+
* Single-hop adjacent edges of `opts.from`.
|
|
19
|
+
*
|
|
20
|
+
* - "out": edges where `from === opts.from`.
|
|
21
|
+
* - "in": edges where `to === opts.from`.
|
|
22
|
+
* - "both": the union of the two (deduped by edge id).
|
|
23
|
+
*
|
|
24
|
+
* `opts.edgeFilter` (a StoreFilter on edge fields) is applied at the store level;
|
|
25
|
+
* `opts.edgeTypes`, if given, is applied in-memory afterward (the port's `where`
|
|
26
|
+
* is exact-match only and cannot express "type IN [...]").
|
|
27
|
+
*
|
|
28
|
+
* Returns the full stored edge record untouched — no projection. All extra fields
|
|
29
|
+
* (e.g. `from_type`, `to_type`, `weight`) are present on every returned edge.
|
|
30
|
+
*/
|
|
31
|
+
declare function neighbors(store: AsyncStore, collection: string, opts: {
|
|
32
|
+
from: string;
|
|
33
|
+
direction?: Direction;
|
|
34
|
+
edgeTypes?: string[];
|
|
35
|
+
edgeFilter?: StoreFilter;
|
|
36
|
+
}): Promise<Edge[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Batched, fanout-free BFS to `depth` hops from `start`.
|
|
39
|
+
*
|
|
40
|
+
* Load-once strategy (the fanout avoidance): the candidate edge set is loaded
|
|
41
|
+
* with ONE store call — `store.list(collection, edgeFilter)` — applying the
|
|
42
|
+
* caller's policy `edgeFilter` at load. Everything after is in-memory: filter by
|
|
43
|
+
* `edgeTypes`, build an adjacency index, then BFS level-by-level. No per-node
|
|
44
|
+
* query fan-out, regardless of graph size.
|
|
45
|
+
*
|
|
46
|
+
* Return shape:
|
|
47
|
+
* - `nodes`: reached node ids, EXCLUDING `start`. `start` is the origin, not a
|
|
48
|
+
* discovered neighbor — depth 0 therefore yields `[]`, depth 1 yields the
|
|
49
|
+
* direct neighbors, etc. Cycles terminate: a node is expanded at most once.
|
|
50
|
+
* - `edges`: every edge actually traversed, deduped by id.
|
|
51
|
+
*
|
|
52
|
+
* Both arrays are sorted by id so results are deterministic across backings
|
|
53
|
+
* (the parity test relies on this). depth ≤ 0 → `{ nodes: [], edges: [] }`.
|
|
54
|
+
*
|
|
55
|
+
* "both" adjacency: an edge {from:a,to:b} makes a→b AND b→a reachable.
|
|
56
|
+
*
|
|
57
|
+
* Returns the full stored edge record untouched — no projection. All extra fields
|
|
58
|
+
* (e.g. `from_type`, `to_type`, `weight`) are present on every returned edge.
|
|
59
|
+
*/
|
|
60
|
+
declare function traverse(store: AsyncStore, collection: string, opts: {
|
|
61
|
+
start: string;
|
|
62
|
+
depth: number;
|
|
63
|
+
direction?: Direction;
|
|
64
|
+
edgeTypes?: string[];
|
|
65
|
+
edgeFilter?: StoreFilter;
|
|
66
|
+
}): Promise<{
|
|
67
|
+
nodes: string[];
|
|
68
|
+
edges: Edge[];
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Frontier-IN batched BFS to `depth` hops from `start`.
|
|
72
|
+
*
|
|
73
|
+
* When the store exposes the optional `listWhereIn` capability, traversal fetches
|
|
74
|
+
* only edges adjacent to the current BFS frontier at each depth level:
|
|
75
|
+
*
|
|
76
|
+
* - "out": `from IN (frontier)`
|
|
77
|
+
* - "in": `to IN (frontier)`
|
|
78
|
+
* - "both": both queries, deduped by edge id
|
|
79
|
+
*
|
|
80
|
+
* `edgeFilter.where` is still pushed down into the same store query; the
|
|
81
|
+
* structural frontier field (`from`/`to`) overrides any same-named caller filter,
|
|
82
|
+
* matching `neighbors()`. `edgeTypes` remains in-memory because the base port has
|
|
83
|
+
* exact-match filters, not `type IN (...)`.
|
|
84
|
+
*
|
|
85
|
+
* Stores without `listWhereIn` fall back to `traverse()`'s load-once strategy.
|
|
86
|
+
*/
|
|
87
|
+
declare function traverseFrontierBatched(store: AsyncStore, collection: string, opts: {
|
|
88
|
+
start: string;
|
|
89
|
+
depth: number;
|
|
90
|
+
direction?: Direction;
|
|
91
|
+
edgeTypes?: string[];
|
|
92
|
+
edgeFilter?: StoreFilter;
|
|
93
|
+
}): Promise<{
|
|
94
|
+
nodes: string[];
|
|
95
|
+
edges: Edge[];
|
|
96
|
+
}>;
|
|
97
|
+
|
|
98
|
+
export { type Direction, type Edge, neighbors, traverse, traverseFrontierBatched };
|
package/dist/graph.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// src/graph.ts
|
|
2
|
+
function filterByTypes(edges, edgeTypes) {
|
|
3
|
+
if (!edgeTypes || edgeTypes.length === 0) return edges;
|
|
4
|
+
const set = new Set(edgeTypes);
|
|
5
|
+
return edges.filter((e) => set.has(e.type));
|
|
6
|
+
}
|
|
7
|
+
function dedupById(edges) {
|
|
8
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const e of edges) {
|
|
11
|
+
if (seen.has(e.id)) continue;
|
|
12
|
+
seen.add(e.id);
|
|
13
|
+
out.push(e);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
function withWhere(filter, override) {
|
|
18
|
+
return { ...filter, where: { ...filter?.where, ...override } };
|
|
19
|
+
}
|
|
20
|
+
function withoutWhereField(filter, field) {
|
|
21
|
+
if (!filter?.where || !(field in filter.where)) return filter;
|
|
22
|
+
const where = { ...filter.where };
|
|
23
|
+
delete where[field];
|
|
24
|
+
return { ...filter, where };
|
|
25
|
+
}
|
|
26
|
+
function hasListWhereIn(store) {
|
|
27
|
+
return typeof store.listWhereIn === "function";
|
|
28
|
+
}
|
|
29
|
+
async function frontierEdges(store, collection, frontier, direction, edgeFilter) {
|
|
30
|
+
if (direction === "out") {
|
|
31
|
+
return store.listWhereIn(collection, "from", frontier, withoutWhereField(edgeFilter, "from"));
|
|
32
|
+
}
|
|
33
|
+
if (direction === "in") {
|
|
34
|
+
return store.listWhereIn(collection, "to", frontier, withoutWhereField(edgeFilter, "to"));
|
|
35
|
+
}
|
|
36
|
+
const [out, inc] = await Promise.all([
|
|
37
|
+
store.listWhereIn(collection, "from", frontier, withoutWhereField(edgeFilter, "from")),
|
|
38
|
+
store.listWhereIn(collection, "to", frontier, withoutWhereField(edgeFilter, "to"))
|
|
39
|
+
]);
|
|
40
|
+
return dedupById([...out, ...inc]);
|
|
41
|
+
}
|
|
42
|
+
async function neighbors(store, collection, opts) {
|
|
43
|
+
const direction = opts.direction ?? "out";
|
|
44
|
+
let edges;
|
|
45
|
+
if (direction === "out") {
|
|
46
|
+
edges = await store.list(
|
|
47
|
+
collection,
|
|
48
|
+
withWhere(opts.edgeFilter, { from: opts.from })
|
|
49
|
+
);
|
|
50
|
+
} else if (direction === "in") {
|
|
51
|
+
edges = await store.list(
|
|
52
|
+
collection,
|
|
53
|
+
withWhere(opts.edgeFilter, { to: opts.from })
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
const [out, inc] = await Promise.all([
|
|
57
|
+
store.list(collection, withWhere(opts.edgeFilter, { from: opts.from })),
|
|
58
|
+
store.list(collection, withWhere(opts.edgeFilter, { to: opts.from }))
|
|
59
|
+
]);
|
|
60
|
+
edges = dedupById([...out, ...inc]);
|
|
61
|
+
}
|
|
62
|
+
return filterByTypes(edges, opts.edgeTypes);
|
|
63
|
+
}
|
|
64
|
+
async function traverse(store, collection, opts) {
|
|
65
|
+
const direction = opts.direction ?? "out";
|
|
66
|
+
if (!Number.isFinite(opts.depth) || opts.depth <= 0) {
|
|
67
|
+
return { nodes: [], edges: [] };
|
|
68
|
+
}
|
|
69
|
+
const loaded = await store.list(collection, opts.edgeFilter);
|
|
70
|
+
const all = filterByTypes(loaded, opts.edgeTypes);
|
|
71
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
72
|
+
const addAdj = (node, edge) => {
|
|
73
|
+
const list = adjacency.get(node);
|
|
74
|
+
if (list) list.push(edge);
|
|
75
|
+
else adjacency.set(node, [edge]);
|
|
76
|
+
};
|
|
77
|
+
for (const e of all) {
|
|
78
|
+
if (direction === "out") {
|
|
79
|
+
addAdj(e.from, e);
|
|
80
|
+
} else if (direction === "in") {
|
|
81
|
+
addAdj(e.to, e);
|
|
82
|
+
} else {
|
|
83
|
+
addAdj(e.from, e);
|
|
84
|
+
addAdj(e.to, e);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const visited = /* @__PURE__ */ new Set([opts.start]);
|
|
88
|
+
const reached = [];
|
|
89
|
+
const traversedEdges = [];
|
|
90
|
+
const seenEdgeIds = /* @__PURE__ */ new Set();
|
|
91
|
+
let frontier = [opts.start];
|
|
92
|
+
for (let hop = 0; hop < opts.depth && frontier.length > 0; hop++) {
|
|
93
|
+
const next = [];
|
|
94
|
+
for (const node of frontier) {
|
|
95
|
+
const out = adjacency.get(node);
|
|
96
|
+
if (!out) continue;
|
|
97
|
+
for (const edge of out) {
|
|
98
|
+
if (!seenEdgeIds.has(edge.id)) {
|
|
99
|
+
seenEdgeIds.add(edge.id);
|
|
100
|
+
traversedEdges.push(edge);
|
|
101
|
+
}
|
|
102
|
+
const neighbor = edge.from === node ? edge.to : edge.from;
|
|
103
|
+
if (!visited.has(neighbor)) {
|
|
104
|
+
visited.add(neighbor);
|
|
105
|
+
reached.push(neighbor);
|
|
106
|
+
next.push(neighbor);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
frontier = next;
|
|
111
|
+
}
|
|
112
|
+
reached.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
113
|
+
traversedEdges.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
114
|
+
return { nodes: reached, edges: traversedEdges };
|
|
115
|
+
}
|
|
116
|
+
async function traverseFrontierBatched(store, collection, opts) {
|
|
117
|
+
if (!hasListWhereIn(store)) {
|
|
118
|
+
return traverse(store, collection, opts);
|
|
119
|
+
}
|
|
120
|
+
const direction = opts.direction ?? "out";
|
|
121
|
+
if (!Number.isFinite(opts.depth) || opts.depth <= 0) {
|
|
122
|
+
return { nodes: [], edges: [] };
|
|
123
|
+
}
|
|
124
|
+
const visited = /* @__PURE__ */ new Set([opts.start]);
|
|
125
|
+
const reached = [];
|
|
126
|
+
const traversedEdges = [];
|
|
127
|
+
const seenEdgeIds = /* @__PURE__ */ new Set();
|
|
128
|
+
let frontier = [opts.start];
|
|
129
|
+
for (let hop = 0; hop < opts.depth && frontier.length > 0; hop++) {
|
|
130
|
+
const edges = filterByTypes(
|
|
131
|
+
await frontierEdges(store, collection, frontier, direction, opts.edgeFilter),
|
|
132
|
+
opts.edgeTypes
|
|
133
|
+
);
|
|
134
|
+
const next = [];
|
|
135
|
+
for (const node of frontier) {
|
|
136
|
+
for (const edge of edges) {
|
|
137
|
+
const adjacent = direction === "out" ? edge.from === node : direction === "in" ? edge.to === node : edge.from === node || edge.to === node;
|
|
138
|
+
if (!adjacent) continue;
|
|
139
|
+
if (!seenEdgeIds.has(edge.id)) {
|
|
140
|
+
seenEdgeIds.add(edge.id);
|
|
141
|
+
traversedEdges.push(edge);
|
|
142
|
+
}
|
|
143
|
+
const neighbor = edge.from === node ? edge.to : edge.from;
|
|
144
|
+
if (!visited.has(neighbor)) {
|
|
145
|
+
visited.add(neighbor);
|
|
146
|
+
reached.push(neighbor);
|
|
147
|
+
next.push(neighbor);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
frontier = next;
|
|
152
|
+
}
|
|
153
|
+
reached.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
154
|
+
traversedEdges.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
155
|
+
return { nodes: reached, edges: traversedEdges };
|
|
156
|
+
}
|
|
157
|
+
export {
|
|
158
|
+
neighbors,
|
|
159
|
+
traverse,
|
|
160
|
+
traverseFrontierBatched
|
|
161
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export { A as AsyncStore, S as StoreFilter,
|
|
1
|
+
export { A as AsyncStore, a as AsyncStoreInQuery, S as StoreFilter, b as StoreMeta, c as SyncStore, d as SyncStoreInQuery } from './types-DHLdLZ_W.js';
|
|
2
2
|
export { InMemoryKv, toAsync } from './kv.js';
|
|
3
|
-
export { V as Vector, a as VectorDocument, b as VectorSearchOptions, c as VectorSearchResult, d as VectorStore, e as VectorStoreMeta } from './types-
|
|
4
|
-
export { InMemoryVectorStore, assertDimensions, bufferToVector, cosineSimilarity, isUsableVector, vectorToBuffer } from './vector.js';
|
|
3
|
+
export { A as AsyncVectorStore, V as Vector, a as VectorDocument, b as VectorSearchOptions, c as VectorSearchResult, d as VectorStore, e as VectorStoreMeta } from './types-B0XrD10b.js';
|
|
4
|
+
export { InMemoryVectorStore, assertDimensions, bufferToVector, cosineSimilarity, isUsableVector, toAsyncVector, vectorToBuffer } from './vector.js';
|
|
5
|
+
export { m as matchesWhere } from './filter-B9hP-TKF.js';
|
|
6
|
+
export { S as SearchDocument, a as SearchOptions, b as SearchResult, c as SearchStore } from './types-BLZTMsQO.js';
|
|
7
|
+
export { InMemorySearchStore, sanitizeFtsQuery, tokenize } from './search.js';
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
InMemoryStore,
|
|
3
3
|
toAsync
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DKCPULXT.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
} from "./chunk-
|
|
6
|
+
InMemorySearchStore
|
|
7
|
+
} from "./chunk-77IIKHQW.js";
|
|
8
|
+
import {
|
|
9
|
+
InMemoryVectorStore,
|
|
10
|
+
toAsyncVector
|
|
11
|
+
} from "./chunk-OUY3D4RT.js";
|
|
12
|
+
import {
|
|
13
|
+
sanitizeFtsQuery,
|
|
14
|
+
tokenize
|
|
15
|
+
} from "./chunk-N27Y55CA.js";
|
|
8
16
|
import {
|
|
9
17
|
assertDimensions,
|
|
10
18
|
bufferToVector,
|
|
@@ -12,13 +20,21 @@ import {
|
|
|
12
20
|
isUsableVector,
|
|
13
21
|
vectorToBuffer
|
|
14
22
|
} from "./chunk-BXM3YDOC.js";
|
|
23
|
+
import {
|
|
24
|
+
matchesWhere
|
|
25
|
+
} from "./chunk-KPMRRYTL.js";
|
|
15
26
|
export {
|
|
16
27
|
InMemoryStore as InMemoryKv,
|
|
28
|
+
InMemorySearchStore,
|
|
17
29
|
InMemoryVectorStore,
|
|
18
30
|
assertDimensions,
|
|
19
31
|
bufferToVector,
|
|
20
32
|
cosineSimilarity,
|
|
21
33
|
isUsableVector,
|
|
34
|
+
matchesWhere,
|
|
35
|
+
sanitizeFtsQuery,
|
|
22
36
|
toAsync,
|
|
37
|
+
toAsyncVector,
|
|
38
|
+
tokenize,
|
|
23
39
|
vectorToBuffer
|
|
24
40
|
};
|
package/dist/kv.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as SyncStore, d as SyncStoreInQuery, A as AsyncStore, a as AsyncStoreInQuery, b as StoreMeta, S as StoreFilter } from './types-DHLdLZ_W.js';
|
|
2
2
|
|
|
3
3
|
/** Lift a synchronous store to the {@link AsyncStore} interface. The bridge only
|
|
4
|
-
* goes this direction — sync ⊂ async. */
|
|
4
|
+
* goes this direction — sync ⊂ async. Optional sync capabilities are lifted too. */
|
|
5
|
+
declare function toAsync(store: SyncStore & SyncStoreInQuery): AsyncStore & AsyncStoreInQuery;
|
|
5
6
|
declare function toAsync(store: SyncStore): AsyncStore;
|
|
6
7
|
|
|
7
|
-
declare class InMemoryStore implements SyncStore {
|
|
8
|
+
declare class InMemoryStore implements SyncStore, SyncStoreInQuery {
|
|
8
9
|
readonly meta: StoreMeta;
|
|
9
10
|
/** Key-value storage. */
|
|
10
11
|
private kv;
|
|
@@ -17,6 +18,7 @@ declare class InMemoryStore implements SyncStore {
|
|
|
17
18
|
keys(prefix?: string): string[];
|
|
18
19
|
private ensureCollection;
|
|
19
20
|
list<T>(collection: string, filter?: StoreFilter): T[];
|
|
21
|
+
listWhereIn<T>(collection: string, field: string, values: readonly unknown[], filter?: StoreFilter): T[];
|
|
20
22
|
getById<T>(collection: string, id: string): T | null;
|
|
21
23
|
put<T extends {
|
|
22
24
|
id: string;
|
|
@@ -25,4 +27,4 @@ declare class InMemoryStore implements SyncStore {
|
|
|
25
27
|
count(collection: string, filter?: StoreFilter): number;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
export { AsyncStore, InMemoryStore as InMemoryKv, StoreFilter, StoreMeta, SyncStore, toAsync };
|
|
30
|
+
export { AsyncStore, AsyncStoreInQuery, InMemoryStore as InMemoryKv, StoreFilter, StoreMeta, SyncStore, SyncStoreInQuery, toAsync };
|
package/dist/kv.js
CHANGED
package/dist/search.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { c as SearchStore, S as SearchDocument, a as SearchOptions, b as SearchResult } from './types-BLZTMsQO.js';
|
|
2
|
+
export { d as SearchFieldDocument, e as SearchTextDocument } from './types-BLZTMsQO.js';
|
|
3
|
+
export { m as matchesWhere } from './filter-B9hP-TKF.js';
|
|
4
|
+
import './types-DHLdLZ_W.js';
|
|
5
|
+
|
|
6
|
+
declare class InMemorySearchStore implements SearchStore {
|
|
7
|
+
private readonly collections;
|
|
8
|
+
index<M extends Record<string, unknown> = Record<string, unknown>>(collection: string, doc: SearchDocument<M>): void;
|
|
9
|
+
indexMany<M extends Record<string, unknown> = Record<string, unknown>>(collection: string, docs: ReadonlyArray<SearchDocument<M>>): void;
|
|
10
|
+
remove(collection: string, id: string): boolean;
|
|
11
|
+
search<M extends Record<string, unknown> = Record<string, unknown>>(collection: string, query: string, opts?: SearchOptions): SearchResult<M>[];
|
|
12
|
+
private removeFromColl;
|
|
13
|
+
private collectionFor;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Tokenize `text` into lowercase unicode word tokens. A reasonable FTS5
|
|
17
|
+
* `unicode61`-ish split: letters and digits are token characters, everything
|
|
18
|
+
* else is a separator. */
|
|
19
|
+
declare function tokenize(text: string): string[];
|
|
20
|
+
/** Sanitize a raw query into an FTS5 MATCH expression: tokenize, double-quote
|
|
21
|
+
* each token (doubling embedded quotes), OR them. Mirrors the knowledge/session
|
|
22
|
+
* index sanitizers so user punctuation / operators never throw. Returns "" for
|
|
23
|
+
* an empty/whitespace query — callers treat that as "no results". */
|
|
24
|
+
declare function sanitizeFtsQuery(q: string): string;
|
|
25
|
+
|
|
26
|
+
export { InMemorySearchStore, SearchDocument, SearchOptions, SearchResult, SearchStore, sanitizeFtsQuery, tokenize };
|
package/dist/search.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InMemorySearchStore
|
|
3
|
+
} from "./chunk-77IIKHQW.js";
|
|
4
|
+
import {
|
|
5
|
+
sanitizeFtsQuery,
|
|
6
|
+
tokenize
|
|
7
|
+
} from "./chunk-N27Y55CA.js";
|
|
8
|
+
import {
|
|
9
|
+
matchesWhere
|
|
10
|
+
} from "./chunk-KPMRRYTL.js";
|
|
11
|
+
export {
|
|
12
|
+
InMemorySearchStore,
|
|
13
|
+
matchesWhere,
|
|
14
|
+
sanitizeFtsQuery,
|
|
15
|
+
tokenize
|
|
16
|
+
};
|
package/dist/sql.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { S as StoreFilter } from './types-DHLdLZ_W.js';
|
|
2
|
+
|
|
3
|
+
/** A bound SQL parameter — the common subset both better-sqlite3 and @libsql/client
|
|
4
|
+
* accept. Booleans are pre-converted to 0/1 by the builders (better-sqlite3 rejects
|
|
5
|
+
* a raw boolean), so they never appear here. */
|
|
6
|
+
type SqlParam = string | number | bigint | null;
|
|
7
|
+
/** A field name is ONE top-level JSON key, never a nested path. Build the JSON path
|
|
8
|
+
* `$."field"` (with `"` in the field doubled per SQLite JSON-path quoting) so a
|
|
9
|
+
* dotted name (`"a.b"`) resolves to the single top-level key `a.b`, matching the
|
|
10
|
+
* in-memory reference's `record[key]` lookup — not the nested path `$.a.b`. Returned
|
|
11
|
+
* as a value to BIND, never interpolated into SQL: field names are caller-supplied,
|
|
12
|
+
* so inlining them would be a SQL-injection vector. */
|
|
13
|
+
declare function jsonPath(field: string): string;
|
|
14
|
+
declare function buildWhereClause(filter?: StoreFilter): {
|
|
15
|
+
clause: string;
|
|
16
|
+
params: SqlParam[];
|
|
17
|
+
};
|
|
18
|
+
declare function buildOrderBy(filter?: StoreFilter): {
|
|
19
|
+
clause: string;
|
|
20
|
+
params: SqlParam[];
|
|
21
|
+
};
|
|
22
|
+
declare function buildLimitOffset(filter?: StoreFilter): string;
|
|
23
|
+
/** Deterministic 32-bit FNV-1a hash → base36. Used to make collection table names
|
|
24
|
+
* injective (distinct collection names never alias to one physical table). */
|
|
25
|
+
declare function hashName(s: string): string;
|
|
26
|
+
|
|
27
|
+
export { type SqlParam, buildLimitOffset, buildOrderBy, buildWhereClause, hashName, jsonPath };
|