@delali/narsil 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE +201 -0
  2. package/dist/adapters/filesystem.d.ts +8 -0
  3. package/dist/adapters/filesystem.mjs +107 -0
  4. package/dist/adapters/indexeddb.d.ts +9 -0
  5. package/dist/adapters/indexeddb.mjs +153 -0
  6. package/dist/adapters/memory.d.ts +5 -0
  7. package/dist/adapters/memory.mjs +30 -0
  8. package/dist/adapters-XODT2F3h.d.ts +30 -0
  9. package/dist/chunk-5HXMVMSM.mjs +3147 -0
  10. package/dist/chunk-JKIBUPAY.mjs +42 -0
  11. package/dist/chunk-N37VMC62.mjs +18 -0
  12. package/dist/chunk-WYC635GI.mjs +335 -0
  13. package/dist/direct-executor-MQD7JRD4.mjs +3 -0
  14. package/dist/index.d.ts +386 -0
  15. package/dist/index.mjs +1099 -0
  16. package/dist/invalidation/broadcast-channel.d.ts +8 -0
  17. package/dist/invalidation/broadcast-channel.mjs +46 -0
  18. package/dist/invalidation/filesystem.d.ts +10 -0
  19. package/dist/invalidation/filesystem.mjs +159 -0
  20. package/dist/invalidation/noop.d.ts +5 -0
  21. package/dist/invalidation/noop.mjs +13 -0
  22. package/dist/language-ClUGLuoC.d.ts +13 -0
  23. package/dist/languages/arabic.d.ts +5 -0
  24. package/dist/languages/arabic.mjs +631 -0
  25. package/dist/languages/armenian.d.ts +5 -0
  26. package/dist/languages/armenian.mjs +306 -0
  27. package/dist/languages/bulgarian.d.ts +5 -0
  28. package/dist/languages/bulgarian.mjs +330 -0
  29. package/dist/languages/chinese.d.ts +5 -0
  30. package/dist/languages/chinese.mjs +250 -0
  31. package/dist/languages/dagbani.d.ts +5 -0
  32. package/dist/languages/dagbani.mjs +85 -0
  33. package/dist/languages/danish.d.ts +5 -0
  34. package/dist/languages/danish.mjs +224 -0
  35. package/dist/languages/dutch.d.ts +5 -0
  36. package/dist/languages/dutch.mjs +296 -0
  37. package/dist/languages/english.d.ts +5 -0
  38. package/dist/languages/english.mjs +1 -0
  39. package/dist/languages/ewe.d.ts +5 -0
  40. package/dist/languages/ewe.mjs +85 -0
  41. package/dist/languages/finnish.d.ts +5 -0
  42. package/dist/languages/finnish.mjs +536 -0
  43. package/dist/languages/french.d.ts +5 -0
  44. package/dist/languages/french.mjs +635 -0
  45. package/dist/languages/ga.d.ts +5 -0
  46. package/dist/languages/ga.mjs +69 -0
  47. package/dist/languages/german.d.ts +5 -0
  48. package/dist/languages/german.mjs +414 -0
  49. package/dist/languages/greek.d.ts +5 -0
  50. package/dist/languages/greek.mjs +888 -0
  51. package/dist/languages/hausa.d.ts +5 -0
  52. package/dist/languages/hausa.mjs +243 -0
  53. package/dist/languages/hindi.d.ts +5 -0
  54. package/dist/languages/hindi.mjs +333 -0
  55. package/dist/languages/hungarian.d.ts +5 -0
  56. package/dist/languages/hungarian.mjs +600 -0
  57. package/dist/languages/igbo.d.ts +5 -0
  58. package/dist/languages/igbo.mjs +96 -0
  59. package/dist/languages/indonesian.d.ts +5 -0
  60. package/dist/languages/indonesian.mjs +896 -0
  61. package/dist/languages/irish.d.ts +5 -0
  62. package/dist/languages/irish.mjs +268 -0
  63. package/dist/languages/italian.d.ts +5 -0
  64. package/dist/languages/italian.mjs +713 -0
  65. package/dist/languages/japanese.d.ts +5 -0
  66. package/dist/languages/japanese.mjs +210 -0
  67. package/dist/languages/nepali.d.ts +5 -0
  68. package/dist/languages/nepali.mjs +262 -0
  69. package/dist/languages/norwegian.d.ts +5 -0
  70. package/dist/languages/norwegian.mjs +274 -0
  71. package/dist/languages/portuguese.d.ts +5 -0
  72. package/dist/languages/portuguese.mjs +606 -0
  73. package/dist/languages/romanian.d.ts +5 -0
  74. package/dist/languages/romanian.mjs +634 -0
  75. package/dist/languages/russian.d.ts +5 -0
  76. package/dist/languages/russian.mjs +424 -0
  77. package/dist/languages/sanskrit.d.ts +5 -0
  78. package/dist/languages/sanskrit.mjs +155 -0
  79. package/dist/languages/serbian.d.ts +5 -0
  80. package/dist/languages/serbian.mjs +277 -0
  81. package/dist/languages/slovenian.d.ts +5 -0
  82. package/dist/languages/slovenian.mjs +522 -0
  83. package/dist/languages/spanish.d.ts +5 -0
  84. package/dist/languages/spanish.mjs +606 -0
  85. package/dist/languages/swahili.d.ts +5 -0
  86. package/dist/languages/swahili.mjs +212 -0
  87. package/dist/languages/swedish.d.ts +5 -0
  88. package/dist/languages/swedish.mjs +224 -0
  89. package/dist/languages/tamil.d.ts +5 -0
  90. package/dist/languages/tamil.mjs +186 -0
  91. package/dist/languages/turkish.d.ts +5 -0
  92. package/dist/languages/turkish.mjs +675 -0
  93. package/dist/languages/twi.d.ts +5 -0
  94. package/dist/languages/twi.mjs +84 -0
  95. package/dist/languages/ukrainian.d.ts +5 -0
  96. package/dist/languages/ukrainian.mjs +444 -0
  97. package/dist/languages/yoruba.d.ts +5 -0
  98. package/dist/languages/yoruba.mjs +103 -0
  99. package/dist/languages/zulu.d.ts +5 -0
  100. package/dist/languages/zulu.mjs +113 -0
  101. package/dist/workers/entry.d.ts +3 -0
  102. package/dist/workers/entry.mjs +106 -0
  103. package/package.json +87 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,1099 @@
