@voltro/plugin-search 0.33.0 → 0.35.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/dist/index.js CHANGED
@@ -1,9 +1,127 @@
1
- import { queryDescriptor as e, searchRpcClientImports as t } from "./rpc.js";
2
- import { Data as n, Effect as r } from "effect";
3
- import { definePlugin as i } from "@voltro/protocol";
4
- import { and as a, eq as o, id as s, integer as c, table as l, text as u, timestamp as d } from "@voltro/database";
5
- //#region src/backend.ts
6
- var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.isArray(e) ? e : [e], m = (e, t, n) => {
1
+ import { a as e, i as t, n, r } from "./rpc-DaYt-dP6.js";
2
+ import { Cause as i, Chunk as a, Data as o, Duration as s, Effect as c, Exit as l, Option as u, Schedule as d, Stream as f } from "effect";
3
+ import { and as p, count as m, eq as h, id as g, integer as _, streamTable as ee, table as v, text as y, timestamp as b } from "@voltro/database";
4
+ import { definePlugin as te, pluginInstanceName as ne } from "@voltro/protocol";
5
+ //#region src/sanitize.ts
6
+ var x = /^[A-Za-z_][A-Za-z0-9_.]*$/, S = (e) => x.test(e), re = (e, t) => {
7
+ let n = t ? new Set(t) : void 0;
8
+ for (let [t, r] of e) {
9
+ if (!S(r)) return {
10
+ field: r,
11
+ where: t,
12
+ reason: "not-an-identifier"
13
+ };
14
+ if (n && !n.has(r)) return {
15
+ field: r,
16
+ where: t,
17
+ reason: "not-queryable"
18
+ };
19
+ }
20
+ }, C = {
21
+ typesense: [
22
+ "per_page",
23
+ "page",
24
+ "offset",
25
+ "limit",
26
+ "sort_by",
27
+ "num_typos",
28
+ "typo_tokens_threshold",
29
+ "drop_tokens_threshold",
30
+ "prefix",
31
+ "prioritize_exact_match",
32
+ "highlight_start_tag",
33
+ "highlight_end_tag",
34
+ "highlight_affix_num_tokens",
35
+ "snippet_threshold",
36
+ "max_facet_values"
37
+ ],
38
+ meilisearch: [
39
+ "limit",
40
+ "offset",
41
+ "page",
42
+ "hitsPerPage",
43
+ "sort",
44
+ "matchingStrategy",
45
+ "showRankingScore",
46
+ "rankingScoreThreshold",
47
+ "distinct",
48
+ "attributesToHighlight",
49
+ "highlightPreTag",
50
+ "highlightPostTag",
51
+ "attributesToCrop",
52
+ "cropLength",
53
+ "cropMarker",
54
+ "showMatchesPosition"
55
+ ],
56
+ algolia: [
57
+ "hitsPerPage",
58
+ "page",
59
+ "offset",
60
+ "length",
61
+ "typoTolerance",
62
+ "minWordSizefor1Typo",
63
+ "minWordSizefor2Typos",
64
+ "ignorePlurals",
65
+ "removeStopWords",
66
+ "queryType",
67
+ "distinct",
68
+ "attributesToHighlight",
69
+ "attributesToSnippet",
70
+ "snippetEllipsisText",
71
+ "highlightPreTag",
72
+ "highlightPostTag"
73
+ ]
74
+ }, ie = {
75
+ typesense: [
76
+ "filter_by",
77
+ "q",
78
+ "preset",
79
+ "pinned_hits",
80
+ "hidden_hits",
81
+ "enable_overrides",
82
+ "override_tags",
83
+ "include_fields",
84
+ "exclude_fields",
85
+ "vector_query"
86
+ ],
87
+ meilisearch: [
88
+ "filter",
89
+ "q",
90
+ "attributesToSearchOn",
91
+ "attributesToRetrieve",
92
+ "hybrid",
93
+ "vector",
94
+ "federation"
95
+ ],
96
+ algolia: [
97
+ "filters",
98
+ "facetFilters",
99
+ "numericFilters",
100
+ "tagFilters",
101
+ "optionalFilters",
102
+ "query",
103
+ "restrictSearchableAttributes",
104
+ "attributesToRetrieve",
105
+ "enableRules",
106
+ "ruleContexts",
107
+ "similarQuery",
108
+ "aroundLatLng",
109
+ "insideBoundingBox"
110
+ ]
111
+ }, w = (e, t, n) => {
112
+ if (!t) return {
113
+ params: {},
114
+ appAllowed: {},
115
+ dropped: []
116
+ };
117
+ let r = new Set(ie[e]), i = new Set(C[e]), a = new Set(n ?? []), o = {}, s = {}, c = [];
118
+ for (let [e, n] of Object.entries(t)) r.has(e) ? c.push(e) : i.has(e) ? o[e] = n : a.has(e) ? s[e] = n : c.push(e);
119
+ return {
120
+ params: o,
121
+ appAllowed: s,
122
+ dropped: c
123
+ };
124
+ }, T = (e) => C[e], E = class extends o.TaggedError("SearchBackendError") {}, D = (e) => Array.isArray(e) ? e : [e], O = (e, t, n) => {
7
125
  if (Math.abs(e.length - t.length) > n) return !1;
8
126
  if (e === t) return !0;
9
127
  let r = t.length, i = Array.from({ length: r + 1 }, (e, t) => t);
@@ -19,7 +137,7 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
19
137
  i = o;
20
138
  }
21
139
  return (i[r] ?? Infinity) <= n;
22
- }, h = (e) => e.length <= 4 ? 1 : 2, g = () => {
140
+ }, k = (e) => e.length <= 4 ? 1 : 2, A = () => {
23
141
  let e = /* @__PURE__ */ new Map(), t = (t) => {
24
142
  let n = e.get(t);
25
143
  return n || (n = /* @__PURE__ */ new Map(), e.set(t, n)), n;
@@ -28,13 +146,13 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
28
146
  switch (t.op) {
29
147
  case "eq": return String(n) === String(t.value);
30
148
  case "neq": return String(n) !== String(t.value);
31
- case "in": return p(t.value).some((e) => String(n) === String(e));
32
- case "nin": return p(t.value).every((e) => String(n) !== String(e));
149
+ case "in": return D(t.value).some((e) => String(n) === String(e));
150
+ case "nin": return D(t.value).every((e) => String(n) !== String(e));
33
151
  case "gt":
34
152
  case "gte":
35
153
  case "lt":
36
154
  case "lte": {
37
- let [e, r] = [_(n), _(t.value)];
155
+ let [e, r] = [j(n), j(t.value)];
38
156
  return e === void 0 || r === void 0 ? !1 : t.op === "gt" ? e > r : t.op === "gte" ? e >= r : t.op === "lt" ? e < r : e <= r;
39
157
  }
40
158
  }
@@ -44,8 +162,8 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
44
162
  let e = r.toLowerCase();
45
163
  if (e.includes(t)) return !0;
46
164
  if (n !== void 0 && n !== 0) {
47
- let r = n === "auto" ? h(t) : n;
48
- for (let n of e.split(/\s+/)) if (m(n, t, r)) return !0;
165
+ let r = n === "auto" ? k(t) : n;
166
+ for (let n of e.split(/\s+/)) if (O(n, t, r)) return !0;
49
167
  }
50
168
  }
51
169
  return !1;
@@ -56,6 +174,7 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
56
174
  return s;
57
175
  };
58
176
  return {
177
+ engineKind: "memory",
59
178
  upsert: async (e, n) => {
60
179
  let r = t(e);
61
180
  for (let e of n) r.set(e.id, e);
@@ -88,7 +207,7 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
88
207
  };
89
208
  }
90
209
  };
