@voltro/plugin-search 0.84.0 → 0.86.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.
- package/CHANGELOG.md +1215 -0
- package/THIRD-PARTY-NOTICES.md +6 -90
- package/dist/backend.js +398 -0
- package/dist/drift.js +121 -0
- package/dist/errors.js +13 -0
- package/dist/index.d.ts +8 -12
- package/dist/index.js +173 -1017
- package/dist/memoryGuard.js +38 -0
- package/dist/rpc.d.ts +32 -35
- package/dist/rpc.js +57 -2
- package/dist/sanitize.js +122 -0
- package/dist/statsBuffer.js +36 -0
- package/dist/statsStore.js +163 -0
- package/dist/web.js +1 -1
- package/package.json +5 -5
- package/dist/rpc-CpryHBcM.js +0 -62
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/memoryGuard.ts
|
|
2
|
+
var e = (e) => e === void 0 || e === "memory" ? "memory" : typeof e.query == "function" ? e.engineKind === "memory" ? "memory" : "custom" : "vendor", t = class extends Error {
|
|
3
|
+
_tag = "SearchBackendNotDurable";
|
|
4
|
+
constructor(e) {
|
|
5
|
+
super(e), this.name = "SearchBackendNotDurable";
|
|
6
|
+
}
|
|
7
|
+
}, n = [
|
|
8
|
+
"plugin-search refuses to boot in production on the in-memory backend.",
|
|
9
|
+
"",
|
|
10
|
+
"The memory index lives in THIS process's heap. Two consequences, both silent:",
|
|
11
|
+
" • every replica holds a different index, so a result depends on which replica served you;",
|
|
12
|
+
" • the index starts EMPTY after every restart/deploy, and nothing re-seeds it automatically.",
|
|
13
|
+
"",
|
|
14
|
+
"Configure a durable engine in app.config.ts:",
|
|
15
|
+
" searchPlugin({ backend: { engine: 'typesense', url: …, apiKey: … }, indexes })",
|
|
16
|
+
" searchPlugin({ backend: { engine: 'meilisearch', url: …, apiKey: … }, indexes })",
|
|
17
|
+
" searchPlugin({ backend: { engine: 'algolia', appId: …, apiKey: … }, indexes })",
|
|
18
|
+
"or pass your own `SearchBackend` implementation.",
|
|
19
|
+
"",
|
|
20
|
+
"If this deployment really is ONE process that re-seeds its index at startup",
|
|
21
|
+
"(a `*.startup.tsx` calling `backfillIndex`), say so explicitly:",
|
|
22
|
+
" searchPlugin({ singleProcessMemoryIndex: true, indexes })",
|
|
23
|
+
"That is a claim about your topology, not a mute switch: if a second replica",
|
|
24
|
+
"ever announces itself, the plugin logs that the claim was contradicted."
|
|
25
|
+
].join("\n"), r = [
|
|
26
|
+
"plugin-search is running the in-memory backend in production under",
|
|
27
|
+
"`singleProcessMemoryIndex: true`. Holding you to the two conditions that makes true:",
|
|
28
|
+
"exactly ONE process serves search, and it re-seeds every index at startup",
|
|
29
|
+
"(`backfillIndex`) — the index is empty after each restart until it does."
|
|
30
|
+
].join(" "), i = (e) => e.backendKind !== "memory" || !e.production ? { level: "quiet" } : e.singleProcessDeclared ? {
|
|
31
|
+
level: "note",
|
|
32
|
+
message: r
|
|
33
|
+
} : {
|
|
34
|
+
level: "refuse",
|
|
35
|
+
message: n
|
|
36
|
+
}, a = (e, t) => t ? `search: replica "${e}" joined, but this app declared \`singleProcessMemoryIndex: true\`. That declaration is now false: each replica has its own in-memory index, so search results depend on which one serves the request. Configure a durable backend (typesense / meilisearch / algolia) or run one process.` : `search: replica "${e}" joined while search is served from an in-memory index. Each replica indexes only its OWN writes, so results depend on which replica answers. Configure a durable backend (typesense / meilisearch / algolia) before relying on these results.`;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { t as SearchBackendNotDurable, i as auditMemoryBackend, e as backendKindOf, a as peerObservedWarning };
|
package/dist/rpc.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ActionProcedureDescriptor } from '@voltro/protocol';
|
|
2
2
|
import { PluginRpcClientDescriptor } from '@voltro/protocol';
|
|
3
3
|
import { Schema } from 'effect';
|
|
4
|
+
import { YieldableError } from 'effect/Cause';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The plugin's canonical package name — the key an app's `alias` is recorded
|
|
@@ -18,43 +19,43 @@ export declare const queryDescriptor: ActionProcedureDescriptor<"search.query",
|
|
|
18
19
|
/** One of the indexes the app declared. An unregistered name is refused
|
|
19
20
|
* (`SearchIndexNotFound`) — it has no spec, so it has no tenant field to
|
|
20
21
|
* scope by, and answering it would query an unscoped collection. */
|
|
21
|
-
index:
|
|
22
|
-
q:
|
|
23
|
-
limit: Schema.optional<
|
|
24
|
-
offset: Schema.optional<
|
|
22
|
+
readonly index: Schema.String;
|
|
23
|
+
readonly q: Schema.String;
|
|
24
|
+
readonly limit: Schema.optional<Schema.Number>;
|
|
25
|
+
readonly offset: Schema.optional<Schema.Number>;
|
|
25
26
|
/** Clauses ANDed together — equality, range, negation, set membership. */
|
|
26
|
-
filters: Schema.optional<Schema
|
|
27
|
+
readonly filters: Schema.optional<Schema.$Array<Schema.Struct<{
|
|
27
28
|
/** A doc field path. Validated server-side against the index's declared
|
|
28
29
|
* `queryableFields` (or a plain-identifier pattern) before it is spliced
|
|
29
30
|
* into the engine's filter DSL — a mismatch is a typed
|
|
30
31
|
* `SearchFieldRejected`, not a query. */
|
|
31
|
-
field:
|
|
32
|
-
op: Schema.
|
|
33
|
-
value: Schema.Union<[Schema.Union<[
|
|
32
|
+
readonly field: Schema.String;
|
|
33
|
+
readonly op: Schema.Literals<readonly ["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin"]>;
|
|
34
|
+
readonly value: Schema.Union<readonly [Schema.Union<readonly [Schema.String, Schema.Number, Schema.Boolean]>, Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Number, Schema.Boolean]>>]>;
|
|
34
35
|
}>>>;
|
|
35
36
|
/** Facet-count fields. */
|
|
36
|
-
facets: Schema.optional<Schema
|
|
37
|
+
readonly facets: Schema.optional<Schema.$Array<Schema.String>>;
|
|
37
38
|
/** Matched-term highlighting. */
|
|
38
|
-
highlight: Schema.optional<Schema.Struct<{
|
|
39
|
-
fields: Schema.optional<Schema
|
|
40
|
-
preTag: Schema.optional<
|
|
41
|
-
postTag: Schema.optional<
|
|
39
|
+
readonly highlight: Schema.optional<Schema.Struct<{
|
|
40
|
+
readonly fields: Schema.optional<Schema.$Array<Schema.String>>;
|
|
41
|
+
readonly preTag: Schema.optional<Schema.String>;
|
|
42
|
+
readonly postTag: Schema.optional<Schema.String>;
|
|
42
43
|
}>>;
|
|
43
44
|
/** Typo tolerance — a max edit distance (0 = exact) or `'auto'`. */
|
|
44
|
-
fuzziness: Schema.optional<Schema.Union<[
|
|
45
|
+
readonly fuzziness: Schema.optional<Schema.Union<readonly [Schema.Number, Schema.Literal<"auto">]>>;
|
|
45
46
|
/** Escape hatch — the engine's presentation-only params (paging, ordering,
|
|
46
47
|
* typo tolerance, highlight shaping). Anything that could select a
|
|
47
48
|
* different document set is dropped server-side and logged; express a
|
|
48
49
|
* filter as a `filters[]` clause instead. */
|
|
49
|
-
engineParams: Schema.optional<Schema
|
|
50
|
+
readonly engineParams: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
|
|
50
51
|
}>, Schema.Struct<{
|
|
51
|
-
hits: Schema
|
|
52
|
-
doc: Schema
|
|
53
|
-
highlights: Schema.optional<Schema
|
|
52
|
+
readonly hits: Schema.$Array<Schema.Struct<{
|
|
53
|
+
readonly doc: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
54
|
+
readonly highlights: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
|
|
54
55
|
}>>;
|
|
55
56
|
/** field → (value → count) for the requested facets. */
|
|
56
|
-
facets: Schema
|
|
57
|
-
}>, Schema.Union<[typeof SearchIndexNotFound, typeof SearchFieldRejected]>>;
|
|
57
|
+
readonly facets: Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Number>>;
|
|
58
|
+
}>, Schema.Union<readonly [typeof SearchIndexNotFound, typeof SearchFieldRejected]>>;
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
61
|
* A field name the caller supplied — in `filters[].field`, `facets[]` or
|
|
@@ -72,22 +73,20 @@ export declare const queryDescriptor: ActionProcedureDescriptor<"search.query",
|
|
|
72
73
|
declare class SearchFieldRejected extends SearchFieldRejected_base {
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
declare const SearchFieldRejected_base: Schema.
|
|
76
|
-
readonly
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
reason: Schema.Literal<["not-an-identifier", "not-queryable"]>;
|
|
81
|
-
}>;
|
|
76
|
+
declare const SearchFieldRejected_base: Schema.Class<SearchFieldRejected, Schema.TaggedStruct<"SearchFieldRejected", {
|
|
77
|
+
readonly field: Schema.String;
|
|
78
|
+
readonly where: Schema.Literals<readonly ["filter", "facet", "highlight"]>;
|
|
79
|
+
readonly reason: Schema.Literals<readonly ["not-an-identifier", "not-queryable"]>;
|
|
80
|
+
}>, YieldableError>;
|
|
82
81
|
|
|
83
82
|
export declare const SearchFilterSchema: Schema.Struct<{
|
|
84
83
|
/** A doc field path. Validated server-side against the index's declared
|
|
85
84
|
* `queryableFields` (or a plain-identifier pattern) before it is spliced
|
|
86
85
|
* into the engine's filter DSL — a mismatch is a typed
|
|
87
86
|
* `SearchFieldRejected`, not a query. */
|
|
88
|
-
field:
|
|
89
|
-
op: Schema.
|
|
90
|
-
value: Schema.Union<[Schema.Union<[
|
|
87
|
+
readonly field: Schema.String;
|
|
88
|
+
readonly op: Schema.Literals<readonly ["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin"]>;
|
|
89
|
+
readonly value: Schema.Union<readonly [Schema.Union<readonly [Schema.String, Schema.Number, Schema.Boolean]>, Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Number, Schema.Boolean]>>]>;
|
|
91
90
|
}>;
|
|
92
91
|
|
|
93
92
|
/**
|
|
@@ -102,11 +101,9 @@ export declare const SearchFilterSchema: Schema.Struct<{
|
|
|
102
101
|
declare class SearchIndexNotFound extends SearchIndexNotFound_base {
|
|
103
102
|
}
|
|
104
103
|
|
|
105
|
-
declare const SearchIndexNotFound_base: Schema.
|
|
106
|
-
readonly
|
|
107
|
-
}
|
|
108
|
-
index: typeof Schema.String;
|
|
109
|
-
}>;
|
|
104
|
+
declare const SearchIndexNotFound_base: Schema.Class<SearchIndexNotFound, Schema.TaggedStruct<"SearchIndexNotFound", {
|
|
105
|
+
readonly index: Schema.String;
|
|
106
|
+
}>, YieldableError>;
|
|
110
107
|
|
|
111
108
|
/** Declared for the codegen (`VoltroPlugin.rpcClientDescriptors`) so each tag is
|
|
112
109
|
* emitted into the generated client group. Kept in lockstep with the exports. */
|
package/dist/rpc.js
CHANGED
|
@@ -1,2 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { SearchFieldRejected as e, SearchIndexNotFound as t } from "./errors.js";
|
|
2
|
+
import { Schema as n } from "effect";
|
|
3
|
+
import { defineAction as r } from "@voltro/protocol";
|
|
4
|
+
//#region src/rpc.ts
|
|
5
|
+
var i = "@voltro/plugin-search", a = n.Union([
|
|
6
|
+
n.String,
|
|
7
|
+
n.Number,
|
|
8
|
+
n.Boolean
|
|
9
|
+
]), o = n.Struct({
|
|
10
|
+
field: n.String,
|
|
11
|
+
op: n.Literals([
|
|
12
|
+
"eq",
|
|
13
|
+
"neq",
|
|
14
|
+
"gt",
|
|
15
|
+
"gte",
|
|
16
|
+
"lt",
|
|
17
|
+
"lte",
|
|
18
|
+
"in",
|
|
19
|
+
"nin"
|
|
20
|
+
]),
|
|
21
|
+
value: n.Union([a, n.Array(a)])
|
|
22
|
+
}), s = n.Struct({
|
|
23
|
+
fields: n.optional(n.Array(n.String)),
|
|
24
|
+
preTag: n.optional(n.String),
|
|
25
|
+
postTag: n.optional(n.String)
|
|
26
|
+
}), c = n.Struct({
|
|
27
|
+
doc: n.Record(n.String, n.Unknown),
|
|
28
|
+
highlights: n.optional(n.Record(n.String, n.String))
|
|
29
|
+
}), l = r({
|
|
30
|
+
name: "search.query",
|
|
31
|
+
openAccess: "tenant scoping, index registration and field validation are enforced server-side per call; a non-tenant index exposes only what the app's map(row) deliberately indexed",
|
|
32
|
+
input: n.Struct({
|
|
33
|
+
index: n.String,
|
|
34
|
+
q: n.String,
|
|
35
|
+
limit: n.optional(n.Number),
|
|
36
|
+
offset: n.optional(n.Number),
|
|
37
|
+
filters: n.optional(n.Array(o)),
|
|
38
|
+
facets: n.optional(n.Array(n.String)),
|
|
39
|
+
highlight: n.optional(s),
|
|
40
|
+
fuzziness: n.optional(n.Union([n.Number, n.Literal("auto")])),
|
|
41
|
+
engineParams: n.optional(n.Record(n.String, n.Unknown))
|
|
42
|
+
}),
|
|
43
|
+
output: n.Struct({
|
|
44
|
+
hits: n.Array(c),
|
|
45
|
+
facets: n.Record(n.String, n.Record(n.String, n.Number))
|
|
46
|
+
}),
|
|
47
|
+
error: n.Union([t, e])
|
|
48
|
+
}), u = [{
|
|
49
|
+
tag: "search.query",
|
|
50
|
+
kind: "action",
|
|
51
|
+
import: {
|
|
52
|
+
module: "@voltro/plugin-search/rpc",
|
|
53
|
+
name: "queryDescriptor"
|
|
54
|
+
}
|
|
55
|
+
}];
|
|
56
|
+
//#endregion
|
|
57
|
+
export { i as BASE_NAME, o as SearchFilterSchema, l as queryDescriptor, u as searchRpcClientImports };
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//#region src/sanitize.ts
|
|
2
|
+
var e = /^[A-Za-z_][A-Za-z0-9_.]*$/, t = (t) => e.test(t), n = (e, n) => {
|
|
3
|
+
let r = n ? new Set(n) : void 0;
|
|
4
|
+
for (let [n, i] of e) {
|
|
5
|
+
if (!t(i)) return {
|
|
6
|
+
field: i,
|
|
7
|
+
where: n,
|
|
8
|
+
reason: "not-an-identifier"
|
|
9
|
+
};
|
|
10
|
+
if (r && !r.has(i)) return {
|
|
11
|
+
field: i,
|
|
12
|
+
where: n,
|
|
13
|
+
reason: "not-queryable"
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}, r = {
|
|
17
|
+
typesense: [
|
|
18
|
+
"per_page",
|
|
19
|
+
"page",
|
|
20
|
+
"offset",
|
|
21
|
+
"limit",
|
|
22
|
+
"sort_by",
|
|
23
|
+
"num_typos",
|
|
24
|
+
"typo_tokens_threshold",
|
|
25
|
+
"drop_tokens_threshold",
|
|
26
|
+
"prefix",
|
|
27
|
+
"prioritize_exact_match",
|
|
28
|
+
"highlight_start_tag",
|
|
29
|
+
"highlight_end_tag",
|
|
30
|
+
"highlight_affix_num_tokens",
|
|
31
|
+
"snippet_threshold",
|
|
32
|
+
"max_facet_values"
|
|
33
|
+
],
|
|
34
|
+
meilisearch: [
|
|
35
|
+
"limit",
|
|
36
|
+
"offset",
|
|
37
|
+
"page",
|
|
38
|
+
"hitsPerPage",
|
|
39
|
+
"sort",
|
|
40
|
+
"matchingStrategy",
|
|
41
|
+
"showRankingScore",
|
|
42
|
+
"rankingScoreThreshold",
|
|
43
|
+
"distinct",
|
|
44
|
+
"attributesToHighlight",
|
|
45
|
+
"highlightPreTag",
|
|
46
|
+
"highlightPostTag",
|
|
47
|
+
"attributesToCrop",
|
|
48
|
+
"cropLength",
|
|
49
|
+
"cropMarker",
|
|
50
|
+
"showMatchesPosition"
|
|
51
|
+
],
|
|
52
|
+
algolia: [
|
|
53
|
+
"hitsPerPage",
|
|
54
|
+
"page",
|
|
55
|
+
"offset",
|
|
56
|
+
"length",
|
|
57
|
+
"typoTolerance",
|
|
58
|
+
"minWordSizefor1Typo",
|
|
59
|
+
"minWordSizefor2Typos",
|
|
60
|
+
"ignorePlurals",
|
|
61
|
+
"removeStopWords",
|
|
62
|
+
"queryType",
|
|
63
|
+
"distinct",
|
|
64
|
+
"attributesToHighlight",
|
|
65
|
+
"attributesToSnippet",
|
|
66
|
+
"snippetEllipsisText",
|
|
67
|
+
"highlightPreTag",
|
|
68
|
+
"highlightPostTag"
|
|
69
|
+
]
|
|
70
|
+
}, i = {
|
|
71
|
+
typesense: [
|
|
72
|
+
"filter_by",
|
|
73
|
+
"q",
|
|
74
|
+
"preset",
|
|
75
|
+
"pinned_hits",
|
|
76
|
+
"hidden_hits",
|
|
77
|
+
"enable_overrides",
|
|
78
|
+
"override_tags",
|
|
79
|
+
"include_fields",
|
|
80
|
+
"exclude_fields",
|
|
81
|
+
"vector_query"
|
|
82
|
+
],
|
|
83
|
+
meilisearch: [
|
|
84
|
+
"filter",
|
|
85
|
+
"q",
|
|
86
|
+
"attributesToSearchOn",
|
|
87
|
+
"attributesToRetrieve",
|
|
88
|
+
"hybrid",
|
|
89
|
+
"vector",
|
|
90
|
+
"federation"
|
|
91
|
+
],
|
|
92
|
+
algolia: [
|
|
93
|
+
"filters",
|
|
94
|
+
"facetFilters",
|
|
95
|
+
"numericFilters",
|
|
96
|
+
"tagFilters",
|
|
97
|
+
"optionalFilters",
|
|
98
|
+
"query",
|
|
99
|
+
"restrictSearchableAttributes",
|
|
100
|
+
"attributesToRetrieve",
|
|
101
|
+
"enableRules",
|
|
102
|
+
"ruleContexts",
|
|
103
|
+
"similarQuery",
|
|
104
|
+
"aroundLatLng",
|
|
105
|
+
"insideBoundingBox"
|
|
106
|
+
]
|
|
107
|
+
}, a = (e, t, n) => {
|
|
108
|
+
if (!t) return {
|
|
109
|
+
params: {},
|
|
110
|
+
appAllowed: {},
|
|
111
|
+
dropped: []
|
|
112
|
+
};
|
|
113
|
+
let a = new Set(i[e]), o = new Set(r[e]), s = new Set(n ?? []), c = {}, l = {}, u = [];
|
|
114
|
+
for (let [e, n] of Object.entries(t)) a.has(e) ? u.push(e) : o.has(e) ? c[e] = n : s.has(e) ? l[e] = n : u.push(e);
|
|
115
|
+
return {
|
|
116
|
+
params: c,
|
|
117
|
+
appAllowed: l,
|
|
118
|
+
dropped: u
|
|
119
|
+
};
|
|
120
|
+
}, o = (e) => r[e];
|
|
121
|
+
//#endregion
|
|
122
|
+
export { e as FIELD_NAME_PATTERN, o as allowedEngineParams, n as firstRejectedField, t as isSafeFieldName, a as sanitizeEngineParams };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/statsBuffer.ts
|
|
2
|
+
var e = (e) => {
|
|
3
|
+
let t = /* @__PURE__ */ new Map(), n = 0, r = !1, i = async () => {
|
|
4
|
+
if (t.size === 0) return;
|
|
5
|
+
let r = t;
|
|
6
|
+
t = /* @__PURE__ */ new Map(), n = 0;
|
|
7
|
+
for (let [t, n] of r) try {
|
|
8
|
+
await e.flush(t, {
|
|
9
|
+
synced: n.synced,
|
|
10
|
+
removed: n.removed,
|
|
11
|
+
dropped: n.dropped
|
|
12
|
+
}, n.scope);
|
|
13
|
+
} catch {}
|
|
14
|
+
}, a = setInterval(() => {
|
|
15
|
+
i();
|
|
16
|
+
}, Math.max(1, e.intervalMs));
|
|
17
|
+
return a.unref?.(), {
|
|
18
|
+
bump: (a, o, s) => {
|
|
19
|
+
if (r) return;
|
|
20
|
+
let c = t.get(a);
|
|
21
|
+
c === void 0 ? t.set(a, {
|
|
22
|
+
synced: +(o === "synced"),
|
|
23
|
+
removed: +(o === "removed"),
|
|
24
|
+
dropped: +(o === "dropped"),
|
|
25
|
+
scope: s
|
|
26
|
+
}) : (o === "synced" ? c.synced += 1 : o === "removed" ? c.removed += 1 : c.dropped += 1, c.scope = s), n += 1, n >= e.maxBuffered && i();
|
|
27
|
+
},
|
|
28
|
+
flushNow: i,
|
|
29
|
+
shutdown: async () => {
|
|
30
|
+
r || (r = !0, clearInterval(a), await i());
|
|
31
|
+
},
|
|
32
|
+
pending: () => n
|
|
33
|
+
};
|
|
34
|
+
}, t = (e) => (t, n, r) => e().add(t, n, r);
|
|
35
|
+
//#endregion
|
|
36
|
+
export { e as makeSearchStatsBuffer, t as statsStoreFlush };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { SearchBackendError as e } from "./backend.js";
|
|
2
|
+
import { and as t, eq as n, id as r, integer as i, table as a, text as o, timestamp as s } from "@voltro/database";
|
|
3
|
+
import { replicaId as c } from "@voltro/protocol/identity";
|
|
4
|
+
//#region src/statsStore.ts
|
|
5
|
+
var l = "_voltro_search_stats", u = () => ({
|
|
6
|
+
synced: 0,
|
|
7
|
+
removed: 0,
|
|
8
|
+
dropped: 0,
|
|
9
|
+
lastReindexAt: null,
|
|
10
|
+
lastReindexCount: 0
|
|
11
|
+
}), d = a(l, {
|
|
12
|
+
id: r({ prefix: "sstat" }),
|
|
13
|
+
indexName: o().maxLength(191),
|
|
14
|
+
replicaId: o().maxLength(191),
|
|
15
|
+
synced: i().default(0),
|
|
16
|
+
removed: i().default(0),
|
|
17
|
+
dropped: i().default(0),
|
|
18
|
+
scope: o().maxLength(8).default("local"),
|
|
19
|
+
lastReindexAt: s().nullable(),
|
|
20
|
+
lastReindexCount: i().default(0),
|
|
21
|
+
updatedAt: s()
|
|
22
|
+
}).unique("bySearchStatsIndexReplica", ["indexName", "replicaId"]).index("bySearchStatsIndex", ["indexName"]), f = () => [d], p = () => {
|
|
23
|
+
let e = /* @__PURE__ */ new Map(), t = (t) => e.get(t) ?? u();
|
|
24
|
+
return {
|
|
25
|
+
bump: async (n, r) => {
|
|
26
|
+
let i = t(n);
|
|
27
|
+
e.set(n, r === "synced" ? {
|
|
28
|
+
...i,
|
|
29
|
+
synced: i.synced + 1
|
|
30
|
+
} : r === "removed" ? {
|
|
31
|
+
...i,
|
|
32
|
+
removed: i.removed + 1
|
|
33
|
+
} : {
|
|
34
|
+
...i,
|
|
35
|
+
dropped: i.dropped + 1
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
add: async (n, r) => {
|
|
39
|
+
let i = t(n);
|
|
40
|
+
e.set(n, {
|
|
41
|
+
...i,
|
|
42
|
+
synced: i.synced + r.synced,
|
|
43
|
+
removed: i.removed + r.removed,
|
|
44
|
+
dropped: i.dropped + r.dropped
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
recordReindex: async (n, r, i) => {
|
|
48
|
+
let a = t(n);
|
|
49
|
+
e.set(n, {
|
|
50
|
+
...a,
|
|
51
|
+
synced: a.synced + r,
|
|
52
|
+
lastReindexAt: i,
|
|
53
|
+
lastReindexCount: r
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
get: async (e) => t(e)
|
|
57
|
+
};
|
|
58
|
+
}, m = (e) => ({
|
|
59
|
+
table: l,
|
|
60
|
+
predicate: e,
|
|
61
|
+
order: [],
|
|
62
|
+
take: void 0,
|
|
63
|
+
skip: void 0,
|
|
64
|
+
projection: void 0
|
|
65
|
+
}), h = () => `sstat_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, g = () => c(), _ = 32, v = (r, i = g()) => {
|
|
66
|
+
let a = async (e) => (await r.query(m(t(n("indexName", e), n("replicaId", i)))))[0] ?? null, o = async (t) => {
|
|
67
|
+
try {
|
|
68
|
+
return await t();
|
|
69
|
+
} catch (t) {
|
|
70
|
+
throw t instanceof e ? t : new e({
|
|
71
|
+
engine: "stats",
|
|
72
|
+
op: "upsert",
|
|
73
|
+
message: String(t?.message ?? t),
|
|
74
|
+
transient: !0
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}, s = (s, c, u, d) => o(async () => {
|
|
78
|
+
for (let e = 0; e < _; e++) {
|
|
79
|
+
e > 0 && await new Promise((t) => setTimeout(t, e));
|
|
80
|
+
let o = await a(s);
|
|
81
|
+
if (o) {
|
|
82
|
+
let e = Number(o.synced ?? 0), i = Number(o.removed ?? 0), a = Number(o.dropped ?? 0), s = {
|
|
83
|
+
synced: e,
|
|
84
|
+
removed: i,
|
|
85
|
+
dropped: a,
|
|
86
|
+
lastReindexAt: o.lastReindexAt ? new Date(o.lastReindexAt).toISOString() : null,
|
|
87
|
+
lastReindexCount: Number(o.lastReindexCount ?? 0)
|
|
88
|
+
};
|
|
89
|
+
if (await r.updateMany("_voltro_search_stats", {
|
|
90
|
+
...u(s),
|
|
91
|
+
...c ? { scope: c } : {},
|
|
92
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
93
|
+
}, { where: t(n("id", String(o.id)), n("synced", e), n("removed", i), n("dropped", a)) }) === 1) return;
|
|
94
|
+
} else {
|
|
95
|
+
let e = h(), t = await r.insertIgnore(l, {
|
|
96
|
+
id: e,
|
|
97
|
+
indexName: s,
|
|
98
|
+
replicaId: i,
|
|
99
|
+
synced: 0,
|
|
100
|
+
removed: 0,
|
|
101
|
+
dropped: 0,
|
|
102
|
+
scope: c ?? "local",
|
|
103
|
+
lastReindexAt: null,
|
|
104
|
+
lastReindexCount: 0,
|
|
105
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
106
|
+
...d
|
|
107
|
+
}, { conflictColumns: ["indexName", "replicaId"] });
|
|
108
|
+
if (String(t.id) === e) return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
throw new e({
|
|
112
|
+
engine: "stats",
|
|
113
|
+
op: "upsert",
|
|
114
|
+
message: `search stats: contention on (${s}, ${i}) exceeded ${_} attempts`,
|
|
115
|
+
transient: !0
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
bump: (e, t, n) => s(e, n, (e) => t === "synced" ? { synced: e.synced + 1 } : t === "removed" ? { removed: e.removed + 1 } : { dropped: e.dropped + 1 }, t === "synced" ? { synced: 1 } : t === "removed" ? { removed: 1 } : { dropped: 1 }),
|
|
120
|
+
add: (e, t, n) => s(e, n, (e) => ({
|
|
121
|
+
synced: e.synced + t.synced,
|
|
122
|
+
removed: e.removed + t.removed,
|
|
123
|
+
dropped: e.dropped + t.dropped
|
|
124
|
+
}), {
|
|
125
|
+
synced: t.synced,
|
|
126
|
+
removed: t.removed,
|
|
127
|
+
dropped: t.dropped
|
|
128
|
+
}),
|
|
129
|
+
recordReindex: (e, t, n) => s(e, void 0, (e) => ({
|
|
130
|
+
synced: e.synced + t,
|
|
131
|
+
lastReindexAt: new Date(n),
|
|
132
|
+
lastReindexCount: t
|
|
133
|
+
}), {
|
|
134
|
+
synced: t,
|
|
135
|
+
lastReindexAt: new Date(n),
|
|
136
|
+
lastReindexCount: t
|
|
137
|
+
}),
|
|
138
|
+
get: (e) => o(async () => {
|
|
139
|
+
let t = await r.query(m(n("indexName", e))), i = t.some((e) => e.scope === "fleet"), a = (e) => {
|
|
140
|
+
let n = 0;
|
|
141
|
+
for (let r of t) {
|
|
142
|
+
let t = Number(r[e] ?? 0);
|
|
143
|
+
n = i ? Math.max(n, t) : n + t;
|
|
144
|
+
}
|
|
145
|
+
return n;
|
|
146
|
+
}, o = null, s = 0;
|
|
147
|
+
for (let e of t) {
|
|
148
|
+
let t = e.lastReindexAt ? new Date(e.lastReindexAt).toISOString() : null;
|
|
149
|
+
t && (o === null || t > o) && (o = t, s = Number(e.lastReindexCount ?? 0));
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
...u(),
|
|
153
|
+
synced: a("synced"),
|
|
154
|
+
removed: a("removed"),
|
|
155
|
+
dropped: a("dropped"),
|
|
156
|
+
lastReindexAt: o,
|
|
157
|
+
lastReindexCount: s
|
|
158
|
+
};
|
|
159
|
+
})
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
163
|
+
export { l as SEARCH_STATS_TABLE, v as dataStoreStatsStore, p as memoryStatsStore, d as searchStatsTable, f as searchTables, u as zeroStats };
|
package/dist/web.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.0",
|
|
4
4
|
"description": "Keep an external search index (Typesense / Meilisearch / Algolia) in sync with framework tables via the post-commit ChangeEvent tap, then query it — tenant-scoped, with a backfill path and a useSearch() hook.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"node": ">=24.0.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@voltro/client": "0.
|
|
47
|
-
"@voltro/database": "0.
|
|
48
|
-
"@voltro/protocol": "0.
|
|
46
|
+
"@voltro/client": "0.86.0",
|
|
47
|
+
"@voltro/database": "0.86.0",
|
|
48
|
+
"@voltro/protocol": "0.86.0"
|
|
49
49
|
},
|
|
50
50
|
"optionalDependencies": {
|
|
51
51
|
"algoliasearch": "^5.59.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"typesense": "^3.0.6"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"effect": "
|
|
56
|
+
"effect": "4.0.1"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
59
59
|
"access": "public"
|
package/dist/rpc-CpryHBcM.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { Schema as e } from "effect";
|
|
2
|
-
import { defineAction as t } from "@voltro/protocol";
|
|
3
|
-
//#region src/errors.ts
|
|
4
|
-
var n = class extends e.TaggedError()("SearchIndexNotFound", { index: e.String }) {}, r = class extends e.TaggedError()("SearchFieldRejected", {
|
|
5
|
-
field: e.String,
|
|
6
|
-
where: e.Literal("filter", "facet", "highlight"),
|
|
7
|
-
reason: e.Literal("not-an-identifier", "not-queryable")
|
|
8
|
-
}) {}, i = "@voltro/plugin-search", a = e.Union(e.String, e.Number, e.Boolean), o = e.Struct({
|
|
9
|
-
field: e.String,
|
|
10
|
-
op: e.Literal("eq", "neq", "gt", "gte", "lt", "lte", "in", "nin"),
|
|
11
|
-
value: e.Union(a, e.Array(a))
|
|
12
|
-
}), s = e.Struct({
|
|
13
|
-
fields: e.optional(e.Array(e.String)),
|
|
14
|
-
preTag: e.optional(e.String),
|
|
15
|
-
postTag: e.optional(e.String)
|
|
16
|
-
}), c = e.Struct({
|
|
17
|
-
doc: e.Record({
|
|
18
|
-
key: e.String,
|
|
19
|
-
value: e.Unknown
|
|
20
|
-
}),
|
|
21
|
-
highlights: e.optional(e.Record({
|
|
22
|
-
key: e.String,
|
|
23
|
-
value: e.String
|
|
24
|
-
}))
|
|
25
|
-
}), l = t({
|
|
26
|
-
name: "search.query",
|
|
27
|
-
openAccess: "tenant scoping, index registration and field validation are enforced server-side per call; a non-tenant index exposes only what the app's map(row) deliberately indexed",
|
|
28
|
-
input: e.Struct({
|
|
29
|
-
index: e.String,
|
|
30
|
-
q: e.String,
|
|
31
|
-
limit: e.optional(e.Number),
|
|
32
|
-
offset: e.optional(e.Number),
|
|
33
|
-
filters: e.optional(e.Array(o)),
|
|
34
|
-
facets: e.optional(e.Array(e.String)),
|
|
35
|
-
highlight: e.optional(s),
|
|
36
|
-
fuzziness: e.optional(e.Union(e.Number, e.Literal("auto"))),
|
|
37
|
-
engineParams: e.optional(e.Record({
|
|
38
|
-
key: e.String,
|
|
39
|
-
value: e.Unknown
|
|
40
|
-
}))
|
|
41
|
-
}),
|
|
42
|
-
output: e.Struct({
|
|
43
|
-
hits: e.Array(c),
|
|
44
|
-
facets: e.Record({
|
|
45
|
-
key: e.String,
|
|
46
|
-
value: e.Record({
|
|
47
|
-
key: e.String,
|
|
48
|
-
value: e.Number
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
}),
|
|
52
|
-
error: e.Union(n, r)
|
|
53
|
-
}), u = [{
|
|
54
|
-
tag: "search.query",
|
|
55
|
-
kind: "action",
|
|
56
|
-
import: {
|
|
57
|
-
module: "@voltro/plugin-search/rpc",
|
|
58
|
-
name: "queryDescriptor"
|
|
59
|
-
}
|
|
60
|
-
}];
|
|
61
|
-
//#endregion
|
|
62
|
-
export { r as a, u as i, o as n, n as o, l as r, i as t };
|