1
+ import { createDirectExecutor, fanOutQuery, tokenize, validateSchema, getLanguage } from './chunk-5HXMVMSM.mjs';
2
+ export { getLanguage, registerLanguage } from './chunk-5HXMVMSM.mjs';
3
+ import './chunk-WYC635GI.mjs';
4
+ import { NarsilError, ErrorCodes } from './chunk-JKIBUPAY.mjs';
5
+ export { ErrorCodes, NarsilError } from './chunk-JKIBUPAY.mjs';
6
+ import { generateId } from './chunk-N37VMC62.mjs';
7
+
8
+ // src/highlighting/highlighter.ts
9
+ function buildTokenCharOffsets(text, originalTokens) {
10
+ const offsets = [];
11
+ let searchFrom = 0;
12
+ for (const token of originalTokens) {
13
+ const idx = text.indexOf(token, searchFrom);
14
+ if (idx === -1) continue;
15
+ offsets.push({ start: idx, end: idx + token.length });
16
+ searchFrom = idx + token.length;
17
+ }
18
+ return offsets;
19
+ }
20
+ function mergeOverlapping(ranges) {
21
+ if (ranges.length === 0) return [];
22
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
23
+ const merged = [sorted[0]];
24
+ for (let i = 1; i < sorted.length; i++) {
25
+ const prev = merged[merged.length - 1];
26
+ const curr = sorted[i];
27
+ if (curr.start <= prev.end) {
28
+ prev.end = Math.max(prev.end, curr.end);
29
+ } else {
30
+ merged.push(curr);
31
+ }
32
+ }
33
+ return merged;
34
+ }
35
+ function findDensestWindow(positions, text, windowSize) {
36
+ if (positions.length === 0) {
37
+ return { start: 0, end: Math.min(windowSize, text.length) };
38
+ }
39
+ let bestStart = 0;
40
+ let bestCount = 0;
41
+ for (const pos of positions) {
42
+ const windowStart = Math.max(0, pos.start - Math.floor(windowSize / 2));
43
+ const windowEnd = Math.min(text.length, windowStart + windowSize);
44
+ const adjustedStart = Math.max(0, windowEnd - windowSize);
45
+ let count = 0;
46
+ for (const p of positions) {
47
+ if (p.start >= adjustedStart && p.end <= windowEnd) {
48
+ count++;
49
+ }
50
+ }
51
+ if (count > bestCount) {
52
+ bestCount = count;
53
+ bestStart = adjustedStart;
54
+ }
55
+ }
56
+ return { start: bestStart, end: Math.min(text.length, bestStart + windowSize) };
57
+ }
58
+ function applySnippetTruncation(taggedText, originalText, positions, maxLen, preTag, postTag) {
59
+ if (maxLen <= 0 || originalText.length <= maxLen) {
60
+ return { snippet: taggedText, adjustedPositions: positions };
61
+ }
62
+ const window = findDensestWindow(positions, originalText, maxLen);
63
+ const snippetRaw = originalText.substring(window.start, window.end);
64
+ const adjustedPositions = [];
65
+ for (const pos of positions) {
66
+ if (pos.start >= window.start && pos.end <= window.end) {
67
+ adjustedPositions.push({
68
+ start: pos.start - window.start,
69
+ end: pos.end - window.start
70
+ });
71
+ }
72
+ }
73
+ let snippet = snippetRaw;
74
+ const sortedDesc = [...adjustedPositions].sort((a, b) => b.start - a.start);
75
+ for (const pos of sortedDesc) {
76
+ snippet = snippet.substring(0, pos.start) + preTag + snippet.substring(pos.start, pos.end) + postTag + snippet.substring(pos.end);
77
+ }
78
+ const needsLeadingEllipsis = window.start > 0;
79
+ const needsTrailingEllipsis = window.end < originalText.length;
80
+ if (needsLeadingEllipsis) snippet = `...${snippet}`;
81
+ if (needsTrailingEllipsis) snippet = `${snippet}...`;
82
+ return { snippet, adjustedPositions };
83
+ }
84
+ function highlightField(text, queryTokens, language, options) {
85
+ const preTag = options?.preTag ?? "<mark>";
86
+ const postTag = options?.postTag ?? "</mark>";
87
+ const maxSnippetLength = options?.maxSnippetLength ?? 200;
88
+ if (text.length === 0) {
89
+ return { snippet: text, positions: [] };
90
+ }
91
+ const fieldResult = tokenize(text, language, { stem: false, removeStopWords: false });
92
+ if (fieldResult.tokens.length === 0) {
93
+ return { snippet: text, positions: [] };
94
+ }
95
+ const lowercaseText = text.normalize("NFC").toLowerCase();
96
+ const charOffsets = buildTokenCharOffsets(lowercaseText, fieldResult.originalTokens);
97
+ const stemmedQueryTokens = queryTokens.map((qt) => {
98
+ if (language.stemmer) {
99
+ return language.stemmer(qt.token.toLowerCase());
100
+ }
101
+ return qt.token.toLowerCase();
102
+ });
103
+ const matchedRanges = [];
104
+ for (let i = 0; i < fieldResult.tokens.length; i++) {
105
+ if (i >= charOffsets.length) break;
106
+ const fieldToken = fieldResult.tokens[i].token;
107
+ const stemmedField = language.stemmer ? language.stemmer(fieldToken) : fieldToken;
108
+ for (const stemmedQuery of stemmedQueryTokens) {
109
+ if (stemmedField === stemmedQuery) {
110
+ matchedRanges.push({ start: charOffsets[i].start, end: charOffsets[i].end });
111
+ break;
112
+ }
113
+ }
114
+ }
115
+ const mergedPositions = mergeOverlapping(matchedRanges);
116
+ if (mergedPositions.length === 0) {
117
+ if (maxSnippetLength > 0 && text.length > maxSnippetLength) {
118
+ const truncated = text.substring(0, maxSnippetLength);
119
+ return { snippet: `${truncated}...`, positions: [] };
120
+ }
121
+ return { snippet: text, positions: [] };
122
+ }
123
+ if (maxSnippetLength > 0 && text.length > maxSnippetLength) {
124
+ const { snippet, adjustedPositions } = applySnippetTruncation(
125
+ text,
126
+ text,
127
+ mergedPositions,
128
+ maxSnippetLength,
129
+ preTag,
130
+ postTag
131
+ );
132
+ return { snippet, positions: adjustedPositions };
133
+ }
134
+ let result = text;
135
+ const sortedDesc = [...mergedPositions].sort((a, b) => b.start - a.start);
136
+ for (const pos of sortedDesc) {
137
+ result = result.substring(0, pos.start) + preTag + result.substring(pos.start, pos.end) + postTag + result.substring(pos.end);
138
+ }
139
+ return { snippet: result, positions: mergedPositions };
140
+ }
141
+
142
+ // src/persistence/flush-manager.ts
143
+ var DEFAULT_INTERVAL = 5e3;
144
+ var DEFAULT_MUTATION_THRESHOLD = 1e3;
145
+ var DEFAULT_MAX_RETRIES = 3;
146
+ var DEFAULT_BASE_RETRY_DELAY = 100;
147
+ function createFlushManager(config, getPartitionData, getInstanceId) {
148
+ const {
149
+ persistence,
150
+ invalidation,
151
+ interval = DEFAULT_INTERVAL,
152
+ mutationThreshold = DEFAULT_MUTATION_THRESHOLD,
153
+ maxRetries = DEFAULT_MAX_RETRIES,
154
+ baseRetryDelay = DEFAULT_BASE_RETRY_DELAY,
155
+ onError
156
+ } = config;
157
+ const dirtyPartitions = /* @__PURE__ */ new Map();
158
+ let mutationCount = 0;
159
+ let flushTimerId = null;
160
+ let isShuttingDown = false;
161
+ let flushPromise = null;
162
+ function startTimer() {
163
+ if (flushTimerId !== null || isShuttingDown) {
164
+ return;
165
+ }
166
+ flushTimerId = setInterval(() => {
167
+ flush();
168
+ }, interval);
169
+ }
170
+ function addToDirtySet(indexName, partitionId) {
171
+ let partitions = dirtyPartitions.get(indexName);
172
+ if (partitions === void 0) {
173
+ partitions = /* @__PURE__ */ new Set();
174
+ dirtyPartitions.set(indexName, partitions);
175
+ }
176
+ partitions.add(partitionId);
177
+ }
178
+ function snapshotAndClearDirty() {
179
+ const snapshot = /* @__PURE__ */ new Map();
180
+ for (const [indexName, partitions] of dirtyPartitions) {
181
+ snapshot.set(indexName, new Set(partitions));
182
+ }
183
+ dirtyPartitions.clear();
184
+ return snapshot;
185
+ }
186
+ async function saveWithRetry(indexName, partitionId) {
187
+ const key = `${indexName}/${partitionId}`;
188
+ const data = getPartitionData(indexName, partitionId);
189
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
190
+ try {
191
+ await persistence.save(key, data);
192
+ return true;
193
+ } catch (err) {
194
+ const isLastAttempt = attempt === maxRetries - 1;
195
+ if (isLastAttempt) {
196
+ onError?.(indexName, partitionId, err, true);
197
+ return false;
198
+ }
199
+ const cappedAttempt = Math.min(attempt, 20);
200
+ const delay = baseRetryDelay * 2 ** cappedAttempt * (0.5 + Math.random() * 0.5);
201
+ await new Promise((resolve) => setTimeout(resolve, delay));
202
+ }
203
+ }
204
+ return false;
205
+ }
206
+ async function performFlush() {
207
+ const snapshot = snapshotAndClearDirty();
208
+ for (const [indexName, partitions] of snapshot) {
209
+ const succeededPartitions = [];
210
+ for (const partitionId of partitions) {
211
+ const saved = await saveWithRetry(indexName, partitionId);
212
+ if (saved) {
213
+ succeededPartitions.push(partitionId);
214
+ } else {
215
+ addToDirtySet(indexName, partitionId);
216
+ }
217
+ }
218
+ if (succeededPartitions.length > 0) {
219
+ await invalidation.publish({
220
+ type: "partition",
221
+ indexName,
222
+ partitions: succeededPartitions,
223
+ timestamp: Date.now(),
224
+ sourceInstanceId: getInstanceId()
225
+ });
226
+ }
227
+ }
228
+ mutationCount = 0;
229
+ }
230
+ function flush() {
231
+ if (flushPromise !== null) {
232
+ return flushPromise;
233
+ }
234
+ flushPromise = performFlush().finally(() => {
235
+ flushPromise = null;
236
+ });
237
+ return flushPromise;
238
+ }
239
+ function markDirty(indexName, partitionId) {
240
+ if (isShuttingDown) return;
241
+ addToDirtySet(indexName, partitionId);
242
+ mutationCount++;
243
+ if (mutationCount >= mutationThreshold) {
244
+ flush();
245
+ }
246
+ startTimer();
247
+ }
248
+ async function shutdown() {
249
+ isShuttingDown = true;
250
+ if (flushTimerId !== null) {
251
+ clearInterval(flushTimerId);
252
+ flushTimerId = null;
253
+ }
254
+ await flush();
255
+ }
256
+ return {
257
+ markDirty,
258
+ flush,
259
+ shutdown
260
+ };
261
+ }
262
+
263
+ // src/plugins/registry.ts
264
+ function isThenable(value) {
265
+ return value !== null && typeof value === "object" && typeof value.then === "function";
266
+ }
267
+ async function continueAsync(pending, hooks, startIndex, context) {
268
+ await pending;
269
+ for (let i = startIndex; i < hooks.length; i++) {
270
+ await hooks[i](context);
271
+ }
272
+ }
273
+ function createPluginRegistry() {
274
+ const plugins = [];
275
+ return {
276
+ register(plugin) {
277
+ plugins.push(plugin);
278
+ },
279
+ runHook(hookName, context) {
280
+ const hooks = [];
281
+ for (const plugin of plugins) {
282
+ const hook = plugin[hookName];
283
+ if (typeof hook === "function") {
284
+ hooks.push(hook.bind(plugin));
285
+ }
286
+ }
287
+ if (hooks.length === 0) return;
288
+ for (let i = 0; i < hooks.length; i++) {
289
+ const result = hooks[i](context);
290
+ if (isThenable(result)) {
291
+ return continueAsync(result, hooks, i + 1, context);
292
+ }
293
+ }
294
+ }
295
+ };
296
+ }
297
+
298
+ // src/search/grouping.ts
299
+ function getNestedValue(obj, path) {
300
+ const segments = path.split(".");
301
+ let current = obj;
302
+ for (const segment of segments) {
303
+ if (current === null || current === void 0 || typeof current !== "object") {
304
+ return void 0;
305
+ }
306
+ if (!Object.hasOwn(current, segment)) {
307
+ return void 0;
308
+ }
309
+ current = current[segment];
310
+ }
311
+ return current;
312
+ }
313
+ function applyGrouping(hits, group, getDocument) {
314
+ if (!group.fields || group.fields.length === 0) {
315
+ return [{ values: {}, hits }];
316
+ }
317
+ const groupMap = /* @__PURE__ */ new Map();
318
+ for (const hit of hits) {
319
+ const doc = getDocument(hit.id);
320
+ const fieldValues = [];
321
+ const valuesRecord = {};
322
+ for (const field of group.fields) {
323
+ const value = doc ? getNestedValue(doc, field) : void 0;
324
+ fieldValues.push(value);
325
+ valuesRecord[field] = value;
326
+ }
327
+ const compositeKey = JSON.stringify(fieldValues);
328
+ const existing = groupMap.get(compositeKey);
329
+ if (existing) {
330
+ existing.hits.push(hit);
331
+ } else {
332
+ groupMap.set(compositeKey, { values: valuesRecord, hits: [hit] });
333
+ }
334
+ }
335
+ const groups = [];
336
+ for (const entry of groupMap.values()) {
337
+ let groupHits = entry.hits;
338
+ if (group.maxPerGroup !== void 0 && group.maxPerGroup > 0) {
339
+ groupHits = groupHits.slice(0, group.maxPerGroup);
340
+ }
341
+ const result = { values: entry.values, hits: groupHits };
342
+ if (group.reduce) {
343
+ try {
344
+ let accumulator = group.reduce.initialValue();
345
+ for (const hit of groupHits) {
346
+ const doc = getDocument(hit.id);
347
+ if (doc) {
348
+ accumulator = group.reduce.reducer(accumulator, doc, hit.score);
349
+ }
350
+ }
351
+ ;
352
+ result.reduced = accumulator;
353
+ } catch (err) {
354
+ result.reducerError = err instanceof Error ? err.message : String(err);
355
+ }
356
+ }
357
+ groups.push(result);
358
+ }
359
+ groups.sort((a, b) => {
360
+ const aScore = a.hits.length > 0 ? a.hits[0].score : 0;
361
+ const bScore = b.hits.length > 0 ? b.hits[0].score : 0;
362
+ return bScore - aScore;
363
+ });
364
+ return groups;
365
+ }
366
+
367
+ // src/search/pagination.ts
368
+ function encodeCursor(state) {
369
+ const json = JSON.stringify(state);
370
+ if (typeof Buffer !== "undefined") {
371
+ return Buffer.from(json).toString("base64");
372
+ }
373
+ return btoa(json);
374
+ }
375
+ function decodeCursor(encoded) {
376
+ let json;
377
+ try {
378
+ if (typeof Buffer !== "undefined") {
379
+ json = Buffer.from(encoded, "base64").toString("utf-8");
380
+ } else {
381
+ json = atob(encoded);
382
+ }
383
+ } catch {
384
+ throw new NarsilError(ErrorCodes.SEARCH_INVALID_CURSOR, "Failed to decode cursor: invalid base64 encoding", {
385
+ cursor: encoded
386
+ });
387
+ }
388
+ let parsed;
389
+ try {
390
+ parsed = JSON.parse(json);
391
+ } catch {
392
+ throw new NarsilError(ErrorCodes.SEARCH_INVALID_CURSOR, "Failed to decode cursor: invalid JSON", {
393
+ cursor: encoded
394
+ });
395
+ }
396
+ if (!Array.isArray(parsed)) {
397
+ throw new NarsilError(ErrorCodes.SEARCH_INVALID_CURSOR, "Invalid cursor structure: expected an array", {
398
+ cursor: encoded
399
+ });
400
+ }
401
+ for (let i = 0; i < parsed.length; i++) {
402
+ const entry = parsed[i];
403
+ if (typeof entry !== "object" || entry === null) {
404
+ throw new NarsilError(
405
+ ErrorCodes.SEARCH_INVALID_CURSOR,
406
+ `Invalid cursor entry at index ${i}: expected an object`,
407
+ {
408
+ cursor: encoded,
409
+ index: i
410
+ }
411
+ );
412
+ }
413
+ const { s, d, p } = entry;
414
+ if (typeof s !== "number" || !Number.isFinite(s)) {
415
+ throw new NarsilError(
416
+ ErrorCodes.SEARCH_INVALID_CURSOR,
417
+ `Invalid cursor entry at index ${i}: "s" must be a finite number`,
418
+ { cursor: encoded, index: i }
419
+ );
420
+ }
421
+ if (typeof d !== "string") {
422
+ throw new NarsilError(
423
+ ErrorCodes.SEARCH_INVALID_CURSOR,
424
+ `Invalid cursor entry at index ${i}: "d" must be a string`,
425
+ { cursor: encoded, index: i }
426
+ );
427
+ }
428
+ if (typeof p !== "number" || !Number.isInteger(p) || p < 0) {
429
+ throw new NarsilError(
430
+ ErrorCodes.SEARCH_INVALID_CURSOR,
431
+ `Invalid cursor entry at index ${i}: "p" must be a non-negative integer`,
432
+ { cursor: encoded, index: i }
433
+ );
434
+ }
435
+ }
436
+ return parsed;
437
+ }
438
+ function applyPagination(results, limit, offset, cursor) {
439
+ if (limit <= 0) {
440
+ return { paginated: [] };
441
+ }
442
+ let startIndex = 0;
443
+ if (cursor) {
444
+ const cursorState = decodeCursor(cursor);
445
+ const anchor = cursorState[0];
446
+ for (let i = 0; i < results.length; i++) {
447
+ const result = results[i];
448
+ if (result.score < anchor.s || result.score === anchor.s && result.id > anchor.d) {
449
+ startIndex = i;
450
+ break;
451
+ }
452
+ if (i === results.length - 1) {
453
+ startIndex = results.length;
454
+ }
455
+ }
456
+ }
457
+ const afterOffset = startIndex + offset;
458
+ const sliced = results.slice(afterOffset, afterOffset + limit);
459
+ let nextCursor;
460
+ const hasMore = afterOffset + limit < results.length;
461
+ if (hasMore && sliced.length > 0) {
462
+ const lastResult = sliced[sliced.length - 1];
463
+ const cursorState = [{ s: lastResult.score, d: lastResult.id, p: 0 }];
464
+ nextCursor = encodeCursor(cursorState);
465
+ }
466
+ return { paginated: sliced, nextCursor };
467
+ }
468
+
469
+ // src/search/pinning.ts
470
+ function applyPinning(hits, pinned, resolveDoc) {
471
+ const result = hits.slice();
472
+ const pinnedDocIds = new Set(pinned.map((p) => p.docId));
473
+ for (let i = result.length - 1; i >= 0; i--) {
474
+ if (pinnedDocIds.has(result[i].id)) {
475
+ result.splice(i, 1);
476
+ }
477
+ }
478
+ const sorted = pinned.slice().sort((a, b) => a.position - b.position);
479
+ for (const entry of sorted) {
480
+ const doc = resolveDoc(entry.docId);
481
+ if (!doc) continue;
482
+ const pos = Math.max(0, Math.min(entry.position, result.length));
483
+ result.splice(pos, 0, doc);
484
+ }
485
+ return result;
486
+ }
487
+
488
+ // src/search/sorting.ts
489
+ function getNestedValue2(obj, path) {
490
+ const segments = path.split(".");
491
+ let current = obj;
492
+ for (const segment of segments) {
493
+ if (current === null || current === void 0 || typeof current !== "object") {
494
+ return void 0;
495
+ }
496
+ if (!Object.hasOwn(current, segment)) {
497
+ return void 0;
498
+ }
499
+ current = current[segment];
500
+ }
501
+ return current;
502
+ }
503
+ function applySorting(hits, sort, getDocument) {
504
+ const sortFields = Object.entries(sort);
505
+ if (sortFields.length === 0) return hits;
506
+ const collator = new Intl.Collator(void 0, { sensitivity: "base" });
507
+ const valueCache = /* @__PURE__ */ new Map();
508
+ for (const hit of hits) {
509
+ const doc = getDocument(hit.id);
510
+ if (!doc) {
511
+ valueCache.set(
512
+ hit.id,
513
+ sortFields.map(() => void 0)
514
+ );
515
+ continue;
516
+ }
517
+ const values = sortFields.map(([field]) => getNestedValue2(doc, field));
518
+ valueCache.set(hit.id, values);
519
+ }
520
+ const sorted = hits.slice();
521
+ sorted.sort((a, b) => {
522
+ const aValues = valueCache.get(a.id) ?? [];
523
+ const bValues = valueCache.get(b.id) ?? [];
524
+ for (let i = 0; i < sortFields.length; i++) {
525
+ const direction = sortFields[i][1];
526
+ const aVal = aValues[i];
527
+ const bVal = bValues[i];
528
+ const aIsNullish = aVal === void 0 || aVal === null;
529
+ const bIsNullish = bVal === void 0 || bVal === null;
530
+ if (aIsNullish && bIsNullish) continue;
531
+ if (aIsNullish) return 1;
532
+ if (bIsNullish) return -1;
533
+ let comparison = 0;
534
+ if (typeof aVal === "string" && typeof bVal === "string") {
535
+ comparison = collator.compare(aVal, bVal);
536
+ } else if (typeof aVal === "number" && typeof bVal === "number") {
537
+ const aIsNaN = Number.isNaN(aVal);
538
+ const bIsNaN = Number.isNaN(bVal);
539
+ if (aIsNaN && bIsNaN) continue;
540
+ if (aIsNaN) return 1;
541
+ if (bIsNaN) return -1;
542
+ comparison = aVal - bVal;
543
+ } else if (typeof aVal === "boolean" && typeof bVal === "boolean") {
544
+ comparison = aVal === bVal ? 0 : aVal ? 1 : -1;
545
+ }
546
+ if (comparison !== 0) {
547
+ return direction === "desc" ? -comparison : comparison;
548
+ }
549
+ }
550
+ return 0;
551
+ });
552
+ return sorted;
553
+ }
554
+
555
+ // src/workers/promoter.ts
556
+ var DEFAULT_PER_INDEX_THRESHOLD = 1e4;
557
+ var DEFAULT_TOTAL_THRESHOLD = 5e4;
558
+ function createExecutionPromoter(config) {
559
+ const perIndexThreshold = config?.perIndexThreshold ?? DEFAULT_PER_INDEX_THRESHOLD;
560
+ const totalThreshold = config?.totalThreshold ?? DEFAULT_TOTAL_THRESHOLD;
561
+ let promoted = false;
562
+ function check(indexes) {
563
+ if (promoted) {
564
+ return { shouldPromote: false, reason: "" };
565
+ }
566
+ let totalDocuments = 0;
567
+ for (const [indexName, stats] of indexes) {
568
+ if (stats.documentCount >= perIndexThreshold) {
569
+ return {
570
+ shouldPromote: true,
571
+ reason: `Index "${indexName}" has ${stats.documentCount} documents, exceeding the per-index threshold of ${perIndexThreshold}`
572
+ };
573
+ }
574
+ totalDocuments += stats.documentCount;
575
+ }
576
+ if (totalDocuments >= totalThreshold) {
577
+ return {
578
+ shouldPromote: true,
579
+ reason: `Total document count of ${totalDocuments} exceeds the total threshold of ${totalThreshold}`
580
+ };
581
+ }
582
+ return { shouldPromote: false, reason: "" };
583
+ }
584
+ function markPromoted() {
585
+ promoted = true;
586
+ }
587
+ function isPromoted() {
588
+ return promoted;
589
+ }
590
+ return { check, markPromoted, isPromoted };
591
+ }
592
+
593
+ // src/narsil.ts
594
+ var INDEX_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
595
+ var MAX_INDEX_NAME_LENGTH = 256;
596
+ var MAX_DOC_ID_LENGTH = 512;
597
+ var BATCH_CHUNK_SIZE = 1e3;
598
+ var MAX_LIMIT = 1e4;
599
+ var MAX_OFFSET = 1e5;
600
+ var DEFAULT_LIMIT = 10;
601
+ var DEFAULT_OFFSET = 0;
602
+ function now() {
603
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
604
+ return performance.now();
605
+ }
606
+ return Date.now();
607
+ }
608
+ function validateIndexName(name) {
609
+ if (!name || name.length === 0) {
610
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "Index name must not be empty", { indexName: name });
611
+ }
612
+ if (name.length > MAX_INDEX_NAME_LENGTH) {
613
+ throw new NarsilError(
614
+ ErrorCodes.INDEX_NOT_FOUND,
615
+ `Index name must not exceed ${MAX_INDEX_NAME_LENGTH} characters`,
616
+ { indexName: name, length: name.length }
617
+ );
618
+ }
619
+ if (!INDEX_NAME_PATTERN.test(name)) {
620
+ throw new NarsilError(
621
+ ErrorCodes.INDEX_NOT_FOUND,
622
+ `Index name "${name}" contains invalid characters; use alphanumeric, dots, hyphens, and underscores only`,
623
+ { indexName: name }
624
+ );
625
+ }
626
+ if (name.includes("..")) {
627
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index name "${name}" must not contain ".."`, { indexName: name });
628
+ }
629
+ }
630
+ function validateDocId(docId) {
631
+ if (!docId || docId.length === 0) {
632
+ throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not be empty", { docId });
633
+ }
634
+ if (docId.length > MAX_DOC_ID_LENGTH) {
635
+ throw new NarsilError(
636
+ ErrorCodes.DOC_VALIDATION_FAILED,
637
+ `Document ID must not exceed ${MAX_DOC_ID_LENGTH} characters`,
638
+ { docId, length: docId.length }
639
+ );
640
+ }
641
+ if (docId.includes("\0")) {
642
+ throw new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, "Document ID must not contain null bytes", { docId });
643
+ }
644
+ }
645
+ function clampLimit(limit) {
646
+ if (limit === void 0) return DEFAULT_LIMIT;
647
+ return Math.max(0, Math.min(limit, MAX_LIMIT));
648
+ }
649
+ function clampOffset(offset) {
650
+ if (offset === void 0) return DEFAULT_OFFSET;
651
+ return Math.max(0, Math.min(offset, MAX_OFFSET));
652
+ }
653
+ async function createNarsil(config) {
654
+ const executor = createDirectExecutor();
655
+ const promoter = createExecutionPromoter({
656
+ perIndexThreshold: config?.workers?.promotionThreshold,
657
+ totalThreshold: config?.workers?.totalPromotionThreshold
658
+ });
659
+ const pluginRegistry = createPluginRegistry();
660
+ if (config?.plugins) {
661
+ for (const plugin of config.plugins) {
662
+ pluginRegistry.register(plugin);
663
+ }
664
+ }
665
+ let flushManager = null;
666
+ if (config?.persistence) {
667
+ const noopInvalidation = config.invalidation ?? {
668
+ publish: async () => {
669
+ },
670
+ subscribe: async () => {
671
+ },
672
+ shutdown: async () => {
673
+ }
674
+ };
675
+ flushManager = createFlushManager(
676
+ {
677
+ persistence: config.persistence,
678
+ invalidation: noopInvalidation,
679
+ interval: config.flush?.interval,
680
+ mutationThreshold: config.flush?.mutationThreshold
681
+ },
682
+ () => new Uint8Array(0),
683
+ () => "instance-0"
684
+ );
685
+ }
686
+ const idGenerator = config?.idGenerator ?? generateId;
687
+ const indexRegistry = /* @__PURE__ */ new Map();
688
+ const eventHandlers = /* @__PURE__ */ new Map();
689
+ let isShutdown = false;
690
+ function guardShutdown() {
691
+ if (isShutdown) {
692
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, "This Narsil instance has been shut down");
693
+ }
694
+ }
695
+ function requireIndex(indexName) {
696
+ const entry = indexRegistry.get(indexName);
697
+ if (!entry) {
698
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index "${indexName}" does not exist`, { indexName });
699
+ }
700
+ return entry;
701
+ }
702
+ const narsil = {
703
+ async createIndex(name, indexConfig) {
704
+ guardShutdown();
705
+ validateIndexName(name);
706
+ if (indexRegistry.has(name)) {
707
+ throw new NarsilError(ErrorCodes.INDEX_ALREADY_EXISTS, `Index "${name}" already exists`, { indexName: name });
708
+ }
709
+ validateSchema(indexConfig.schema);
710
+ const language = getLanguage(indexConfig.language ?? "english");
711
+ executor.createIndex(name, indexConfig, language);
712
+ indexRegistry.set(name, { config: indexConfig, language });
713
+ await pluginRegistry.runHook("onIndexCreate", { indexName: name, config: indexConfig });
714
+ },
715
+ async dropIndex(name) {
716
+ guardShutdown();
717
+ const entry = requireIndex(name);
718
+ executor.dropIndex(name);
719
+ indexRegistry.delete(name);
720
+ await pluginRegistry.runHook("onIndexDrop", {
721
+ indexName: name,
722
+ config: entry.config
723
+ });
724
+ },
725
+ listIndexes() {
726
+ const infos = [];
727
+ for (const [name, entry] of indexRegistry) {
728
+ const manager = executor.getManager(name);
729
+ infos.push({
730
+ name,
731
+ documentCount: manager?.countDocuments() ?? 0,
732
+ partitionCount: manager?.partitionCount ?? 0,
733
+ language: entry.language.name
734
+ });
735
+ }
736
+ return infos;
737
+ },
738
+ getStats(indexName) {
739
+ guardShutdown();
740
+ const entry = requireIndex(indexName);
741
+ const manager = executor.getManager(indexName);
742
+ return {
743
+ documentCount: manager?.countDocuments() ?? 0,
744
+ partitionCount: manager?.partitionCount ?? 0,
745
+ memoryBytes: 0,
746
+ indexSizeBytes: 0,
747
+ language: entry.language.name,
748
+ schema: entry.config.schema
749
+ };
750
+ },
751
+ async insert(indexName, document, docId) {
752
+ guardShutdown();
753
+ requireIndex(indexName);
754
+ const resolvedDocId = docId ?? idGenerator();
755
+ validateDocId(resolvedDocId);
756
+ await pluginRegistry.runHook("beforeInsert", {
757
+ indexName,
758
+ docId: resolvedDocId,
759
+ document
760
+ });
761
+ await executor.execute({
762
+ type: "insert",
763
+ indexName,
764
+ docId: resolvedDocId,
765
+ document,
766
+ requestId: resolvedDocId
767
+ });
768
+ try {
769
+ await pluginRegistry.runHook("afterInsert", {
770
+ indexName,
771
+ docId: resolvedDocId,
772
+ document
773
+ });
774
+ } catch (err) {
775
+ console.warn("afterInsert plugin hook error:", err);
776
+ }
777
+ flushManager?.markDirty(indexName, 0);
778
+ const indexMap = /* @__PURE__ */ new Map();
779
+ for (const [name] of indexRegistry) {
780
+ const mgr = executor.getManager(name);
781
+ indexMap.set(name, { documentCount: mgr?.countDocuments() ?? 0 });
782
+ }
783
+ promoter.check(indexMap);
784
+ return resolvedDocId;
785
+ },
786
+ async insertBatch(indexName, documents) {
787
+ guardShutdown();
788
+ requireIndex(indexName);
789
+ const succeeded = [];
790
+ const failed = [];
791
+ for (let chunkStart = 0; chunkStart < documents.length; chunkStart += BATCH_CHUNK_SIZE) {
792
+ const chunkEnd = Math.min(chunkStart + BATCH_CHUNK_SIZE, documents.length);
793
+ for (let i = chunkStart; i < chunkEnd; i++) {
794
+ const docId = idGenerator();
795
+ try {
796
+ await narsil.insert(indexName, documents[i], docId);
797
+ succeeded.push(docId);
798
+ } catch (err) {
799
+ failed.push({
800
+ docId,
801
+ error: err instanceof NarsilError ? err : new NarsilError(ErrorCodes.DOC_VALIDATION_FAILED, String(err))
802
+ });
803
+ }
804
+ }
805
+ if (chunkEnd < documents.length) {
806
+ await new Promise((r) => setTimeout(r, 0));
807
+ }
808
+ }
809
+ return { succeeded, failed };
810
+ },
811
+ async remove(indexName, docId) {
812
+ guardShutdown();
813
+ requireIndex(indexName);
814
+ validateDocId(docId);
815
+ await pluginRegistry.runHook("beforeRemove", { indexName, docId });
816
+ await executor.execute({
817
+ type: "remove",
818
+ indexName,
819
+ docId,
820
+ requestId: docId
821
+ });
822
+ try {
823
+ await pluginRegistry.runHook("afterRemove", { indexName, docId });
824
+ } catch (err) {
825
+ console.warn("afterRemove plugin hook error:", err);
826
+ }
827
+ flushManager?.markDirty(indexName, 0);
828
+ },
829
+ async removeBatch(indexName, docIds) {
830
+ guardShutdown();
831
+ requireIndex(indexName);
832
+ const succeeded = [];
833
+ const failed = [];
834
+ for (let chunkStart = 0; chunkStart < docIds.length; chunkStart += BATCH_CHUNK_SIZE) {
835
+ const chunkEnd = Math.min(chunkStart + BATCH_CHUNK_SIZE, docIds.length);
836
+ for (let i = chunkStart; i < chunkEnd; i++) {
837
+ try {
838
+ await narsil.remove(indexName, docIds[i]);
839
+ succeeded.push(docIds[i]);
840
+ } catch (err) {
841
+ failed.push({
842
+ docId: docIds[i],
843
+ error: err instanceof NarsilError ? err : new NarsilError(ErrorCodes.DOC_NOT_FOUND, String(err))
844
+ });
845
+ }
846
+ }
847
+ if (chunkEnd < docIds.length) {
848
+ await new Promise((r) => setTimeout(r, 0));
849
+ }
850
+ }
851
+ return { succeeded, failed };
852
+ },
853
+ async update(indexName, docId, document) {
854
+ guardShutdown();
855
+ requireIndex(indexName);
856
+ validateDocId(docId);
857
+ const manager = executor.getManager(indexName);
858
+ const oldDocument = manager?.get(docId);
859
+ await pluginRegistry.runHook("beforeUpdate", {
860
+ indexName,
861
+ docId,
862
+ oldDocument: oldDocument ?? {},
863
+ newDocument: document
864
+ });
865
+ await executor.execute({
866
+ type: "update",
867
+ indexName,
868
+ docId,
869
+ document,
870
+ requestId: docId
871
+ });
872
+ try {
873
+ await pluginRegistry.runHook("afterUpdate", {
874
+ indexName,
875
+ docId,
876
+ oldDocument: oldDocument ?? {},
877
+ newDocument: document
878
+ });
879
+ } catch (err) {
880
+ console.warn("afterUpdate plugin hook error:", err);
881
+ }
882
+ flushManager?.markDirty(indexName, 0);
883
+ },
884
+ async updateBatch(indexName, updates) {
885
+ guardShutdown();
886
+ requireIndex(indexName);
887
+ const succeeded = [];
888
+ const failed = [];
889
+ for (let chunkStart = 0; chunkStart < updates.length; chunkStart += BATCH_CHUNK_SIZE) {
890
+ const chunkEnd = Math.min(chunkStart + BATCH_CHUNK_SIZE, updates.length);
891
+ for (let i = chunkStart; i < chunkEnd; i++) {
892
+ try {
893
+ await narsil.update(indexName, updates[i].docId, updates[i].document);
894
+ succeeded.push(updates[i].docId);
895
+ } catch (err) {
896
+ failed.push({
897
+ docId: updates[i].docId,
898
+ error: err instanceof NarsilError ? err : new NarsilError(ErrorCodes.DOC_NOT_FOUND, String(err))
899
+ });
900
+ }
901
+ }
902
+ if (chunkEnd < updates.length) {
903
+ await new Promise((r) => setTimeout(r, 0));
904
+ }
905
+ }
906
+ return { succeeded, failed };
907
+ },
908
+ async get(indexName, docId) {
909
+ guardShutdown();
910
+ requireIndex(indexName);
911
+ return executor.execute({ type: "get", indexName, docId, requestId: docId });
912
+ },
913
+ async getMultiple(indexName, docIds) {
914
+ guardShutdown();
915
+ requireIndex(indexName);
916
+ const result = /* @__PURE__ */ new Map();
917
+ for (const docId of docIds) {
918
+ const doc = await narsil.get(indexName, docId);
919
+ if (doc !== void 0) {
920
+ result.set(docId, doc);
921
+ }
922
+ }
923
+ return result;
924
+ },
925
+ async has(indexName, docId) {
926
+ guardShutdown();
927
+ requireIndex(indexName);
928
+ return executor.execute({ type: "has", indexName, docId, requestId: docId });
929
+ },
930
+ async countDocuments(indexName) {
931
+ guardShutdown();
932
+ requireIndex(indexName);
933
+ return executor.execute({ type: "count", indexName, requestId: indexName });
934
+ },
935
+ async query(indexName, params) {
936
+ guardShutdown();
937
+ const entry = requireIndex(indexName);
938
+ const startTime = now();
939
+ const limit = clampLimit(params.limit);
940
+ const offset = clampOffset(params.offset);
941
+ await pluginRegistry.runHook("beforeSearch", { indexName, params });
942
+ const manager = executor.getManager(indexName);
943
+ if (!manager) {
944
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index "${indexName}" manager not found`, { indexName });
945
+ }
946
+ const searchOptions = {
947
+ bm25Params: entry.config.bm25,
948
+ stopWords: entry.config.stopWords,
949
+ customTokenizer: entry.config.tokenizer
950
+ };
951
+ const fanOutResult = fanOutQuery(
952
+ manager,
953
+ params,
954
+ entry.language,
955
+ entry.config.schema,
956
+ {
957
+ scoringMode: params.scoring ?? entry.config.defaultScoring ?? "local"
958
+ },
959
+ searchOptions
960
+ );
961
+ let hits = fanOutResult.scored.map((scored) => ({
962
+ id: scored.docId,
963
+ score: scored.score,
964
+ document: manager.get(scored.docId) ?? {},
965
+ scoreComponents: {
966
+ termFrequencies: scored.termFrequencies,
967
+ fieldLengths: scored.fieldLengths,
968
+ idf: scored.idf
969
+ }
970
+ }));
971
+ if (params.highlight) {
972
+ const queryTokenResult = tokenize(params.term ?? "", entry.language, {
973
+ stem: true,
974
+ removeStopWords: true,
975
+ customTokenizer: entry.config.tokenizer,
976
+ stopWordOverride: entry.config.stopWords
977
+ });
978
+ for (const hit of hits) {
979
+ const highlights = {};
980
+ for (const field of params.highlight.fields) {
981
+ const doc = hit.document;
982
+ const fieldValue = doc[field];
983
+ if (typeof fieldValue === "string") {
984
+ highlights[field] = highlightField(fieldValue, queryTokenResult.tokens, entry.language, {
985
+ preTag: params.highlight.preTag,
986
+ postTag: params.highlight.postTag,
987
+ maxSnippetLength: params.highlight.maxSnippetLength
988
+ });
989
+ }
990
+ }
991
+ hit.highlights = highlights;
992
+ }
993
+ }
994
+ if (params.sort) {
995
+ hits = applySorting(hits, params.sort, (docId) => manager.get(docId));
996
+ }
997
+ let groups;
998
+ if (params.group) {
999
+ groups = applyGrouping(hits, params.group, (docId) => manager.get(docId));
1000
+ }
1001
+ if (params.pinned) {
1002
+ hits = applyPinning(hits, params.pinned, (docId) => {
1003
+ const doc = manager.get(docId);
1004
+ if (!doc) return void 0;
1005
+ return { id: docId, score: 0, document: doc };
1006
+ });
1007
+ }
1008
+ const { paginated, nextCursor } = applyPagination(hits, limit, offset, params.searchAfter);
1009
+ const elapsed = now() - startTime;
1010
+ const result = {
1011
+ hits: paginated,
1012
+ count: fanOutResult.totalMatched,
1013
+ elapsed,
1014
+ cursor: nextCursor,
1015
+ facets: fanOutResult.facets,
1016
+ groups
1017
+ };
1018
+ try {
1019
+ await pluginRegistry.runHook("afterSearch", {
1020
+ indexName,
1021
+ params,
1022
+ results: result
1023
+ });
1024
+ } catch (err) {
1025
+ console.warn("afterSearch plugin hook error:", err);
1026
+ }
1027
+ return result;
1028
+ },
1029
+ async preflight(indexName, params) {
1030
+ guardShutdown();
1031
+ const entry = requireIndex(indexName);
1032
+ const startTime = now();
1033
+ const manager = executor.getManager(indexName);
1034
+ if (!manager) {
1035
+ throw new NarsilError(ErrorCodes.INDEX_NOT_FOUND, `Index "${indexName}" manager not found`, { indexName });
1036
+ }
1037
+ const searchOptions = {
1038
+ bm25Params: entry.config.bm25,
1039
+ stopWords: entry.config.stopWords,
1040
+ customTokenizer: entry.config.tokenizer
1041
+ };
1042
+ const fanOutResult = fanOutQuery(
1043
+ manager,
1044
+ params,
1045
+ entry.language,
1046
+ entry.config.schema,
1047
+ {
1048
+ scoringMode: params.scoring ?? entry.config.defaultScoring ?? "local"
1049
+ },
1050
+ searchOptions
1051
+ );
1052
+ const elapsed = now() - startTime;
1053
+ return { count: fanOutResult.totalMatched, elapsed };
1054
+ },
1055
+ async clear(indexName) {
1056
+ guardShutdown();
1057
+ requireIndex(indexName);
1058
+ await executor.execute({ type: "clear", indexName, requestId: indexName });
1059
+ },
1060
+ getMemoryStats() {
1061
+ return { totalBytes: 0, workers: [] };
1062
+ },
1063
+ on(event, handler) {
1064
+ const key = event;
1065
+ let handlers = eventHandlers.get(key);
1066
+ if (!handlers) {
1067
+ handlers = /* @__PURE__ */ new Set();
1068
+ eventHandlers.set(key, handlers);
1069
+ }
1070
+ handlers.add(handler);
1071
+ },
1072
+ off(event, handler) {
1073
+ const key = event;
1074
+ const handlers = eventHandlers.get(key);
1075
+ if (handlers) {
1076
+ handlers.delete(handler);
1077
+ if (handlers.size === 0) {
1078
+ eventHandlers.delete(key);
1079
+ }
1080
+ }
1081
+ },
1082
+ async shutdown() {
1083
+ if (isShutdown) return;
1084
+ isShutdown = true;
1085
+ if (flushManager) {
1086
+ await flushManager.shutdown();
1087
+ }
1088
+ await executor.shutdown();
1089
+ eventHandlers.clear();
1090
+ indexRegistry.clear();
1091
+ }
1092
+ };
1093
+ return narsil;
1094
+ }
1095
+
1096
+ // src/index.ts
1097
+ var VERSION = "0.1.0";
1098
+
1099
+ export { VERSION, createNarsil };