91
- }, _ = (e) => {
210
+ }, j = (e) => {
92
211
  if (typeof e == "number") return e;
93
212
  if (typeof e == "boolean") return +!!e;
94
213
  if (typeof e == "string") {
@@ -97,41 +216,58 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
97
216
  let n = Date.parse(e);
98
217
  return Number.isNaN(n) ? void 0 : n;
99
218
  }
100
- }, v = async (e, t, n) => {
219
+ }, M = async (e, t, n) => {
101
220
  try {
102
221
  return await n();
103
222
  } catch {
104
- throw new f({
223
+ throw new E({
105
224
  engine: t,
106
225
  op: "query",
107
226
  message: `the '${t}' search backend requires the '${e}' optional dependency. Install it to enable it (e.g. \`npm i ${e}\`).`,
108
227
  transient: !1
109
228
  });
110
229
  }
111
- }, y = (e) => {
112
- let t = (e) => JSON.stringify(e);
230
+ }, N = (e, t) => (n) => {
231
+ let { params: r, appAllowed: i, dropped: a } = w(e, n, t.allowEngineParams);
232
+ return a.length > 0 && t.onWarn?.("search dropped disallowed engineParams", {
233
+ engine: e,
234
+ keys: a
235
+ }), {
236
+ before: r,
237
+ after: i
238
+ };
239
+ }, P = (e, t) => {
240
+ if (!S(t)) throw new E({
241
+ engine: e,
242
+ op: "query",
243
+ message: `filter field "${t}" is not a plain field path (^[A-Za-z_][A-Za-z0-9_.]*$)`,
244
+ transient: !1
245
+ });
246
+ return t;
247
+ }, ae = (e) => {
248
+ let t = (e) => JSON.stringify(e), n = P("typesense", e.field);
113
249
  switch (e.op) {
114
- case "eq": return `${e.field}:=${t(e.value)}`;
115
- case "neq": return `${e.field}:!=${t(e.value)}`;
116
- case "gt": return `${e.field}:>${t(e.value)}`;
117
- case "gte": return `${e.field}:>=${t(e.value)}`;
118
- case "lt": return `${e.field}:<${t(e.value)}`;
119
- case "lte": return `${e.field}:<=${t(e.value)}`;
120
- case "in": return `${e.field}:[${p(e.value).map(t).join(",")}]`;
121
- case "nin": return `${e.field}:!=[${p(e.value).map(t).join(",")}]`;
250
+ case "eq": return `${n}:=${t(e.value)}`;
251
+ case "neq": return `${n}:!=${t(e.value)}`;
252
+ case "gt": return `${n}:>${t(e.value)}`;
253
+ case "gte": return `${n}:>=${t(e.value)}`;
254
+ case "lt": return `${n}:<${t(e.value)}`;
255
+ case "lte": return `${n}:<=${t(e.value)}`;
256
+ case "in": return `${n}:[${D(e.value).map(t).join(",")}]`;
257
+ case "nin": return `${n}:!=[${D(e.value).map(t).join(",")}]`;
122
258
  }
123
- }, b = (e) => {
124
- let t, n = async () => (t ||= v("typesense", "typesense", () => import("typesense")).then((t) => {
259
+ }, oe = (e, t = {}) => {
260
+ let n = N("typesense", t), r, i = async () => (r ||= M("typesense", "typesense", () => import("typesense")).then((t) => {
125
261
  let n = t.Client;
126
262
  return new n({
127
263
  nodes: [{ url: e.url ?? e.host ?? "http://localhost:8108" }],
128
264
  apiKey: e.apiKey
129
265
  });
130
- }), t), r = (e) => e && e.length > 0 ? e.map(y).join(" && ") : void 0, i = async (e, t) => {
266
+ }), r), a = (e) => e && e.length > 0 ? e.map(ae).join(" && ") : void 0, o = async (e, t) => {
131
267
  try {
132
268
  return await t();
133
269
  } catch (t) {
134
- throw t instanceof f ? t : new f({
270
+ throw t instanceof E ? t : new E({
135
271
  engine: "typesense",
136
272
  op: e,
137
273
  message: String(t?.message ?? t),
@@ -140,32 +276,33 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
140
276
  }
141
277
  };
142
278
  return {
143
- upsert: (e, t) => i("upsert", async () => {
144
- await (await n()).collections(e).documents().import(t, { action: "upsert" });
279
+ upsert: (e, t) => o("upsert", async () => {
280
+ await (await i()).collections(e).documents().import(t, { action: "upsert" });
145
281
  }),
146
- remove: (e, t) => i("remove", async () => {
147
- await (await n()).collections(e).documents().delete({ filter_by: `id:[${t.map((e) => JSON.stringify(e)).join(",")}]` });
282
+ remove: (e, t) => o("remove", async () => {
283
+ await (await i()).collections(e).documents().delete({ filter_by: `id:[${t.map((e) => JSON.stringify(e)).join(",")}]` });
148
284
  }),
149
- query: (e, { q: t, limit: a, offset: o, filters: s, facets: c, highlight: l, fuzziness: u, engineParams: d }) => i("query", async () => {
150
- let i = await n(), f = {
285
+ query: (e, { q: t, limit: r, offset: s, filters: c, facets: l, highlight: u, fuzziness: d, engineParams: f }) => o("query", async () => {
286
+ let o = await i(), p = n(f), m = {
287
+ ...p.before,
151
288
  q: t || "*",
152
289
  query_by: "*",
153
- per_page: a ?? 20,
154
- ...o ? { offset: o } : {},
155
- ...r(s) ? { filter_by: r(s) } : {},
156
- ...c && c.length > 0 ? { facet_by: c.join(",") } : {},
157
- ...l ? {
158
- ...l.fields && l.fields.length > 0 ? { highlight_fields: l.fields.join(",") } : { highlight_full_fields: "*" },
159
- ...l.preTag ? { highlight_start_tag: l.preTag } : {},
160
- ...l.postTag ? { highlight_end_tag: l.postTag } : {}
290
+ per_page: r ?? 20,
291
+ ...s ? { offset: s } : {},
292
+ ...a(c) ? { filter_by: a(c) } : {},
293
+ ...l && l.length > 0 ? { facet_by: l.join(",") } : {},
294
+ ...u ? {
295
+ ...u.fields && u.fields.length > 0 ? { highlight_fields: u.fields.join(",") } : { highlight_full_fields: "*" },
296
+ ...u.preTag ? { highlight_start_tag: u.preTag } : {},
297
+ ...u.postTag ? { highlight_end_tag: u.postTag } : {}
161
298
  } : {},
162
- ...u !== void 0 && u !== "auto" ? { num_typos: Math.min(2, Math.max(0, u)) } : {},
163
- ...d ?? {}
299
+ ...d !== void 0 && d !== "auto" ? { num_typos: Math.min(2, Math.max(0, d)) } : {},
300
+ ...p.after
164
301
  };
165
- return x(await i.collections(e).documents().search(f));
302
+ return se(await o.collections(e).documents().search(m));
166
303
  })
167
304
  };
168
- }, x = (e) => {
305
+ }, se = (e) => {
169
306
  let t = (e.hits ?? []).map((e) => {
170
307
  let t = {};
171
308
  for (let n of e.highlights ?? []) {
@@ -182,27 +319,27 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
182
319
  hits: t,
183
320
  facets: n
184
321
  };
185
- }, S = (e) => {
186
- let t = (e) => JSON.stringify(e);
322
+ }, ce = (e) => {
323
+ let t = (e) => JSON.stringify(e), n = P("meilisearch", e.field);
187
324
  switch (e.op) {
188
- case "eq": return `${e.field} = ${t(e.value)}`;
189
- case "neq": return `${e.field} != ${t(e.value)}`;
190
- case "gt": return `${e.field} > ${t(e.value)}`;
191
- case "gte": return `${e.field} >= ${t(e.value)}`;
192
- case "lt": return `${e.field} < ${t(e.value)}`;
193
- case "lte": return `${e.field} <= ${t(e.value)}`;
194
- case "in": return `${e.field} IN [${p(e.value).map(t).join(", ")}]`;
195
- case "nin": return `${e.field} NOT IN [${p(e.value).map(t).join(", ")}]`;
325
+ case "eq": return `${n} = ${t(e.value)}`;
326
+ case "neq": return `${n} != ${t(e.value)}`;
327
+ case "gt": return `${n} > ${t(e.value)}`;
328
+ case "gte": return `${n} >= ${t(e.value)}`;
329
+ case "lt": return `${n} < ${t(e.value)}`;
330
+ case "lte": return `${n} <= ${t(e.value)}`;
331
+ case "in": return `${n} IN [${D(e.value).map(t).join(", ")}]`;
332
+ case "nin": return `${n} NOT IN [${D(e.value).map(t).join(", ")}]`;
196
333
  }
197
- }, C = (e) => {
198
- let t, n = async () => (t ||= v("meilisearch", "meilisearch", () => import("meilisearch")).then((t) => new t.MeiliSearch({
334
+ }, F = (e, t = {}) => {
335
+ let n = N("meilisearch", t), r, i = async () => (r ||= M("meilisearch", "meilisearch", () => import("meilisearch")).then((t) => new t.MeiliSearch({
199
336
  host: e.host ?? e.url ?? "http://localhost:7700",
200
337
  apiKey: e.apiKey
201
- })), t), r = (e) => e && e.length > 0 ? e.map(S) : void 0, i = async (e, t) => {
338
+ })), r), a = (e) => e && e.length > 0 ? e.map(ce) : void 0, o = async (e, t) => {
202
339
  try {
203
340
  return await t();
204
341
  } catch (t) {
205
- throw t instanceof f ? t : new f({
342
+ throw t instanceof E ? t : new E({
206
343
  engine: "meilisearch",
207
344
  op: e,
208
345
  message: String(t?.message ?? t),
@@ -211,33 +348,34 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
211
348
  }
212
349
  };
213
350
  return {
214
- upsert: (e, t) => i("upsert", async () => {
215
- await (await n()).index(e).addDocuments(t);
351
+ upsert: (e, t) => o("upsert", async () => {
352
+ await (await i()).index(e).addDocuments(t);
216
353
  }),
217
- remove: (e, t) => i("remove", async () => {
218
- await (await n()).index(e).deleteDocuments([...t]);
354
+ remove: (e, t) => o("remove", async () => {
355
+ await (await i()).index(e).deleteDocuments([...t]);
219
356
  }),
220
- query: (e, { q: t, limit: a, offset: o, filters: s, facets: c, highlight: l, fuzziness: u, engineParams: d }) => i("query", async () => {
221
- let i = {
222
- limit: a ?? 20,
223
- ...o ? { offset: o } : {},
224
- ...r(s) ? { filter: r(s) } : {},
225
- ...c && c.length > 0 ? { facets: [...c] } : {},
226
- ...l ? {
227
- attributesToHighlight: l.fields && l.fields.length > 0 ? [...l.fields] : ["*"],
228
- ...l.preTag ? { highlightPreTag: l.preTag } : {},
229
- ...l.postTag ? { highlightPostTag: l.postTag } : {}
357
+ query: (e, { q: t, limit: r, offset: s, filters: c, facets: l, highlight: u, fuzziness: d, engineParams: f }) => o("query", async () => {
358
+ let o = n(f), p = {
359
+ ...o.before,
360
+ limit: r ?? 20,
361
+ ...s ? { offset: s } : {},
362
+ ...a(c) ? { filter: a(c) } : {},
363
+ ...l && l.length > 0 ? { facets: [...l] } : {},
364
+ ...u ? {
365
+ attributesToHighlight: u.fields && u.fields.length > 0 ? [...u.fields] : ["*"],
366
+ ...u.preTag ? { highlightPreTag: u.preTag } : {},
367
+ ...u.postTag ? { highlightPostTag: u.postTag } : {}
230
368
  } : {},
231
- ...u === 0 ? {
369
+ ...d === 0 ? {
232
370
  rankingScoreThreshold: void 0,
233
371
  typoTolerance: { enabled: !1 }
234
372
  } : {},
235
- ...d ?? {}
373
+ ...o.after
236
374
  };
237
- return w(await (await n()).index(e).search(t, i), l);
375
+ return le(await (await i()).index(e).search(t, p), u);
238
376
  })
239
377
  };
240
- }, w = (e, t) => ({
378
+ }, le = (e, t) => ({
241
379
  hits: e.hits.map((e) => {
242
380
  let { _formatted: n, ...r } = e;
243
381
  if (t && n) {
@@ -251,44 +389,56 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
251
389
  return { doc: r };
252
390
  }),
253
391
  facets: e.facetDistribution ?? {}
254
- }), T = (e) => {
392
+ }), ue = (e) => String(e).replace(/^-/, "\\-"), de = (e, t) => {
393
+ let n = typeof t == "boolean" ? +!!t : Number(t);
394
+ if (!Number.isFinite(n)) throw new E({
395
+ engine: "algolia",
396
+ op: "query",
397
+ message: `range filter on "${e}" needs a numeric value (got ${JSON.stringify(t)})`,
398
+ transient: !1
399
+ });
400
+ return n;
401
+ }, fe = (e) => {
255
402
  let t = [], n = [];
256
- for (let r of e ?? []) switch (r.op) {
257
- case "eq":
258
- t.push(`${r.field}:${r.value}`);
259
- break;
260
- case "neq":
261
- t.push(`${r.field}:-${r.value}`);
262
- break;
263
- case "in":
264
- t.push(p(r.value).map((e) => `${r.field}:${e}`));
265
- break;
266
- case "nin":
267
- for (let e of p(r.value)) t.push(`${r.field}:-${e}`);
268
- break;
269
- case "gt":
270
- n.push(`${r.field} > ${r.value}`);
271
- break;
272
- case "gte":
273
- n.push(`${r.field} >= ${r.value}`);
274
- break;
275
- case "lt":
276
- n.push(`${r.field} < ${r.value}`);
277
- break;
278
- case "lte":
279
- n.push(`${r.field} <= ${r.value}`);
280
- break;
403
+ for (let r of e ?? []) {
404
+ let e = P("algolia", r.field), i = (e) => ue(e), a = (t) => de(e, t);
405
+ switch (r.op) {
406
+ case "eq":
407
+ t.push(`${e}:${i(r.value)}`);
408
+ break;
409
+ case "neq":
410
+ t.push(`${e}:-${i(r.value)}`);
411
+ break;
412
+ case "in":
413
+ t.push(D(r.value).map((t) => `${e}:${i(t)}`));
414
+ break;
415
+ case "nin":
416
+ for (let n of D(r.value)) t.push(`${e}:-${i(n)}`);
417
+ break;
418
+ case "gt":
419
+ n.push(`${e} > ${a(r.value)}`);
420
+ break;
421
+ case "gte":
422
+ n.push(`${e} >= ${a(r.value)}`);
423
+ break;
424
+ case "lt":
425
+ n.push(`${e} < ${a(r.value)}`);
426
+ break;
427
+ case "lte":
428
+ n.push(`${e} <= ${a(r.value)}`);
429
+ break;
430
+ }
281
431
  }
282
432
  return {
283
433
  facetFilters: t,
284
434
  numericFilters: n
285
435
  };
286
- }, E = (e) => {
287
- let t, n = async () => (t ||= v("algoliasearch", "algolia", () => import("algoliasearch")).then((t) => t.algoliasearch(e.appId ?? "", e.apiKey)), t), r = async (e, t) => {
436
+ }, I = (e, t = {}) => {
437
+ let n = N("algolia", t), r, i = async () => (r ||= M("algoliasearch", "algolia", () => import("algoliasearch")).then((t) => t.algoliasearch(e.appId ?? "", e.apiKey)), r), a = async (e, t) => {
288
438
  try {
289
439
  return await t();
290
440
  } catch (t) {
291
- throw t instanceof f ? t : new f({
441
+ throw t instanceof E ? t : new E({
292
442
  engine: "algolia",
293
443
  op: e,
294
444
  message: String(t?.message ?? t),
@@ -297,8 +447,8 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
297
447
  }
298
448
  };
299
449
  return {
300
- upsert: (e, t) => r("upsert", async () => {
301
- await (await n()).saveObjects({
450
+ upsert: (e, t) => a("upsert", async () => {
451
+ await (await i()).saveObjects({
302
452
  indexName: e,
303
453
  objects: t.map((e) => ({
304
454
  ...e,
@@ -306,37 +456,38 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
306
456
  }))
307
457
  });
308
458
  }),
309
- remove: (e, t) => r("remove", async () => {
310
- await (await n()).deleteObjects({
459
+ remove: (e, t) => a("remove", async () => {
460
+ await (await i()).deleteObjects({
311
461
  indexName: e,
312
462
  objectIDs: [...t]
313
463
  });
314
464
  }),
315
- query: (e, { q: t, limit: i, offset: a, filters: o, facets: s, highlight: c, fuzziness: l, engineParams: u }) => r("query", async () => {
316
- let { facetFilters: r, numericFilters: d } = T(o), f = {
465
+ query: (e, { q: t, limit: r, offset: o, filters: s, facets: c, highlight: l, fuzziness: u, engineParams: d }) => a("query", async () => {
466
+ let { facetFilters: a, numericFilters: f } = fe(s), p = o ? {
467
+ offset: o,
468
+ length: r ?? 20
469
+ } : { hitsPerPage: r ?? 20 }, m = n(d), h = {
470
+ ...m.before,
317
471
  query: t,
318
- ...a ? {
319
- offset: a,
320
- length: i ?? 20
321
- } : { hitsPerPage: i ?? 20 },
322
- ...r.length > 0 ? { facetFilters: r } : {},
323
- ...d.length > 0 ? { numericFilters: d } : {},
324
- ...s && s.length > 0 ? { facets: [...s] } : {},
325
- ...c ? {
326
- ...c.fields && c.fields.length > 0 ? { attributesToHighlight: [...c.fields] } : {},
327
- ...c.preTag ? { highlightPreTag: c.preTag } : {},
328
- ...c.postTag ? { highlightPostTag: c.postTag } : {}
472
+ ...p,
473
+ ...a.length > 0 ? { facetFilters: a } : {},
474
+ ...f.length > 0 ? { numericFilters: f } : {},
475
+ ...c && c.length > 0 ? { facets: [...c] } : {},
476
+ ...l ? {
477
+ ...l.fields && l.fields.length > 0 ? { attributesToHighlight: [...l.fields] } : {},
478
+ ...l.preTag ? { highlightPreTag: l.preTag } : {},
479
+ ...l.postTag ? { highlightPostTag: l.postTag } : {}
329
480
  } : {},
330
- ...l === 0 ? { typoTolerance: !1 } : {},
331
- ...u ?? {}
481
+ ...u === 0 ? { typoTolerance: !1 } : {},
482
+ ...m.after
332
483
  };
333
- return D(await (await n()).searchSingleIndex({
484
+ return pe(await (await i()).searchSingleIndex({
334
485
  indexName: e,
335
- searchParams: f
336
- }), c);
486
+ searchParams: h
487
+ }), l);
337
488
  })
338
489
  };
339
- }, D = (e, t) => ({
490
+ }, pe = (e, t) => ({
340
491
  hits: e.hits.map((e) => {
341
492
  let { _highlightResult: n, ...r } = e;
342
493
  if (t && n) {
@@ -350,31 +501,46 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
350
501
  return { doc: r };
351
502
  }),
352
503
  facets: e.facets ?? {}
353
- }), O = "_voltro_search_stats", k = () => ({
504
+ }), L = "_voltro_search_stats", R = () => ({
354
505
  synced: 0,
355
506
  removed: 0,
507
+ dropped: 0,
356
508
  lastReindexAt: null,
357
509
  lastReindexCount: 0
358
- }), A = l(O, {
359
- id: s({ prefix: "sstat" }),
360
- indexName: u().maxLength(191),
361
- replicaId: u().maxLength(191),
362
- synced: c().default(0),
363
- removed: c().default(0),
364
- lastReindexAt: d().nullable(),
365
- lastReindexCount: c().default(0),
366
- updatedAt: d()
367
- }).unique("bySearchStatsIndexReplica", ["indexName", "replicaId"]).index("bySearchStatsIndex", ["indexName"]), j = () => [A], M = () => {
368
- let e = /* @__PURE__ */ new Map(), t = (t) => e.get(t) ?? k();
510
+ }), z = v(L, {
511
+ id: g({ prefix: "sstat" }),
512
+ indexName: y().maxLength(191),
513
+ replicaId: y().maxLength(191),
514
+ synced: _().default(0),
515
+ removed: _().default(0),
516
+ dropped: _().default(0),
517
+ scope: y().maxLength(8).default("local"),
518
+ lastReindexAt: b().nullable(),
519
+ lastReindexCount: _().default(0),
520
+ updatedAt: b()
521
+ }).unique("bySearchStatsIndexReplica", ["indexName", "replicaId"]).index("bySearchStatsIndex", ["indexName"]), B = () => [z], V = () => {
522
+ let e = /* @__PURE__ */ new Map(), t = (t) => e.get(t) ?? R();
369
523
  return {
370
524
  bump: async (n, r) => {
371
525
  let i = t(n);
372
526
  e.set(n, r === "synced" ? {
373
527
  ...i,
374
528
  synced: i.synced + 1
375
- } : {
529
+ } : r === "removed" ? {
376
530
  ...i,
377
531
  removed: i.removed + 1
532
+ } : {
533
+ ...i,
534
+ dropped: i.dropped + 1
535
+ });
536
+ },
537
+ add: async (n, r) => {
538
+ let i = t(n);
539
+ e.set(n, {
540
+ ...i,
541
+ synced: i.synced + r.synced,
542
+ removed: i.removed + r.removed,
543
+ dropped: i.dropped + r.dropped
378
544
  });
379
545
  },
380
546
  recordReindex: async (n, r, i) => {
@@ -388,65 +554,78 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
388
554
  },
389
555
  get: async (e) => t(e)
390
556
  };
391
- }, N = (e) => ({
392
- table: O,
557
+ }, H = (e) => ({
558
+ table: L,
393
559
  predicate: e,
394
560
  order: [],
395
561
  take: void 0,
396
562
  skip: void 0,
397
563
  projection: void 0
398
- }), P = () => `sstat_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, F = () => process.env.VOLTRO_REPLICA_ID ?? process.env.HOSTNAME ?? process.env.POD_NAME ?? `pid_${process.pid}_${Math.random().toString(36).slice(2, 8)}`, I = 32, L = (e, t = F()) => {
399
- let n = async (n) => (await e.query(N(a(o("indexName", n), o("replicaId", t)))))[0] ?? null, r = async (e) => {
564
+ }), me = () => `sstat_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, he = () => process.env.VOLTRO_REPLICA_ID ?? process.env.HOSTNAME ?? process.env.POD_NAME ?? `pid_${process.pid}_${Math.random().toString(36).slice(2, 8)}`, U = 32, W = (e, t = he()) => {
565
+ let n = async (n) => (await e.query(H(p(h("indexName", n), h("replicaId", t)))))[0] ?? null, r = async (e) => {
400
566
  try {
401
567
  return await e();
402
568
  } catch (e) {
403
- throw e instanceof f ? e : new f({
569
+ throw e instanceof E ? e : new E({
404
570
  engine: "stats",
405
571
  op: "upsert",
406
572
  message: String(e?.message ?? e),
407
573
  transient: !0
408
574
  });
409
575
  }
410
- }, i = (i, s, c) => r(async () => {
411
- for (let r = 0; r < I; r++) {
576
+ }, i = (i, a, o, s) => r(async () => {
577
+ for (let r = 0; r < U; r++) {
412
578
  r > 0 && await new Promise((e) => setTimeout(e, r));
413
- let l = await n(i);
414
- if (l) {
415
- let t = Number(l.synced ?? 0), n = Number(l.removed ?? 0), r = {
579
+ let c = await n(i);
580
+ if (c) {
581
+ let t = Number(c.synced ?? 0), n = Number(c.removed ?? 0), r = Number(c.dropped ?? 0), i = {
416
582
  synced: t,
417
583
  removed: n,
418
- lastReindexAt: l.lastReindexAt ? new Date(l.lastReindexAt).toISOString() : null,
419
- lastReindexCount: Number(l.lastReindexCount ?? 0)
584
+ dropped: r,
585
+ lastReindexAt: c.lastReindexAt ? new Date(c.lastReindexAt).toISOString() : null,
586
+ lastReindexCount: Number(c.lastReindexCount ?? 0)
420
587
  };
421
588
  if (await e.updateMany("_voltro_search_stats", {
422
- ...s(r),
589
+ ...o(i),
590
+ ...a ? { scope: a } : {},
423
591
  updatedAt: /* @__PURE__ */ new Date()
424
- }, { where: a(o("id", String(l.id)), o("synced", t), o("removed", n)) }) === 1) return;
592
+ }, { where: p(h("id", String(c.id)), h("synced", t), h("removed", n), h("dropped", r)) }) === 1) return;
425
593
  } else {
426
- let n = P(), r = await e.insertIgnore(O, {
594
+ let n = me(), r = await e.insertIgnore(L, {
427
595
  id: n,
428
596
  indexName: i,
429
597
  replicaId: t,
430
598
  synced: 0,
431
599
  removed: 0,
600
+ dropped: 0,
601
+ scope: a ?? "local",
432
602
  lastReindexAt: null,
433
603
  lastReindexCount: 0,
434
604
  updatedAt: /* @__PURE__ */ new Date(),
435
- ...c
605
+ ...s
436
606
  }, { conflictColumns: ["indexName", "replicaId"] });
437
607
  if (String(r.id) === n) return;
438
608
  }
439
609
  }
440
- throw new f({
610
+ throw new E({
441
611
  engine: "stats",
442
612
  op: "upsert",
443
- message: `search stats: contention on (${i}, ${t}) exceeded ${I} attempts`,
613
+ message: `search stats: contention on (${i}, ${t}) exceeded ${U} attempts`,
444
614
  transient: !0
445
615
  });
446
616
  });
447
617
  return {
448
- bump: (e, t) => i(e, (e) => t === "synced" ? { synced: e.synced + 1 } : { removed: e.removed + 1 }, t === "synced" ? { synced: 1 } : { removed: 1 }),
449
- recordReindex: (e, t, n) => i(e, (e) => ({
618
+ bump: (e, t, n) => i(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 }),
619
+ add: (e, t, n) => i(e, n, (e) => ({
620
+ synced: e.synced + t.synced,
621
+ removed: e.removed + t.removed,
622
+ dropped: e.dropped + t.dropped
623
+ }), {
624
+ synced: t.synced,
625
+ removed: t.removed,
626
+ dropped: t.dropped
627
+ }),
628
+ recordReindex: (e, t, n) => i(e, void 0, (e) => ({
450
629
  synced: e.synced + t,
451
630
  lastReindexAt: new Date(n),
452
631
  lastReindexCount: t
@@ -456,37 +635,249 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
456
635
  lastReindexCount: t
457
636
  }),
458
637
  get: (t) => r(async () => {
459
- let n = await e.query(N(o("indexName", t))), r = k(), i = 0, a = 0, s = null, c = 0;
638
+ let n = await e.query(H(h("indexName", t))), r = n.some((e) => e.scope === "fleet"), i = (e) => {
639
+ let t = 0;
640
+ for (let i of n) {
641
+ let n = Number(i[e] ?? 0);
642
+ t = r ? Math.max(t, n) : t + n;
643
+ }
644
+ return t;
645
+ }, a = null, o = 0;
460
646
  for (let e of n) {
461
- i += Number(e.synced ?? 0), a += Number(e.removed ?? 0);
462
647
  let t = e.lastReindexAt ? new Date(e.lastReindexAt).toISOString() : null;
463
- t && (s === null || t > s) && (s = t, c = Number(e.lastReindexCount ?? 0));
648
+ t && (a === null || t > a) && (a = t, o = Number(e.lastReindexCount ?? 0));
464
649
  }
465
650
  return {
466
- ...r,
467
- synced: i,
468
- removed: a,
469
- lastReindexAt: s,
470
- lastReindexCount: c
651
+ ...R(),
652
+ synced: i("synced"),
653
+ removed: i("removed"),
654
+ dropped: i("dropped"),
655
+ lastReindexAt: a,
656
+ lastReindexCount: o
471
657
  };
472
658
  })
473
659
  };
474
- }, R = (e) => {
475
- if (e === void 0 || e === "memory") return g();
660
+ }, G = (e) => {
661
+ let t = /* @__PURE__ */ new Map(), n = 0, r = !1, i = async () => {
662
+ if (t.size === 0) return;
663
+ let r = t;
664
+ t = /* @__PURE__ */ new Map(), n = 0;
665
+ for (let [t, n] of r) try {
666
+ await e.flush(t, {
667
+ synced: n.synced,
668
+ removed: n.removed,
669
+ dropped: n.dropped
670
+ }, n.scope);
671
+ } catch {}
672
+ }, a = setInterval(() => {
673
+ i();
674
+ }, Math.max(1, e.intervalMs));
675
+ return a.unref?.(), {
676
+ bump: (a, o, s) => {
677
+ if (r) return;
678
+ let c = t.get(a);
679
+ c === void 0 ? t.set(a, {
680
+ synced: +(o === "synced"),
681
+ removed: +(o === "removed"),
682
+ dropped: +(o === "dropped"),
683
+ scope: s
684
+ }) : (o === "synced" ? c.synced += 1 : o === "removed" ? c.removed += 1 : c.dropped += 1, c.scope = s), n += 1, n >= e.maxBuffered && i();
685
+ },
686
+ flushNow: i,
687
+ shutdown: async () => {
688
+ r || (r = !0, clearInterval(a), await i());
689
+ },
690
+ pending: () => n
691
+ };
692
+ }, K = (e) => (t, n, r) => e().add(t, n, r), q = (e) => e === void 0 || e === "memory" ? "memory" : typeof e.query == "function" ? e.engineKind === "memory" ? "memory" : "custom" : "vendor", ge = class extends Error {
693
+ _tag = "SearchBackendNotDurable";
694
+ constructor(e) {
695
+ super(e), this.name = "SearchBackendNotDurable";
696
+ }
697
+ }, _e = [
698
+ "plugin-search refuses to boot in production on the in-memory backend.",
699
+ "",
700
+ "The memory index lives in THIS process's heap. Two consequences, both silent:",
701
+ " • every replica holds a different index, so a result depends on which replica served you;",
702
+ " • the index starts EMPTY after every restart/deploy, and nothing re-seeds it automatically.",
703
+ "",
704
+ "Configure a durable engine in app.config.ts:",
705
+ " searchPlugin({ backend: { engine: 'typesense', url: …, apiKey: … }, indexes })",
706
+ " searchPlugin({ backend: { engine: 'meilisearch', url: …, apiKey: … }, indexes })",
707
+ " searchPlugin({ backend: { engine: 'algolia', appId: …, apiKey: … }, indexes })",
708
+ "or pass your own `SearchBackend` implementation.",
709
+ "",
710
+ "If this deployment really is ONE process that re-seeds its index at startup",
711
+ "(a `*.startup.tsx` calling `backfillIndex`), say so explicitly:",
712
+ " searchPlugin({ singleProcessMemoryIndex: true, indexes })",
713
+ "That is a claim about your topology, not a mute switch: if a second replica",
714
+ "ever announces itself, the plugin logs that the claim was contradicted."
715
+ ].join("\n"), ve = [
716
+ "plugin-search is running the in-memory backend in production under",
717
+ "`singleProcessMemoryIndex: true`. Holding you to the two conditions that makes true:",
718
+ "exactly ONE process serves search, and it re-seeds every index at startup",
719
+ "(`backfillIndex`) — the index is empty after each restart until it does."
720
+ ].join(" "), ye = (e) => e.backendKind !== "memory" || !e.production ? { level: "quiet" } : e.singleProcessDeclared ? {
721
+ level: "note",
722
+ message: ve
723
+ } : {
724
+ level: "refuse",
725
+ message: _e
726
+ }, be = (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.`, J = "_voltro_search_drift", xe = v(J, {
727
+ id: g({ prefix: "sdrft" }),
728
+ indexName: y().maxLength(191),
729
+ sourceTable: y().maxLength(191),
730
+ rowId: y().maxLength(191),
731
+ docId: y().maxLength(191),
732
+ op: y().maxLength(16),
733
+ attempts: _().default(0),
734
+ lastError: y().maxLength(1e3),
735
+ firstFailedAt: b(),
736
+ lastFailedAt: b()
737
+ }).unique("bySearchDriftIndexRow", ["indexName", "rowId"]).index("bySearchDriftIndex", ["indexName"]), Se = (e, t) => ({
738
+ table: e,
739
+ predicate: h("id", t),
740
+ order: [],
741
+ take: 1,
742
+ skip: void 0,
743
+ projection: void 0
744
+ }), Y = (e) => e ? new Date(e).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString(), Ce = (e) => ({
745
+ indexName: String(e.indexName),
746
+ sourceTable: String(e.sourceTable),
747
+ rowId: String(e.rowId),
748
+ docId: String(e.docId),
749
+ op: e.op === "remove" ? "remove" : "upsert",
750
+ attempts: Number(e.attempts ?? 0),
751
+ lastError: String(e.lastError ?? ""),
752
+ firstFailedAt: Y(e.firstFailedAt),
753
+ lastFailedAt: Y(e.lastFailedAt)
754
+ }), we = () => {
755
+ let e = /* @__PURE__ */ new Map(), t = (e, t) => `${e}\u0000${t}`;
756
+ return {
757
+ record: async (n) => {
758
+ let r = (/* @__PURE__ */ new Date()).toISOString(), i = e.get(t(n.indexName, n.rowId));
759
+ e.set(t(n.indexName, n.rowId), {
760
+ indexName: n.indexName,
761
+ sourceTable: n.sourceTable,
762
+ rowId: n.rowId,
763
+ docId: n.docId,
764
+ op: n.op,
765
+ attempts: (i?.attempts ?? 0) + 1,
766
+ lastError: n.error,
767
+ firstFailedAt: i?.firstFailedAt ?? r,
768
+ lastFailedAt: r
769
+ });
770
+ },
771
+ list: async (t) => [...e.values()].sort((e, t) => e.firstFailedAt.localeCompare(t.firstFailedAt)).slice(0, t),
772
+ summary: async (t) => {
773
+ let n = [...e.values()].filter((e) => e.indexName === t), r = null;
774
+ for (let e of n) (r === null || e.lastFailedAt > r) && (r = e.lastFailedAt);
775
+ return {
776
+ pending: n.length,
777
+ lastFailedAt: r
778
+ };
779
+ },
780
+ clear: async (n, r) => {
781
+ e.delete(t(n, r));
782
+ }
783
+ };
784
+ }, Te = () => `sdrft_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, X = (e, t) => ({
785
+ table: J,
786
+ predicate: e,
787
+ order: [{
788
+ column: "firstFailedAt",
789
+ direction: "asc"
790
+ }],
791
+ take: t,
792
+ skip: void 0,
793
+ projection: void 0
794
+ }), Ee = (e) => ({
795
+ record: async (t) => {
796
+ let n = /* @__PURE__ */ new Date(), r = Te(), i = await e.insertIgnore(J, {
797
+ id: r,
798
+ indexName: t.indexName,
799
+ sourceTable: t.sourceTable,
800
+ rowId: t.rowId,
801
+ docId: t.docId,
802
+ op: t.op,
803
+ attempts: 1,
804
+ lastError: t.error.slice(0, 1e3),
805
+ firstFailedAt: n,
806
+ lastFailedAt: n
807
+ }, { conflictColumns: ["indexName", "rowId"] });
808
+ String(i.id) !== r && await e.updateMany(J, {
809
+ attempts: Number(i.attempts ?? 0) + 1,
810
+ op: t.op,
811
+ docId: t.docId,
812
+ lastError: t.error.slice(0, 1e3),
813
+ lastFailedAt: n
814
+ }, { where: h("id", String(i.id)) });
815
+ },
816
+ list: async (t) => (await e.query(X(void 0, t))).map(Ce),
817
+ summary: async (t) => {
818
+ let n = (await e.query({
819
+ ...X(h("indexName", t), void 0),
820
+ order: [],
821
+ aggregations: [m("n")]
822
+ }))[0], r = Number(n?.n ?? n?.count ?? 0);
823
+ if (r === 0) return {
824
+ pending: 0,
825
+ lastFailedAt: null
826
+ };
827
+ let i = await e.query({
828
+ ...X(h("indexName", t), 1),
829
+ order: [{
830
+ column: "lastFailedAt",
831
+ direction: "desc"
832
+ }]
833
+ });
834
+ return {
835
+ pending: r,
836
+ lastFailedAt: i[0] ? Y(i[0].lastFailedAt) : null
837
+ };
838
+ },
839
+ clear: async (t, n) => {
840
+ await e.deleteMany(J, { where: p(h("indexName", t), h("rowId", n)) });
841
+ }
842
+ }), De = () => [xe], Oe = (e, t) => {
843
+ if (e === void 0 || e === "memory") return A();
476
844
  if (typeof e.query == "function") return e;
477
- let t = e;
478
- return t.engine === "typesense" ? b(t) : t.engine === "meilisearch" ? C(t) : t.engine === "algolia" ? E(t) : g();
479
- }, z = async (e, t, n) => {
845
+ let n = e;
846
+ return n.engine === "typesense" ? oe(n, t) : n.engine === "meilisearch" ? F(n, t) : n.engine === "algolia" ? I(n, t) : A();
847
+ }, ke = async (e, t, n) => {
480
848
  let r = n.map((e) => t.map(e));
481
849
  return r.length > 0 && await e.upsert(t.index, r), r.length;
482
- }, B = async (e, t, n) => {
850
+ }, Z = async (e, t, n) => {
483
851
  if (n.op === "delete") {
484
852
  let r = n.old?.id;
485
853
  typeof r == "string" && await e.remove(t.index, [r]);
486
854
  return;
487
855
  }
488
856
  n.new && await e.upsert(t.index, [t.map(n.new)]);
489
- }, V = (e) => {
857
+ }, Q = {
858
+ retries: 5,
859
+ retryBaseDelayMs: 200,
860
+ retryMaxDelayMs: 1e4,
861
+ resyncIntervalMs: 6e4,
862
+ resyncBatchSize: 200,
863
+ reindexBatchSize: 1e3,
864
+ statsFlushIntervalMs: 5e3,
865
+ statsFlushMaxBuffered: 1e3
866
+ }, Ae = (e) => ({
867
+ retries: Math.max(0, e?.retries ?? Q.retries),
868
+ retryBaseDelayMs: Math.max(1, e?.retryBaseDelayMs ?? Q.retryBaseDelayMs),
869
+ retryMaxDelayMs: Math.max(1, e?.retryMaxDelayMs ?? Q.retryMaxDelayMs),
870
+ resyncIntervalMs: Math.max(0, e?.resyncIntervalMs ?? Q.resyncIntervalMs),
871
+ resyncBatchSize: Math.max(1, e?.resyncBatchSize ?? Q.resyncBatchSize),
872
+ reindexBatchSize: Math.max(1, e?.reindexBatchSize ?? Q.reindexBatchSize),
873
+ statsFlushIntervalMs: Math.max(0, e?.statsFlushIntervalMs ?? Q.statsFlushIntervalMs),
874
+ statsFlushMaxBuffered: Math.max(1, e?.statsFlushMaxBuffered ?? Q.statsFlushMaxBuffered)
875
+ }), je = (e) => e instanceof E ? e : new E({
876
+ engine: "unknown",
877
+ op: "upsert",
878
+ message: String(e?.message ?? e),
879
+ transient: !1
880
+ }), Me = (e) => d.exponential(s.millis(e.retryBaseDelayMs), 2).pipe(d.either(d.spaced(s.millis(e.retryMaxDelayMs))), d.intersect(d.recurs(e.retries)), d.whileInput((e) => e.transient)), Ne = (e) => {
490
881
  if (!e || e === "memory" || typeof e.query == "function") return;
491
882
  let t = e;
492
883
  try {
@@ -494,131 +885,292 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
494
885
  } catch {
495
886
  return;
496
887
  }
497
- }, H = (e, t, n) => {
888
+ }, Pe = (e, t, n) => {
498
889
  let r = n ?? [];
499
890
  return e ? t == null ? null : [...r, {
500
891
  field: e,
501
892
  op: "eq",
502
893
  value: t
503
894
  }] : r;
504
- }, U = (n) => {
505
- let a = n.name ? `@voltro/plugin-search#${n.name}` : "@voltro/plugin-search", o = R(n.backend), s = /* @__PURE__ */ new Map();
506
- for (let e of Object.values(n.indexes)) s.set(e.index, e);
507
- let c = {
508
- ...e,
895
+ }, Fe = (e) => [
896
+ ...(e.filters ?? []).map((e) => ["filter", e.field]),
897
+ ...(e.facets ?? []).map((e) => ["facet", e]),
898
+ ...(e.highlight?.fields ?? []).map((e) => ["highlight", e])
899
+ ], $ = "@voltro/plugin-search", Ie = (o) => {
900
+ let s = ne({
901
+ base: $,
902
+ alias: o.alias,
903
+ instance: o.name
904
+ }), d = () => void 0, p = () => void 0, m = Ae(o.sync), h = Me(m), g = Oe(o.backend, {
905
+ ...o.allowedEngineParams ? { allowEngineParams: o.allowedEngineParams } : {},
906
+ onWarn: (e, t) => d(e, t)
907
+ }), _ = /* @__PURE__ */ new Map();
908
+ for (let e of Object.values(o.indexes)) _.set(e.index, e);
909
+ let v = q(o.backend), y = o.singleProcessMemoryIndex === !0, b = !1, x, S = {
910
+ ...n,
509
911
  description: "Full-text search an index (facets/highlight/fuzzy/filters), auto-scoped to the caller tenant.",
510
- execute: (e, t) => r.promise(async () => {
511
- let { index: n, q: r, limit: i, offset: a, filters: c, facets: l, highlight: u, fuzziness: d, engineParams: f } = e, p = s.get(n), m = t.request.subject.tenantId ?? null, h = H(p?.tenantField, m, c);
512
- if (h === null) return {
912
+ execute: (n, r) => {
913
+ let { index: i, q: a, limit: o, offset: s, filters: l, facets: u, highlight: d, fuzziness: f, engineParams: p } = n, m = _.get(i);
914
+ if (!m) return c.fail(new e({ index: i }));
915
+ let h = re(Fe({
916
+ ...l ? { filters: l } : {},
917
+ ...u ? { facets: u } : {},
918
+ ...d ? { highlight: d } : {}
919
+ }), m.queryableFields);
920
+ if (h) return c.fail(new t(h));
921
+ let ee = r.request.subject.tenantId ?? null, v = Pe(m.tenantField, ee, l);
922
+ if (v === null) return c.succeed({
513
923
  hits: [],
514
924
  facets: {}
925
+ });
926
+ let y = {
927
+ q: a,
928
+ ...o === void 0 ? {} : { limit: o },
929
+ ...s === void 0 ? {} : { offset: s },
930
+ ...v.length > 0 ? { filters: v } : {},
931
+ ...u && u.length > 0 ? { facets: u } : {},
932
+ ...d ? { highlight: d } : {},
933
+ ...f === void 0 ? {} : { fuzziness: f },
934
+ ...p ? { engineParams: p } : {}
515
935
  };
516
- let g = {
517
- q: r,
518
- ...i === void 0 ? {} : { limit: i },
519
- ...a === void 0 ? {} : { offset: a },
520
- ...h.length > 0 ? { filters: h } : {},
521
- ...l && l.length > 0 ? { facets: l } : {},
522
- ...u ? { highlight: u } : {},
523
- ...d === void 0 ? {} : { fuzziness: d },
524
- ...f ? { engineParams: f } : {}
525
- };
526
- return o.query(n, g);
527
- })
528
- }, l = V(n.backend), u = [
936
+ return c.promise(() => g.query(i, y));
937
+ }
938
+ }, C = Ne(o.backend), ie = [
529
939
  "store:changes:read",
530
940
  "store:write",
531
941
  "inspect:read",
532
942
  "inspect:write",
533
- ...l ? [`network:outbound:${l}`] : []
534
- ], d = () => void 0, f = M(), p, m = (e) => ({
943
+ ...C ? [`network:outbound:${C}`] : []
944
+ ], w = V(), T = we(), E, D, O, k, A = (e, t, n) => k === void 0 ? c.tryPromise(() => w.bump(e, t, n)).pipe(c.catchAll((n) => c.sync(() => d("search stats bump failed", {
945
+ index: e,
946
+ kind: t,
947
+ error: String(n)
948
+ })))) : c.sync(() => k.bump(e, t, n)), j = async (e) => {
949
+ let t = await T.list(e), n = 0, r = 0;
950
+ for (let e of t) {
951
+ let t = _.get(e.indexName);
952
+ if (!t) {
953
+ await T.clear(e.indexName, e.rowId);
954
+ continue;
955
+ }
956
+ try {
957
+ if (!D) throw Error("data store not bound");
958
+ let r = (await D.query(Se(e.sourceTable, e.rowId)))[0];
959
+ r ? await g.upsert(t.index, [t.map(r)]) : await g.remove(t.index, [e.docId]), await T.clear(e.indexName, e.rowId), n++;
960
+ } catch (t) {
961
+ r++, await T.record({
962
+ ...e,
963
+ error: String(t?.message ?? t)
964
+ }).catch(() => void 0);
965
+ }
966
+ }
967
+ return {
968
+ scanned: t.length,
969
+ repaired: n,
970
+ failed: r
971
+ };
972
+ }, M = (e) => ({
535
973
  kind: "json",
536
974
  data: e
537
- }), h = (e, t) => ({
975
+ }), N = (e, t) => ({
538
976
  kind: "json",
539
977
  status: e,
540
978
  data: { error: t }
541
979
  });
542
- return i({
543
- name: a,
980
+ return te({
981
+ name: s,
982
+ baseName: $,
544
983
  description: "External search index sync (Typesense/Meilisearch/Algolia/memory) via the ChangeEvent tap + a tenant-scoped query route.",
545
- permissions: u,
546
- routes: [c],
547
- rpcClientDescriptors: t,
548
- inspectEndpoints: [{
549
- method: "GET",
550
- path: "/indexes",
551
- description: "Configured indexes + durable, cross-replica sync stats.",
552
- handler: () => r.promise(async () => {
553
- let e = await Promise.all(Object.entries(n.indexes).map(async ([e, t]) => {
554
- let n = await f.get(t.index);
555
- return {
556
- table: e,
557
- index: t.index,
558
- tenantField: t.tenantField ?? null,
559
- ...n
560
- };
561
- }));
562
- return m({
563
- indexes: e,
564
- backend: typeof n.backend == "string" ? n.backend : n.backend ? "custom/vendor" : "memory"
565
- });
566
- })
567
- }, {
568
- method: "POST",
569
- path: "/reindex",
570
- description: "Re-seed an index from the table's current rows.",
571
- handler: (e) => r.promise(async () => {
572
- let t;
573
- try {
574
- t = JSON.parse(e.body || "{}");
575
- } catch {
576
- return h(400, "invalid JSON body");
577
- }
578
- let r = t.table ? n.indexes[t.table] : void 0;
579
- if (!t.table || !r) return h(404, `no index configured for table "${t.table ?? ""}"`);
580
- if (!p) return h(503, "data store not bound yet");
581
- try {
582
- let e = await p.query({
583
- table: t.table,
584
- predicate: void 0,
585
- order: [],
586
- take: void 0,
587
- skip: void 0,
588
- projection: void 0
589
- }), n = await z(o, r, e);
590
- return await f.recordReindex(r.index, n, (/* @__PURE__ */ new Date()).toISOString()), m({
984
+ permissions: ie,
985
+ routes: [S],
986
+ rpcClientDescriptors: r,
987
+ inspectEndpoints: [
988
+ {
989
+ method: "GET",
990
+ path: "/indexes",
991
+ description: "Configured indexes + durable, cross-replica sync stats + current index drift.",
992
+ handler: () => c.promise(async () => {
993
+ await k?.flushNow();
994
+ let e = await Promise.all(Object.entries(o.indexes).map(async ([e, t]) => {
995
+ let n = await w.get(t.index), r = await T.summary(t.index);
996
+ return {
997
+ table: e,
998
+ index: t.index,
999
+ tenantField: t.tenantField ?? null,
1000
+ ...n,
1001
+ pendingDrift: r.pending,
1002
+ lastDriftAt: r.lastFailedAt,
1003
+ drifted: r.pending > 0
1004
+ };
1005
+ }));
1006
+ return M({
1007
+ indexes: e,
1008
+ backend: typeof o.backend == "string" ? o.backend : o.backend ? "custom/vendor" : "memory",
1009
+ durable: v !== "memory",
1010
+ singleProcessMemoryIndex: y,
1011
+ pendingDrift: e.reduce((e, t) => e + t.pendingDrift, 0),
1012
+ sync: {
1013
+ ...m,
1014
+ resyncArmed: E !== void 0
1015
+ }
1016
+ });
1017
+ })
1018
+ },
1019
+ {
1020
+ method: "GET",
1021
+ path: "/drift",
1022
+ description: "The changes that did NOT reach the engine, oldest first (the repair queue).",
1023
+ handler: (e) => c.promise(async () => {
1024
+ let t = Number(new URL(e.url, "http://localhost").searchParams.get("limit") ?? 100), n = await T.list(Number.isFinite(t) && t > 0 ? Math.min(t, 1e3) : 100);
1025
+ return M({ entries: n });
1026
+ })
1027
+ },
1028
+ {
1029
+ method: "POST",
1030
+ path: "/resync",
1031
+ description: "Repair drifted rows now — re-reads each from the DB and re-applies it.",
1032
+ handler: (e) => c.promise(async () => {
1033
+ let t;
1034
+ try {
1035
+ t = JSON.parse(e.body || "{}");
1036
+ } catch {
1037
+ return N(400, "invalid JSON body");
1038
+ }
1039
+ if (!D) return N(503, "data store not bound yet");
1040
+ try {
1041
+ let e = await j(Math.min(Math.max(1, t.limit ?? m.resyncBatchSize), 1e3));
1042
+ return M({
1043
+ ok: !0,
1044
+ ...e
1045
+ });
1046
+ } catch (e) {
1047
+ return N(500, e.message);
1048
+ }
1049
+ })
1050
+ },
1051
+ {
1052
+ method: "POST",
1053
+ path: "/reindex",
1054
+ description: "Re-seed an index from the table's current rows (streamed in bounded batches).",
1055
+ handler: (e) => c.promise(async () => {
1056
+ let t;
1057
+ try {
1058
+ t = JSON.parse(e.body || "{}");
1059
+ } catch {
1060
+ return N(400, "invalid JSON body");
1061
+ }
1062
+ let n = t.table, r = n ? o.indexes[n] : void 0;
1063
+ if (!n || !r) return N(404, `no index configured for table "${t.table ?? ""}"`);
1064
+ if (!O) return N(503, "data store not bound yet");
1065
+ let s = 0, d = await c.runPromiseExit(ee(O, {
1066
+ table: n,
1067
+ chunkSize: m.reindexBatchSize
1068
+ }).pipe(f.chunks, f.runForEach((e) => c.tryPromise(async () => {
1069
+ s += await ke(g, r, a.toReadonlyArray(e));
1070
+ }))));
1071
+ if (l.isFailure(d)) {
1072
+ let e = i.failureOption(d.cause), t = u.isSome(e) ? e.value : d.cause, n = t.cause ?? t.error ?? t;
1073
+ return N(500, String(n?.message ?? n));
1074
+ }
1075
+ try {
1076
+ await w.recordReindex(r.index, s, (/* @__PURE__ */ new Date()).toISOString());
1077
+ } catch (e) {
1078
+ return N(500, e.message);
1079
+ }
1080
+ return M({
591
1081
  ok: !0,
592
- table: t.table,
1082
+ table: n,
593
1083
  index: r.index,
594
- count: n
1084
+ count: s
595
1085
  });
1086
+ })
1087
+ }
1088
+ ],
1089
+ extendSchema: { tables: [...B(), ...De()] },
1090
+ bindDataStore: (e, t) => {
1091
+ D = e, O = e, w = W(e), T = Ee(e), v === "memory" && t?.membership !== void 0 && (x = t.membership.onChange((e) => {
1092
+ e.kind === "left" || b || t.instanceId !== void 0 && e.instanceId === t.instanceId || (b = !0, d(be(e.instanceId, y), {
1093
+ instanceId: t.instanceId ?? null,
1094
+ peer: e.instanceId,
1095
+ indexes: Object.keys(o.indexes)
1096
+ }));
1097
+ })), !(m.resyncIntervalMs <= 0 || t?.scheduleCoordinated === void 0) && (E = t.scheduleCoordinated("search.resync", m.resyncIntervalMs, async () => {
1098
+ try {
1099
+ let e = await j(m.resyncBatchSize);
1100
+ return (e.repaired > 0 || e.failed > 0) && p("search: drift repair pass", e), { idle: e.scanned === 0 };
596
1101
  } catch (e) {
597
- return h(500, e.message);
1102
+ return d("search: drift repair pass failed", { error: String(e?.message ?? e) }), { idle: !1 };
598
1103
  }
599
- })
600
- }],
601
- extendSchema: { tables: [...j()] },
602
- bindDataStore: (e) => {
603
- p = e, f = L(e);
1104
+ }));
604
1105
  },
605
1106
  onChangeEvent: (e) => {
606
- let t = n.indexes[e.table];
607
- if (!t) return r.void;
608
- let i = r.tryPromise(() => f.bump(t.index, e.op === "delete" ? "removed" : "synced")).pipe(r.catchAll((t) => r.sync(() => d("search stats bump failed", {
609
- table: e.table,
610
- op: e.op,
611
- error: String(t)
612
- })))), a = r.tryPromise(() => B(o, t, e));
613
- return r.zipRight(i, a);
1107
+ let t = o.indexes[e.table];
1108
+ if (!t) return c.void;
1109
+ let n = e.changeScope, r = e.changeScope === "fleet" || e.origin !== "injected", i = e.op === "delete" ? e.old : e.new, a = typeof i?.id == "string" ? i.id : "", s = e.op === "delete" ? "remove" : "upsert", l = () => {
1110
+ if (e.op === "delete" || !e.new) return a;
1111
+ try {
1112
+ return String(t.map(e.new).id ?? a);
1113
+ } catch {
1114
+ return a;
1115
+ }
1116
+ };
1117
+ return c.tryPromise({
1118
+ try: () => Z(g, t, e),
1119
+ catch: je
1120
+ }).pipe(c.retry(h)).pipe(c.matchEffect({
1121
+ onSuccess: () => r ? A(t.index, e.op === "delete" ? "removed" : "synced", n) : c.void,
1122
+ onFailure: (r) => {
1123
+ let i = {
1124
+ table: e.table,
1125
+ index: t.index,
1126
+ op: e.op,
1127
+ rowId: a,
1128
+ transient: r.transient,
1129
+ error: r.message
1130
+ };
1131
+ return a === "" ? c.sync(() => d("search: change not indexed and NOT recoverable (row has no id)", i)).pipe(c.zipRight(c.fail(r))) : c.tryPromise(() => T.record({
1132
+ indexName: t.index,
1133
+ sourceTable: e.table,
1134
+ rowId: a,
1135
+ docId: l(),
1136
+ op: s,
1137
+ error: r.message
1138
+ })).pipe(c.matchEffect({
1139
+ onSuccess: () => A(t.index, "dropped", n).pipe(c.zipRight(c.sync(() => d("search: index drift recorded — will be repaired from the database", i)))),
1140
+ onFailure: (e) => c.sync(() => d("search: change LOST — engine failed and the drift ledger could not record it", {
1141
+ ...i,
1142
+ ledgerError: String(e)
1143
+ })).pipe(c.zipRight(c.fail(r)))
1144
+ }));
1145
+ }
1146
+ }));
614
1147
  },
615
- onActivate: (e) => r.sync(() => {
616
- d = (t, n) => e.logger.warn(t, n), e.logger.info("search active", {
617
- indexes: Object.keys(n.indexes),
618
- backend: typeof n.backend == "string" ? n.backend : "custom/vendor"
1148
+ onActivate: (e) => c.suspend(() => {
1149
+ d = (t, n) => e.logger.warn(t, n), p = (t, n) => e.logger.info(t, n);
1150
+ let t = ye({
1151
+ backendKind: v,
1152
+ production: (e.env?.NODE_ENV ?? process.env.NODE_ENV) === "production",
1153
+ singleProcessDeclared: y
619
1154
  });
1155
+ return t.level === "refuse" ? c.fail(new ge(t.message)) : (t.level === "note" && e.logger.info(t.message, { indexes: Object.keys(o.indexes) }), m.statsFlushIntervalMs > 0 && k === void 0 && (k = G({
1156
+ flush: K(() => w),
1157
+ intervalMs: m.statsFlushIntervalMs,
1158
+ maxBuffered: m.statsFlushMaxBuffered
1159
+ })), c.sync(() => {
1160
+ e.logger.info("search active", {
1161
+ indexes: Object.keys(o.indexes),
1162
+ backend: typeof o.backend == "string" ? o.backend : "custom/vendor",
1163
+ durable: v !== "memory",
1164
+ sync: m
1165
+ });
1166
+ }));
1167
+ }),
1168
+ onDeactivate: () => c.promise(async () => {
1169
+ E?.stop(), E = void 0, x?.(), x = void 0, b = !1;
1170
+ let e = k;
1171
+ k = void 0, await e?.shutdown();
620
1172
  })
621
1173
  });
622
1174
  };
623
1175
  //#endregion
624
- export { O as SEARCH_STATS_TABLE, f as SearchBackendError, E as algoliaBackend, B as applyChange, z as backfillIndex, L as dataStoreStatsStore, C as meilisearchBackend, g as memoryBackend, M as memoryStatsStore, H as scopeSearchFilters, U as searchPlugin, A as searchStatsTable, j as searchTables, b as typesenseBackend };
1176
+ export { x as FIELD_NAME_PATTERN, J as SEARCH_DRIFT_TABLE, L as SEARCH_STATS_TABLE, Q as SYNC_DEFAULTS, E as SearchBackendError, ge as SearchBackendNotDurable, t as SearchFieldRejected, e as SearchIndexNotFound, I as algoliaBackend, T as allowedEngineParams, Z as applyChange, je as asBackendError, ye as auditMemoryBackend, q as backendKindOf, ke as backfillIndex, Ee as dataStoreDriftLedger, W as dataStoreStatsStore, Se as driftSourceQuery, De as driftTables, S as isSafeFieldName, G as makeSearchStatsBuffer, F as meilisearchBackend, A as memoryBackend, we as memoryDriftLedger, V as memoryStatsStore, be as peerObservedWarning, Ae as resolveSyncOptions, w as sanitizeEngineParams, Pe as scopeSearchFilters, xe as searchDriftTable, Ie as searchPlugin, z as searchStatsTable, B as searchTables, K as statsStoreFlush, Me as syncRetrySchedule, oe as typesenseBackend };