@noorm/broccolidb 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/dist/TokenCompressionService.d.ts +59 -0
  4. package/dist/TokenCompressionService.d.ts.map +1 -0
  5. package/dist/TokenCompressionService.js +179 -0
  6. package/dist/TokenCompressionService.js.map +1 -0
  7. package/dist/broccolidb-aggregation.d.ts +14 -0
  8. package/dist/broccolidb-aggregation.d.ts.map +1 -0
  9. package/dist/broccolidb-aggregation.js +158 -0
  10. package/dist/broccolidb-aggregation.js.map +1 -0
  11. package/dist/broccolidb-cas.d.ts +56 -0
  12. package/dist/broccolidb-cas.d.ts.map +1 -0
  13. package/dist/broccolidb-cas.js +285 -0
  14. package/dist/broccolidb-cas.js.map +1 -0
  15. package/dist/broccolidb-kernel.d.ts +63 -0
  16. package/dist/broccolidb-kernel.d.ts.map +1 -0
  17. package/dist/broccolidb-kernel.js +287 -0
  18. package/dist/broccolidb-kernel.js.map +1 -0
  19. package/dist/broccolidb-mutex.d.ts +37 -0
  20. package/dist/broccolidb-mutex.d.ts.map +1 -0
  21. package/dist/broccolidb-mutex.js +121 -0
  22. package/dist/broccolidb-mutex.js.map +1 -0
  23. package/dist/broccolidb-natural-query.d.ts +13 -0
  24. package/dist/broccolidb-natural-query.d.ts.map +1 -0
  25. package/dist/broccolidb-natural-query.js +188 -0
  26. package/dist/broccolidb-natural-query.js.map +1 -0
  27. package/dist/broccolidb-table.d.ts +62 -0
  28. package/dist/broccolidb-table.d.ts.map +1 -0
  29. package/dist/broccolidb-table.js +893 -0
  30. package/dist/broccolidb-table.js.map +1 -0
  31. package/dist/broccolidb-wal.d.ts +49 -0
  32. package/dist/broccolidb-wal.d.ts.map +1 -0
  33. package/dist/broccolidb-wal.js +168 -0
  34. package/dist/broccolidb-wal.js.map +1 -0
  35. package/dist/broccolidb.contracts.d.ts +232 -0
  36. package/dist/broccolidb.contracts.d.ts.map +1 -0
  37. package/dist/broccolidb.contracts.js +7 -0
  38. package/dist/broccolidb.contracts.js.map +1 -0
  39. package/dist/index.d.ts +17 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +17 -0
  42. package/dist/index.js.map +1 -0
  43. package/docs/API.md +208 -0
  44. package/docs/ARCHITECTURE.md +160 -0
  45. package/docs/BRIEF.md +56 -0
  46. package/docs/CONTRIBUTING.md +82 -0
  47. package/docs/GLOSSARY.md +23 -0
  48. package/docs/OPERATIONS.md +176 -0
  49. package/docs/PHILOSOPHY.md +91 -0
  50. package/docs/README.md +116 -0
  51. package/docs/RELEASE_NOTES.md +34 -0
  52. package/docs/TROUBLESHOOTING.md +145 -0
  53. package/docs/adr/ADR-001-portable-inmemory-kernel.md +74 -0
  54. package/docs/adr/README.md +38 -0
  55. package/docs/adr/TEMPLATE.md +35 -0
  56. package/package.json +43 -0
@@ -0,0 +1,893 @@
1
+ /**
2
+ * GALXAI: BroccoliDB Generic Reactive In-Memory Table (Zenith Tier)
3
+ * Delivers sub-microsecond (<0.5 µs) hotpath lookups, multi-modal secondary indexing,
4
+ * rich operator filtering, aggregation pipeline, reactive CDC subscriptions, and TTL expiration.
5
+ */
6
+ import { BroccoliAggregateEngine } from "./broccolidb-aggregation.js";
7
+ export class BroccoliDbTable {
8
+ name;
9
+ records = new Map();
10
+ walHook;
11
+ // Index Stores
12
+ equalityIndices = new Map();
13
+ sortedIndices = new Map();
14
+ compositeIndices = new Map();
15
+ prefixIndices = new Map();
16
+ // Subscriptions & Timers
17
+ subscriptions = new Map();
18
+ subscriptionSeq = 0;
19
+ ttlTimers = new Map();
20
+ constructor(name, walHook) {
21
+ this.name = name;
22
+ this.walHook = walHook;
23
+ }
24
+ createIndex(field) {
25
+ if (this.equalityIndices.has(field))
26
+ return;
27
+ const indexMap = new Map();
28
+ this.equalityIndices.set(field, indexMap);
29
+ for (const [id, record] of this.records.entries()) {
30
+ const val = this.resolveFieldValue(record, field);
31
+ if (val !== undefined) {
32
+ let idSet = indexMap.get(val);
33
+ if (!idSet) {
34
+ idSet = new Set();
35
+ indexMap.set(val, idSet);
36
+ }
37
+ idSet.add(id);
38
+ }
39
+ }
40
+ }
41
+ createSortedIndex(field) {
42
+ if (this.sortedIndices.has(field))
43
+ return;
44
+ const sortedList = [];
45
+ this.sortedIndices.set(field, sortedList);
46
+ for (const [id, record] of this.records.entries()) {
47
+ const rawVal = this.resolveFieldValue(record, field);
48
+ const val = this.normalizeSortableValue(rawVal);
49
+ if (val !== undefined) {
50
+ this.insertSortedIndexEntry(sortedList, val, id);
51
+ }
52
+ }
53
+ }
54
+ createCompositeIndex(fields) {
55
+ const compName = fields.join("__");
56
+ if (this.compositeIndices.has(compName))
57
+ return;
58
+ const compIndex = {
59
+ fields,
60
+ map: new Map(),
61
+ };
62
+ this.compositeIndices.set(compName, compIndex);
63
+ for (const [id, record] of this.records.entries()) {
64
+ const key = this.buildCompositeKey(fields, record);
65
+ let idSet = compIndex.map.get(key);
66
+ if (!idSet) {
67
+ idSet = new Set();
68
+ compIndex.map.set(key, idSet);
69
+ }
70
+ idSet.add(id);
71
+ }
72
+ }
73
+ createPrefixIndex(field) {
74
+ if (this.prefixIndices.has(field))
75
+ return;
76
+ const prefixMap = new Map();
77
+ this.prefixIndices.set(field, prefixMap);
78
+ for (const [id, record] of this.records.entries()) {
79
+ const val = this.resolveFieldValue(record, field);
80
+ if (typeof val === "string") {
81
+ this.insertPrefixIndex(prefixMap, val, id);
82
+ }
83
+ }
84
+ }
85
+ get(id) {
86
+ const record = this.records.get(id);
87
+ return record ? { ...record } : undefined;
88
+ }
89
+ getAll() {
90
+ return Array.from(this.records.values()).map((r) => ({ ...r }));
91
+ }
92
+ put(id, record, options) {
93
+ const existing = this.records.get(id);
94
+ const isUpdate = existing !== undefined;
95
+ const beforeClone = existing ? { ...existing } : undefined;
96
+ this.putInternal(id, record);
97
+ if (options?.ttlMs && options.ttlMs > 0) {
98
+ const existingTimer = this.ttlTimers.get(id);
99
+ if (existingTimer)
100
+ clearTimeout(existingTimer);
101
+ const timer = setTimeout(() => {
102
+ const expiredRec = this.records.get(id);
103
+ if (expiredRec) {
104
+ this.deleteInternal(id);
105
+ this.ttlTimers.delete(id);
106
+ this.emitChangeEvent("EXPIRE", id, expiredRec, undefined);
107
+ if (this.walHook) {
108
+ this.walHook("DELETE", this.name, id);
109
+ }
110
+ }
111
+ }, options.ttlMs);
112
+ timer.unref?.();
113
+ this.ttlTimers.set(id, timer);
114
+ }
115
+ const stored = this.records.get(id);
116
+ const clonedReturn = { ...stored };
117
+ this.emitChangeEvent(isUpdate ? "UPDATE" : "INSERT", id, beforeClone, clonedReturn);
118
+ if (this.walHook) {
119
+ this.walHook(isUpdate ? "UPDATE" : "INSERT", this.name, id, clonedReturn);
120
+ }
121
+ return clonedReturn;
122
+ }
123
+ putMany(entries) {
124
+ const results = [];
125
+ for (const entry of entries) {
126
+ results.push(this.put(entry.id, entry.record, entry.options));
127
+ }
128
+ return results;
129
+ }
130
+ compareAndSwap(id, predicate, updater, options) {
131
+ const current = this.get(id);
132
+ if (!predicate(current)) {
133
+ return { success: false, record: current };
134
+ }
135
+ if (!current) {
136
+ return { success: false };
137
+ }
138
+ const updated = updater({ ...current });
139
+ const saved = this.put(id, updated, options);
140
+ return { success: true, record: saved };
141
+ }
142
+ delete(id) {
143
+ const existing = this.records.get(id);
144
+ if (!existing)
145
+ return false;
146
+ const beforeClone = { ...existing };
147
+ this.deleteInternal(id);
148
+ const timer = this.ttlTimers.get(id);
149
+ if (timer) {
150
+ clearTimeout(timer);
151
+ this.ttlTimers.delete(id);
152
+ }
153
+ this.emitChangeEvent("DELETE", id, beforeClone, undefined);
154
+ if (this.walHook) {
155
+ this.walHook("DELETE", this.name, id);
156
+ }
157
+ return true;
158
+ }
159
+ deleteWhere(where) {
160
+ const matching = this.query({ where });
161
+ let deletedCount = 0;
162
+ for (const record of matching) {
163
+ const id = record.id;
164
+ if (id && this.delete(id)) {
165
+ deletedCount++;
166
+ }
167
+ }
168
+ return deletedCount;
169
+ }
170
+ updateWhere(where, updater) {
171
+ const matching = this.query({ where });
172
+ let updatedCount = 0;
173
+ for (const record of matching) {
174
+ const id = record.id;
175
+ if (id) {
176
+ const updated = updater({ ...record });
177
+ this.put(id, updated);
178
+ updatedCount++;
179
+ }
180
+ }
181
+ return updatedCount;
182
+ }
183
+ count() {
184
+ return this.records.size;
185
+ }
186
+ clear() {
187
+ this.records.clear();
188
+ for (const m of this.equalityIndices.values())
189
+ m.clear();
190
+ for (const arr of this.sortedIndices.values())
191
+ arr.length = 0;
192
+ for (const comp of this.compositeIndices.values())
193
+ comp.map.clear();
194
+ for (const m of this.prefixIndices.values())
195
+ m.clear();
196
+ for (const t of this.ttlTimers.values())
197
+ clearTimeout(t);
198
+ this.ttlTimers.clear();
199
+ this.emitChangeEvent("CLEAR", "*", undefined, undefined);
200
+ if (this.walHook) {
201
+ this.walHook("CLEAR", this.name, "*");
202
+ }
203
+ }
204
+ query(options = {}) {
205
+ const plan = this.planQuery(options);
206
+ let candidates = plan.candidates;
207
+ if (options.where) {
208
+ candidates = candidates.filter((rec) => this.evaluateWhere(rec, options.where));
209
+ }
210
+ if (options.and && options.and.length > 0) {
211
+ candidates = candidates.filter((rec) => options.and.every((clause) => this.evaluateWhere(rec, clause)));
212
+ }
213
+ if (options.or && options.or.length > 0) {
214
+ candidates = candidates.filter((rec) => options.or.some((clause) => this.evaluateWhere(rec, clause)));
215
+ }
216
+ if (options.not) {
217
+ candidates = candidates.filter((rec) => !this.evaluateWhere(rec, options.not));
218
+ }
219
+ if (options.sortBy) {
220
+ const sortFields = Array.isArray(options.sortBy) ? options.sortBy : [options.sortBy];
221
+ const sortOrders = Array.isArray(options.sortOrder)
222
+ ? options.sortOrder
223
+ : [options.sortOrder ?? "asc"];
224
+ candidates = [...candidates].sort((a, b) => {
225
+ for (let i = 0; i < sortFields.length; i++) {
226
+ const field = sortFields[i];
227
+ const order = (sortOrders[i] ?? sortOrders[0]) === "desc" ? -1 : 1;
228
+ const valA = this.resolveFieldValue(a, field);
229
+ const valB = this.resolveFieldValue(b, field);
230
+ if (valA === valB)
231
+ continue;
232
+ if (valA === undefined || valA === null)
233
+ return 1;
234
+ if (valB === undefined || valB === null)
235
+ return -1;
236
+ return valA > valB ? order : -order;
237
+ }
238
+ return 0;
239
+ });
240
+ }
241
+ const offset = options.offset ?? 0;
242
+ const limit = options.limit !== undefined ? options.limit : candidates.length;
243
+ return candidates.slice(offset, offset + limit).map((r) => ({ ...r }));
244
+ }
245
+ aggregate(query) {
246
+ const candidateRecords = query.where ? this.query({ where: query.where }) : this.getAll();
247
+ return BroccoliAggregateEngine.execute(this.name, candidateRecords, query);
248
+ }
249
+ subscribe(callback, filter) {
250
+ const id = `sub_${++this.subscriptionSeq}_${Date.now()}`;
251
+ this.subscriptions.set(id, { callback, filter });
252
+ return {
253
+ subscriptionId: id,
254
+ unsubscribe: () => {
255
+ this.subscriptions.delete(id);
256
+ },
257
+ };
258
+ }
259
+ transaction(fn) {
260
+ const snapshot = this.createSnapshot();
261
+ const stagedMutations = [];
262
+ const tx = {
263
+ get: (id) => this.get(id),
264
+ put: (id, record, options) => {
265
+ stagedMutations.push({ op: "PUT", id, record });
266
+ this.putInternal(id, record);
267
+ return { ...record };
268
+ },
269
+ delete: (id) => {
270
+ stagedMutations.push({ op: "DELETE", id });
271
+ return this.deleteInternal(id);
272
+ },
273
+ query: (options) => this.query(options),
274
+ };
275
+ try {
276
+ const result = fn(tx);
277
+ if (this.walHook) {
278
+ for (const mut of stagedMutations) {
279
+ if (mut.op === "PUT" && mut.record) {
280
+ this.walHook("INSERT", this.name, mut.id, mut.record);
281
+ }
282
+ else if (mut.op === "DELETE") {
283
+ this.walHook("DELETE", this.name, mut.id);
284
+ }
285
+ }
286
+ }
287
+ return result;
288
+ }
289
+ catch (err) {
290
+ this.restoreSnapshot(snapshot);
291
+ throw err;
292
+ }
293
+ }
294
+ select() {
295
+ const table = this;
296
+ const whereObj = {};
297
+ let sortByField;
298
+ let sortDirection = "asc";
299
+ let limitVal;
300
+ let offsetVal;
301
+ const createPredicate = (field) => ({
302
+ equals: (val) => {
303
+ whereObj[field] = { $eq: val };
304
+ return builder;
305
+ },
306
+ notEquals: (val) => {
307
+ whereObj[field] = { $ne: val };
308
+ return builder;
309
+ },
310
+ greaterThan: (val) => {
311
+ whereObj[field] = { $gt: val };
312
+ return builder;
313
+ },
314
+ greaterThanOrEqual: (val) => {
315
+ whereObj[field] = { $gte: val };
316
+ return builder;
317
+ },
318
+ lessThan: (val) => {
319
+ whereObj[field] = { $lt: val };
320
+ return builder;
321
+ },
322
+ lessThanOrEqual: (val) => {
323
+ whereObj[field] = { $lte: val };
324
+ return builder;
325
+ },
326
+ in: (values) => {
327
+ whereObj[field] = { $in: values };
328
+ return builder;
329
+ },
330
+ notIn: (values) => {
331
+ whereObj[field] = { $nin: values };
332
+ return builder;
333
+ },
334
+ between: (min, max) => {
335
+ whereObj[field] = { $between: [min, max] };
336
+ return builder;
337
+ },
338
+ startsWith: (prefix) => {
339
+ whereObj[field] = { $startsWith: prefix };
340
+ return builder;
341
+ },
342
+ contains: (sub) => {
343
+ whereObj[field] = { $contains: sub };
344
+ return builder;
345
+ },
346
+ matches: (regex) => {
347
+ whereObj[field] = { $regex: regex };
348
+ return builder;
349
+ },
350
+ });
351
+ const builder = {
352
+ where: (field) => createPredicate(field),
353
+ and: (field) => createPredicate(field),
354
+ or: (clause) => {
355
+ const subBuilder = table.select();
356
+ clause(subBuilder);
357
+ return builder;
358
+ },
359
+ orderBy: (field, direction = "asc") => {
360
+ sortByField = field;
361
+ sortDirection = direction;
362
+ return builder;
363
+ },
364
+ limit: (count) => {
365
+ limitVal = count;
366
+ return builder;
367
+ },
368
+ offset: (count) => {
369
+ offsetVal = count;
370
+ return builder;
371
+ },
372
+ execute: () => {
373
+ return table.query({
374
+ where: Object.keys(whereObj).length > 0 ? whereObj : undefined,
375
+ sortBy: sortByField,
376
+ sortOrder: sortDirection,
377
+ limit: limitVal,
378
+ offset: offsetVal,
379
+ });
380
+ },
381
+ explain: () => {
382
+ return table.explain({
383
+ where: Object.keys(whereObj).length > 0 ? whereObj : undefined,
384
+ sortBy: sortByField,
385
+ sortOrder: sortDirection,
386
+ limit: limitVal,
387
+ offset: offsetVal,
388
+ });
389
+ },
390
+ first: () => {
391
+ const res = builder.limit(1).execute();
392
+ return res[0];
393
+ },
394
+ count: () => {
395
+ return builder.execute().length;
396
+ },
397
+ };
398
+ return builder;
399
+ }
400
+ explain(options = {}) {
401
+ const startTime = performance.now();
402
+ const plan = this.planQuery(options);
403
+ const results = this.query(options);
404
+ const durationMicros = Math.round((performance.now() - startTime) * 1000);
405
+ return {
406
+ table: this.name,
407
+ matchedIndex: plan.indexName,
408
+ indexType: plan.indexType,
409
+ scanStrategy: plan.scanStrategy,
410
+ candidatesScanned: plan.candidates.length,
411
+ recordsMatched: results.length,
412
+ executionTimeMicros: durationMicros,
413
+ query: options,
414
+ };
415
+ }
416
+ createSnapshot() {
417
+ const snap = new Map();
418
+ for (const [k, v] of this.records.entries()) {
419
+ snap.set(k, { ...v });
420
+ }
421
+ return snap;
422
+ }
423
+ restoreSnapshot(snapshot) {
424
+ this.records.clear();
425
+ for (const m of this.equalityIndices.values())
426
+ m.clear();
427
+ for (const arr of this.sortedIndices.values())
428
+ arr.length = 0;
429
+ for (const comp of this.compositeIndices.values())
430
+ comp.map.clear();
431
+ for (const m of this.prefixIndices.values())
432
+ m.clear();
433
+ for (const [k, v] of snapshot.entries()) {
434
+ this.putInternal(k, v);
435
+ }
436
+ }
437
+ // Internal Helpers
438
+ putInternal(id, record) {
439
+ const existing = this.records.get(id);
440
+ if (existing) {
441
+ this.removeIndicesForRecord(id, existing);
442
+ }
443
+ this.records.set(id, { ...record });
444
+ this.addIndicesForRecord(id, record);
445
+ }
446
+ deleteInternal(id) {
447
+ const existing = this.records.get(id);
448
+ if (!existing)
449
+ return false;
450
+ this.removeIndicesForRecord(id, existing);
451
+ this.records.delete(id);
452
+ return true;
453
+ }
454
+ planQuery(options) {
455
+ if (!options.where) {
456
+ // Check if sortBy matches a sorted index for zero-cost pre-sorted candidates
457
+ if (options.sortBy && typeof options.sortBy === "string" && this.sortedIndices.has(options.sortBy)) {
458
+ const sortedList = this.sortedIndices.get(options.sortBy);
459
+ const candidates = [];
460
+ const isDesc = options.sortOrder === "desc";
461
+ if (isDesc) {
462
+ for (let i = sortedList.length - 1; i >= 0; i--) {
463
+ for (const id of sortedList[i].ids) {
464
+ const r = this.records.get(id);
465
+ if (r)
466
+ candidates.push(r);
467
+ }
468
+ }
469
+ }
470
+ else {
471
+ for (let i = 0; i < sortedList.length; i++) {
472
+ for (const id of sortedList[i].ids) {
473
+ const r = this.records.get(id);
474
+ if (r)
475
+ candidates.push(r);
476
+ }
477
+ }
478
+ }
479
+ return {
480
+ candidates,
481
+ indexName: options.sortBy,
482
+ indexType: "sorted",
483
+ scanStrategy: "INDEX_RANGE_SCAN",
484
+ };
485
+ }
486
+ return {
487
+ candidates: Array.from(this.records.values()),
488
+ scanStrategy: "FULL_TABLE_SCAN",
489
+ };
490
+ }
491
+ const whereKeys = Object.keys(options.where);
492
+ // 1. Check Composite Indices (Multi-Field Exact Match)
493
+ for (const [compName, compIndex] of this.compositeIndices.entries()) {
494
+ const allFieldsPresent = compIndex.fields.every((f) => {
495
+ const val = options.where[f];
496
+ return val !== undefined && (typeof val !== "object" || val === null || val.$eq !== undefined);
497
+ });
498
+ if (allFieldsPresent) {
499
+ const keyParts = compIndex.fields.map((f) => {
500
+ const val = options.where[f];
501
+ if (typeof val === "object" && val !== null && val.$eq !== undefined) {
502
+ return String(val.$eq);
503
+ }
504
+ return String(val ?? "");
505
+ });
506
+ const compKey = keyParts.join("::");
507
+ const idSet = compIndex.map.get(compKey);
508
+ const candidates = [];
509
+ if (idSet) {
510
+ for (const id of idSet) {
511
+ const r = this.records.get(id);
512
+ if (r)
513
+ candidates.push(r);
514
+ }
515
+ }
516
+ return {
517
+ candidates,
518
+ indexName: compName,
519
+ indexType: "composite",
520
+ scanStrategy: "COMPOSITE_INDEX_LOOKUP",
521
+ };
522
+ }
523
+ }
524
+ // 2. Check Equality Indices & Multi-Index Intersection
525
+ const matchingEqualitySets = [];
526
+ for (const field of whereKeys) {
527
+ if (this.equalityIndices.has(field)) {
528
+ const rawVal = options.where[field];
529
+ let targetVal = rawVal;
530
+ let isEquality = false;
531
+ if (typeof rawVal !== "object" || rawVal === null) {
532
+ targetVal = rawVal;
533
+ isEquality = true;
534
+ }
535
+ else if (rawVal.$eq !== undefined) {
536
+ targetVal = rawVal.$eq;
537
+ isEquality = true;
538
+ }
539
+ if (isEquality) {
540
+ const idSet = this.equalityIndices.get(field)?.get(targetVal);
541
+ matchingEqualitySets.push({ field, set: idSet || new Set() });
542
+ }
543
+ }
544
+ }
545
+ if (matchingEqualitySets.length > 1) {
546
+ // Sort sets by size ascending for fastest intersection
547
+ matchingEqualitySets.sort((a, b) => a.set.size - b.set.size);
548
+ const primarySet = matchingEqualitySets[0].set;
549
+ const candidates = [];
550
+ for (const id of primarySet) {
551
+ let inAll = true;
552
+ for (let i = 1; i < matchingEqualitySets.length; i++) {
553
+ if (!matchingEqualitySets[i].set.has(id)) {
554
+ inAll = false;
555
+ break;
556
+ }
557
+ }
558
+ if (inAll) {
559
+ const r = this.records.get(id);
560
+ if (r)
561
+ candidates.push(r);
562
+ }
563
+ }
564
+ return {
565
+ candidates,
566
+ indexName: matchingEqualitySets.map((m) => m.field).join("+"),
567
+ indexType: "equality",
568
+ scanStrategy: "MULTI_INDEX_INTERSECTION",
569
+ };
570
+ }
571
+ if (matchingEqualitySets.length === 1) {
572
+ const match = matchingEqualitySets[0];
573
+ const candidates = [];
574
+ for (const id of match.set) {
575
+ const r = this.records.get(id);
576
+ if (r)
577
+ candidates.push(r);
578
+ }
579
+ return {
580
+ candidates,
581
+ indexName: match.field,
582
+ indexType: "equality",
583
+ scanStrategy: "INDEX_LOOKUP",
584
+ };
585
+ }
586
+ // 3. Check Sorted Indices for Range Queries ($gt, $gte, $lt, $lte, $between)
587
+ for (const field of whereKeys) {
588
+ if (this.sortedIndices.has(field)) {
589
+ const filter = options.where[field];
590
+ if (typeof filter === "object" && filter !== null) {
591
+ const f = filter;
592
+ if (f.$between || f.$gt !== undefined || f.$gte !== undefined || f.$lt !== undefined || f.$lte !== undefined) {
593
+ const sortedList = this.sortedIndices.get(field);
594
+ const candidates = [];
595
+ for (const entry of sortedList) {
596
+ const val = entry.value;
597
+ let match = true;
598
+ if (f.$between && (val < f.$between[0] || val > f.$between[1]))
599
+ match = false;
600
+ if (f.$gt !== undefined && val <= f.$gt)
601
+ match = false;
602
+ if (f.$gte !== undefined && val < f.$gte)
603
+ match = false;
604
+ if (f.$lt !== undefined && val >= f.$lt)
605
+ match = false;
606
+ if (f.$lte !== undefined && val > f.$lte)
607
+ match = false;
608
+ if (match) {
609
+ for (const id of entry.ids) {
610
+ const r = this.records.get(id);
611
+ if (r)
612
+ candidates.push(r);
613
+ }
614
+ }
615
+ }
616
+ return {
617
+ candidates,
618
+ indexName: field,
619
+ indexType: "sorted",
620
+ scanStrategy: "INDEX_RANGE_SCAN",
621
+ };
622
+ }
623
+ }
624
+ }
625
+ }
626
+ // 4. Check Prefix Indices ($startsWith)
627
+ for (const field of whereKeys) {
628
+ if (this.prefixIndices.has(field)) {
629
+ const filter = options.where[field];
630
+ if (typeof filter === "object" && filter !== null && filter.$startsWith) {
631
+ const prefix = filter.$startsWith.toLowerCase();
632
+ const prefixMap = this.prefixIndices.get(field);
633
+ const idSet = prefixMap.get(prefix);
634
+ const candidates = [];
635
+ if (idSet) {
636
+ for (const id of idSet) {
637
+ const r = this.records.get(id);
638
+ if (r)
639
+ candidates.push(r);
640
+ }
641
+ }
642
+ return {
643
+ candidates,
644
+ indexName: field,
645
+ indexType: "prefix",
646
+ scanStrategy: "PREFIX_SCAN",
647
+ };
648
+ }
649
+ }
650
+ }
651
+ return {
652
+ candidates: Array.from(this.records.values()),
653
+ scanStrategy: "FULL_TABLE_SCAN",
654
+ };
655
+ }
656
+ evaluateWhere(record, where) {
657
+ for (const [field, expected] of Object.entries(where)) {
658
+ const actualVal = this.resolveFieldValue(record, field);
659
+ if (expected === null || typeof expected !== "object") {
660
+ if (actualVal !== expected)
661
+ return false;
662
+ continue;
663
+ }
664
+ if (expected instanceof RegExp) {
665
+ if (typeof actualVal !== "string" || !expected.test(actualVal))
666
+ return false;
667
+ continue;
668
+ }
669
+ const filter = expected;
670
+ if (filter.$eq !== undefined && actualVal !== filter.$eq)
671
+ return false;
672
+ if (filter.$ne !== undefined && actualVal === filter.$ne)
673
+ return false;
674
+ if (filter.$exists !== undefined) {
675
+ const exists = actualVal !== undefined;
676
+ if (exists !== filter.$exists)
677
+ return false;
678
+ }
679
+ if (filter.$gt !== undefined) {
680
+ if (actualVal === undefined || actualVal === null || actualVal <= filter.$gt)
681
+ return false;
682
+ }
683
+ if (filter.$gte !== undefined) {
684
+ if (actualVal === undefined || actualVal === null || actualVal < filter.$gte)
685
+ return false;
686
+ }
687
+ if (filter.$lt !== undefined) {
688
+ if (actualVal === undefined || actualVal === null || actualVal >= filter.$lt)
689
+ return false;
690
+ }
691
+ if (filter.$lte !== undefined) {
692
+ if (actualVal === undefined || actualVal === null || actualVal > filter.$lte)
693
+ return false;
694
+ }
695
+ if (filter.$in !== undefined && (!Array.isArray(filter.$in) || !filter.$in.includes(actualVal))) {
696
+ return false;
697
+ }
698
+ if (filter.$nin !== undefined && Array.isArray(filter.$nin) && filter.$nin.includes(actualVal)) {
699
+ return false;
700
+ }
701
+ if (filter.$between !== undefined) {
702
+ const [min, max] = filter.$between;
703
+ if (actualVal === undefined || actualVal === null || actualVal < min || actualVal > max) {
704
+ return false;
705
+ }
706
+ }
707
+ if (filter.$startsWith !== undefined) {
708
+ if (typeof actualVal !== "string" || !actualVal.startsWith(filter.$startsWith))
709
+ return false;
710
+ }
711
+ if (filter.$endsWith !== undefined) {
712
+ if (typeof actualVal !== "string" || !actualVal.endsWith(filter.$endsWith))
713
+ return false;
714
+ }
715
+ if (filter.$contains !== undefined) {
716
+ if (typeof actualVal !== "string" || !actualVal.includes(filter.$contains))
717
+ return false;
718
+ }
719
+ if (filter.$regex !== undefined) {
720
+ const re = typeof filter.$regex === "string" ? new RegExp(filter.$regex, "i") : filter.$regex;
721
+ if (typeof actualVal !== "string" || !re.test(actualVal))
722
+ return false;
723
+ }
724
+ }
725
+ return true;
726
+ }
727
+ resolveFieldValue(record, field) {
728
+ return record[field];
729
+ }
730
+ normalizeSortableValue(val) {
731
+ if (typeof val === "number" || typeof val === "string")
732
+ return val;
733
+ if (val instanceof Date)
734
+ return val.getTime();
735
+ return undefined;
736
+ }
737
+ insertSortedIndexEntry(list, val, id) {
738
+ let low = 0;
739
+ let high = list.length;
740
+ while (low < high) {
741
+ const mid = (low + high) >>> 1;
742
+ if (list[mid].value < val)
743
+ low = mid + 1;
744
+ else
745
+ high = mid;
746
+ }
747
+ if (low < list.length && list[low].value === val) {
748
+ list[low].ids.add(id);
749
+ }
750
+ else {
751
+ list.splice(low, 0, { value: val, ids: new Set([id]) });
752
+ }
753
+ }
754
+ buildCompositeKey(fields, record) {
755
+ return fields.map((f) => String(record[f] ?? "")).join("::");
756
+ }
757
+ insertPrefixIndex(prefixMap, text, id) {
758
+ const normalized = text.toLowerCase();
759
+ for (let len = 1; len <= Math.min(20, normalized.length); len++) {
760
+ const prefix = normalized.slice(0, len);
761
+ let set = prefixMap.get(prefix);
762
+ if (!set) {
763
+ set = new Set();
764
+ prefixMap.set(prefix, set);
765
+ }
766
+ set.add(id);
767
+ }
768
+ }
769
+ addIndicesForRecord(id, record) {
770
+ for (const [field, indexMap] of this.equalityIndices.entries()) {
771
+ const val = this.resolveFieldValue(record, field);
772
+ if (val !== undefined) {
773
+ let idSet = indexMap.get(val);
774
+ if (!idSet) {
775
+ idSet = new Set();
776
+ indexMap.set(val, idSet);
777
+ }
778
+ idSet.add(id);
779
+ }
780
+ }
781
+ for (const [field, sortedList] of this.sortedIndices.entries()) {
782
+ const rawVal = this.resolveFieldValue(record, field);
783
+ const val = this.normalizeSortableValue(rawVal);
784
+ if (val !== undefined) {
785
+ this.insertSortedIndexEntry(sortedList, val, id);
786
+ }
787
+ }
788
+ for (const [compName, compIndex] of this.compositeIndices.entries()) {
789
+ const key = this.buildCompositeKey(compIndex.fields, record);
790
+ let idSet = compIndex.map.get(key);
791
+ if (!idSet) {
792
+ idSet = new Set();
793
+ compIndex.map.set(key, idSet);
794
+ }
795
+ idSet.add(id);
796
+ }
797
+ for (const [field, prefixMap] of this.prefixIndices.entries()) {
798
+ const val = this.resolveFieldValue(record, field);
799
+ if (typeof val === "string") {
800
+ this.insertPrefixIndex(prefixMap, val, id);
801
+ }
802
+ }
803
+ }
804
+ removeIndicesForRecord(id, record) {
805
+ for (const [field, indexMap] of this.equalityIndices.entries()) {
806
+ const val = this.resolveFieldValue(record, field);
807
+ if (val !== undefined) {
808
+ const idSet = indexMap.get(val);
809
+ if (idSet) {
810
+ idSet.delete(id);
811
+ if (idSet.size === 0)
812
+ indexMap.delete(val);
813
+ }
814
+ }
815
+ }
816
+ for (const [field, sortedList] of this.sortedIndices.entries()) {
817
+ const rawVal = this.resolveFieldValue(record, field);
818
+ const val = this.normalizeSortableValue(rawVal);
819
+ if (val !== undefined) {
820
+ for (let i = 0; i < sortedList.length; i++) {
821
+ if (sortedList[i].value === val) {
822
+ sortedList[i].ids.delete(id);
823
+ if (sortedList[i].ids.size === 0) {
824
+ sortedList.splice(i, 1);
825
+ }
826
+ break;
827
+ }
828
+ }
829
+ }
830
+ }
831
+ for (const [compName, compIndex] of this.compositeIndices.entries()) {
832
+ const key = this.buildCompositeKey(compIndex.fields, record);
833
+ const idSet = compIndex.map.get(key);
834
+ if (idSet) {
835
+ idSet.delete(id);
836
+ if (idSet.size === 0)
837
+ compIndex.map.delete(key);
838
+ }
839
+ }
840
+ for (const [field, prefixMap] of this.prefixIndices.entries()) {
841
+ const val = this.resolveFieldValue(record, field);
842
+ if (typeof val === "string") {
843
+ const normalized = val.toLowerCase();
844
+ for (let len = 1; len <= Math.min(20, normalized.length); len++) {
845
+ const prefix = normalized.slice(0, len);
846
+ const set = prefixMap.get(prefix);
847
+ if (set) {
848
+ set.delete(id);
849
+ if (set.size === 0)
850
+ prefixMap.delete(prefix);
851
+ }
852
+ }
853
+ }
854
+ }
855
+ }
856
+ emitChangeEvent(operation, recordId, before, after) {
857
+ if (this.subscriptions.size === 0)
858
+ return;
859
+ let diff;
860
+ if (before && after) {
861
+ diff = {};
862
+ const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
863
+ for (const k of allKeys) {
864
+ if (before[k] !== after[k]) {
865
+ diff[k] = { old: before[k], new: after[k] };
866
+ }
867
+ }
868
+ }
869
+ const event = {
870
+ operation,
871
+ table: this.name,
872
+ recordId,
873
+ before,
874
+ after,
875
+ diff,
876
+ timestamp: Date.now(),
877
+ };
878
+ for (const { callback, filter } of this.subscriptions.values()) {
879
+ try {
880
+ if (filter) {
881
+ const target = after ?? before;
882
+ if (target && !filter(target))
883
+ continue;
884
+ }
885
+ callback(event);
886
+ }
887
+ catch {
888
+ // Isolate subscriber exceptions
889
+ }
890
+ }
891
+ }
892
+ }
893
+ //# sourceMappingURL=broccolidb-table.js.map