UNPKG

@oimdb/core

Version:

Core in-memory data library with type-safe indices, reactive subscriptions, and event processing

1,616 lines (1,574 loc) 44 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { EOIMCollectionEventType: () => EOIMCollectionEventType, EOIMEventQueueEventType: () => EOIMEventQueueEventType, EOIMEventQueueSchedulerEventType: () => EOIMEventQueueSchedulerEventType, EOIMIndexEventType: () => EOIMIndexEventType, EOIMUpdateEventCoalescerEventType: () => EOIMUpdateEventCoalescerEventType, OIMCollection: () => OIMCollection, OIMCollectionStore: () => OIMCollectionStore, OIMCollectionStoreMapDriven: () => OIMCollectionStoreMapDriven, OIMComparatorFactory: () => OIMComparatorFactory, OIMDBSettings: () => OIMDBSettings, OIMEntityUpdaterFactory: () => OIMEntityUpdaterFactory, OIMEventEmitter: () => OIMEventEmitter, OIMEventQueue: () => OIMEventQueue, OIMEventQueueScheduler: () => OIMEventQueueScheduler, OIMEventQueueSchedulerAnimationFrame: () => OIMEventQueueSchedulerAnimationFrame, OIMEventQueueSchedulerFactory: () => OIMEventQueueSchedulerFactory, OIMEventQueueSchedulerImmediate: () => OIMEventQueueSchedulerImmediate, OIMEventQueueSchedulerMicrotask: () => OIMEventQueueSchedulerMicrotask, OIMEventQueueSchedulerTimeout: () => OIMEventQueueSchedulerTimeout, OIMIndexArrayBased: () => OIMIndexArrayBased, OIMIndexComparatorFactory: () => OIMIndexComparatorFactory, OIMIndexManualArrayBased: () => OIMIndexManualArrayBased, OIMIndexManualSetBased: () => OIMIndexManualSetBased, OIMIndexSetBased: () => OIMIndexSetBased, OIMIndexStoreArrayBased: () => OIMIndexStoreArrayBased, OIMIndexStoreMapDrivenArrayBased: () => OIMIndexStoreMapDrivenArrayBased, OIMIndexStoreMapDrivenSetBased: () => OIMIndexStoreMapDrivenSetBased, OIMIndexStoreSetBased: () => OIMIndexStoreSetBased, OIMMap2Keys: () => OIMMap2Keys, OIMPkSelectorFactory: () => OIMPkSelectorFactory, OIMRICollection: () => OIMRICollection, OIMReactiveCollection: () => OIMReactiveCollection, OIMReactiveIndexArrayBased: () => OIMReactiveIndexArrayBased, OIMReactiveIndexManualArrayBased: () => OIMReactiveIndexManualArrayBased, OIMReactiveIndexManualSetBased: () => OIMReactiveIndexManualSetBased, OIMReactiveIndexSetBased: () => OIMReactiveIndexSetBased, OIMUpdateEventCoalescer: () => OIMUpdateEventCoalescer, OIMUpdateEventCoalescerCollection: () => OIMUpdateEventCoalescerCollection, OIMUpdateEventCoalescerIndex: () => OIMUpdateEventCoalescerIndex, OIMUpdateEventEmitter: () => OIMUpdateEventEmitter }); module.exports = __toCommonJS(index_exports); // src/abstract/OIMCollectionStore.ts var OIMCollectionStore = class { }; // src/abstract/OIMIndexStoreSetBased.ts var OIMIndexStoreSetBased = class { }; // src/abstract/OIMIndexStoreArrayBased.ts var OIMIndexStoreArrayBased = class { }; // src/core/OIMEventEmitter.ts var OIMEventEmitter = class { constructor() { this.buckets = {}; } getOrCreateBucket(event) { let bucket = this.buckets[event]; if (!bucket) { bucket = { handlers: [], indexByHandler: /* @__PURE__ */ new Map(), tombstoneCount: 0, isEmitting: 0 }; this.buckets[event] = bucket; } return bucket; } compactBucket(bucket) { const handlers = bucket.handlers; const length = handlers.length; const newHandlers = []; bucket.indexByHandler.clear(); if (bucket.tombstoneCount < length >> 2) { newHandlers.length = length - bucket.tombstoneCount; } let writeIndex = 0; for (let i = 0; i < length; i++) { const handler = handlers[i]; if (handler) { newHandlers[writeIndex] = handler; bucket.indexByHandler.set(handler, writeIndex); writeIndex++; } } if (newHandlers.length !== writeIndex) { newHandlers.length = writeIndex; } bucket.handlers = newHandlers; bucket.tombstoneCount = 0; } on(event, handler) { const bucket = this.getOrCreateBucket(event); if (bucket.indexByHandler.has(handler)) return; const index = bucket.handlers.length; bucket.handlers.push(handler); bucket.indexByHandler.set( handler, index ); } emit(event, payload) { const bucket = this.buckets[event]; if (!bucket) return; const handlers = bucket.handlers; const length = handlers.length; if (length === 0) return; bucket.isEmitting++; for (let i = 0; i < length; i++) { const handler = handlers[i]; if (handler) handler(payload); } bucket.isEmitting--; if (bucket.isEmitting === 0 && bucket.tombstoneCount > 0) { if (bucket.tombstoneCount >= length >> 1) { this.compactBucket(bucket); } else if (length === bucket.tombstoneCount) { delete this.buckets[event]; } } } off(event, handler) { const bucket = this.buckets[event]; if (!bucket) return; const index = bucket.indexByHandler.get(handler); if (index === void 0) return; bucket.handlers[index] = null; bucket.tombstoneCount++; bucket.indexByHandler.delete(handler); if (bucket.isEmitting === 0) { const length = bucket.handlers.length; if (bucket.tombstoneCount >= length >> 1) { this.compactBucket(bucket); } else if (length === bucket.tombstoneCount) { delete this.buckets[event]; } } } offAll(event) { if (event !== void 0) { delete this.buckets[event]; } else { this.buckets = {}; } } }; // src/enum/EOIMEventQueueSchedulerEventType.ts var EOIMEventQueueSchedulerEventType = /* @__PURE__ */ ((EOIMEventQueueSchedulerEventType2) => { EOIMEventQueueSchedulerEventType2[EOIMEventQueueSchedulerEventType2["FLUSH"] = 0] = "FLUSH"; return EOIMEventQueueSchedulerEventType2; })(EOIMEventQueueSchedulerEventType || {}); // src/abstract/OIMEventQueueScheduler.ts var OIMEventQueueScheduler = class { constructor() { this.emitter = new OIMEventEmitter(); } on(event, handler) { this.emitter.on(event, handler); } off(event, handler) { this.emitter.off(event, handler); } /** * Trigger a flush event. Should be called by subclasses when they execute the scheduled flush. */ flush() { this.emitter.emit(0 /* FLUSH */, void 0); } }; // src/enum/EOIMIndexEventType.ts var EOIMIndexEventType = /* @__PURE__ */ ((EOIMIndexEventType2) => { EOIMIndexEventType2[EOIMIndexEventType2["UPDATE"] = 0] = "UPDATE"; return EOIMIndexEventType2; })(EOIMIndexEventType || {}); // src/core/OIMIndexStoreMapDrivenSetBased.ts var OIMIndexStoreMapDrivenSetBased = class extends OIMIndexStoreSetBased { constructor() { super(...arguments); this.pks = /* @__PURE__ */ new Map(); } setOneByKey(key, pks) { this.pks.set(key, pks); } removeOneByKey(key) { this.pks.delete(key); } removeManyByKeys(keys) { for (const key of keys) { this.pks.delete(key); } } getOneByKey(key) { return this.pks.get(key); } getManyByKeys(keys) { const result = /* @__PURE__ */ new Map(); for (const key of keys) { const pks = this.getOneByKey(key); if (pks !== void 0) { result.set(key, pks); } } return result; } getAllKeys() { return Array.from(this.pks.keys()); } getAll() { return new Map(this.pks); } countAll() { return this.pks.size; } clear() { this.pks.clear(); } }; // src/abstract/OIMIndexSetBased.ts var OIMIndexSetBased = class { constructor(options = {}) { this.emitter = new OIMEventEmitter(); this.comparePks = options.comparePks; this.store = options.store ?? new OIMIndexStoreMapDrivenSetBased(); } /** * Get primary keys for multiple index keys */ getPksByKeys(keys) { return this.store.getManyByKeys(keys); } /** * @deprecated Use getPksByKey instead * Get primary keys for a specific index key */ getPks(key) { return this.getPksByKey(key); } /** * Get primary keys for a specific index key */ getPksByKey(key) { const pksSet = this.store.getOneByKey(key); return pksSet ? pksSet : /* @__PURE__ */ new Set(); } /** * Check if an index key exists */ hasKey(key) { return this.store.getOneByKey(key) !== void 0; } /** * Get all index keys */ getKeys() { return this.store.getAllKeys(); } /** * Get the number of primary keys for a specific index key */ getKeySize(key) { const pksSet = this.store.getOneByKey(key); return pksSet ? pksSet.size : 0; } /** * Get total number of index keys */ get size() { return this.store.countAll(); } /** * Check if the index is empty */ get isEmpty() { return this.store.countAll() === 0; } /** * Get performance metrics for monitoring and debugging */ getMetrics() { let totalPks = 0; let maxBucketSize = 0; let minBucketSize = Infinity; const allPks = this.store.getAll(); for (const pksSet of allPks.values()) { totalPks += pksSet.size; maxBucketSize = Math.max(maxBucketSize, pksSet.size); minBucketSize = Math.min(minBucketSize, pksSet.size); } return { totalKeys: allPks.size, totalPks, averagePksPerKey: allPks.size > 0 ? totalPks / allPks.size : 0, maxBucketSize: maxBucketSize === -Infinity ? 0 : maxBucketSize, minBucketSize: minBucketSize === Infinity ? 0 : minBucketSize }; } /** * Clean up event listeners when index is no longer needed */ destroy() { this.emitter.offAll(); this.store.clear(); } /** * Set primary keys for a specific index key with optional comparison. * If comparator is provided and returns true (no changes), skip the update. */ setPksWithComparison(key, newPks) { if (this.comparePks) { const existingPksSet = this.store.getOneByKey(key); if (existingPksSet && existingPksSet.size === newPks.size) { const existingPksArray = existingPksSet.size > 0 ? [...existingPksSet] : []; const newPksArray = newPks.size > 0 ? [...newPks] : []; if (this.comparePks(existingPksArray, newPksArray)) { return false; } } else if (!existingPksSet && newPks.size === 0) { return false; } } this.store.setOneByKey(key, newPks); return true; } /** * Emit update event with changed keys */ emitUpdate(keys) { this.emitter.emit(0 /* UPDATE */, { keys }); } }; // src/core/OIMIndexStoreMapDrivenArrayBased.ts var OIMIndexStoreMapDrivenArrayBased = class extends OIMIndexStoreArrayBased { constructor() { super(...arguments); this.pks = /* @__PURE__ */ new Map(); } setOneByKey(key, pks) { this.pks.set(key, pks); } removeOneByKey(key) { this.pks.delete(key); } removeManyByKeys(keys) { for (const key of keys) { this.pks.delete(key); } } getOneByKey(key) { return this.pks.get(key); } getManyByKeys(keys) { const result = /* @__PURE__ */ new Map(); for (const key of keys) { const pks = this.getOneByKey(key); if (pks !== void 0) { result.set(key, pks); } } return result; } getAllKeys() { return Array.from(this.pks.keys()); } getAll() { return new Map(this.pks); } countAll() { return this.pks.size; } clear() { this.pks.clear(); } }; // src/abstract/OIMIndexArrayBased.ts var OIMIndexArrayBased = class { constructor(options = {}) { this.emitter = new OIMEventEmitter(); this.comparePks = options.comparePks; this.store = options.store ?? new OIMIndexStoreMapDrivenArrayBased(); } /** * Get primary keys for multiple index keys */ getPksByKeys(keys) { return this.store.getManyByKeys(keys); } /** * Get primary keys for a specific index key */ getPksByKey(key) { const pksArray = this.store.getOneByKey(key); return pksArray ? pksArray : []; } /** * Check if an index key exists */ hasKey(key) { return this.store.getOneByKey(key) !== void 0; } /** * Get all index keys */ getKeys() { return this.store.getAllKeys(); } /** * Get the number of primary keys for a specific index key */ getKeySize(key) { const pksArray = this.store.getOneByKey(key); return pksArray ? pksArray.length : 0; } /** * Get total number of index keys */ get size() { return this.store.countAll(); } /** * Check if the index is empty */ get isEmpty() { return this.store.countAll() === 0; } /** * Get performance metrics for monitoring and debugging */ getMetrics() { let totalPks = 0; let maxBucketSize = 0; let minBucketSize = Infinity; const allPks = this.store.getAll(); for (const pksArray of allPks.values()) { totalPks += pksArray.length; maxBucketSize = Math.max(maxBucketSize, pksArray.length); minBucketSize = Math.min(minBucketSize, pksArray.length); } return { totalKeys: allPks.size, totalPks, averagePksPerKey: allPks.size > 0 ? totalPks / allPks.size : 0, maxBucketSize: maxBucketSize === -Infinity ? 0 : maxBucketSize, minBucketSize: minBucketSize === Infinity ? 0 : minBucketSize }; } /** * Clean up event listeners when index is no longer needed */ destroy() { this.emitter.offAll(); this.store.clear(); } /** * Set primary keys for a specific index key with optional comparison. * If comparator is provided and returns true (no changes), skip the update. */ setPksWithComparison(key, newPks) { if (this.comparePks) { const existingPksArray = this.store.getOneByKey(key); if (existingPksArray && existingPksArray.length === newPks.length) { if (this.comparePks(existingPksArray, newPks)) { return false; } } else if (!existingPksArray && newPks.length === 0) { return false; } } this.store.setOneByKey(key, newPks); return true; } /** * Emit update event with changed keys */ emitUpdate(keys) { this.emitter.emit(0 /* UPDATE */, { keys }); } }; // src/enum/EOIMUpdateEventCoalescerEventType.ts var EOIMUpdateEventCoalescerEventType = /* @__PURE__ */ ((EOIMUpdateEventCoalescerEventType2) => { EOIMUpdateEventCoalescerEventType2[EOIMUpdateEventCoalescerEventType2["HAS_CHANGES"] = 0] = "HAS_CHANGES"; EOIMUpdateEventCoalescerEventType2[EOIMUpdateEventCoalescerEventType2["BEFORE_FLUSH"] = 1] = "BEFORE_FLUSH"; EOIMUpdateEventCoalescerEventType2[EOIMUpdateEventCoalescerEventType2["AFTER_FLUSH"] = 2] = "AFTER_FLUSH"; return EOIMUpdateEventCoalescerEventType2; })(EOIMUpdateEventCoalescerEventType || {}); // src/core/OIMUpdateEventEmitter.ts var OIMUpdateEventEmitter = class { constructor(opts) { this.keyHandlers = /* @__PURE__ */ new Map(); this.handleHasChanges = () => { this.queue.enqueue(() => { const updatedKeys = this.coalescer.getUpdatedKeys(); if (this.keyHandlers.size === 0) { this.coalescer.clearUpdatedKeys(); return; } if (updatedKeys.size * 2 < this.keyHandlers.size) { for (const key of updatedKeys.values()) { const handlers = this.keyHandlers.get(key); if (handlers) { for (const handler of handlers) { handler(); } } } } else { for (const [key, handlers] of this.keyHandlers) { if (updatedKeys.has(key)) { for (const handler of handlers) { handler(); } } } } this.coalescer.clearUpdatedKeys(); }); }; this.coalescer = opts.coalescer; this.coalescer.emitter.on( 0 /* HAS_CHANGES */, this.handleHasChanges ); this.queue = opts.queue; } destroy() { this.coalescer.emitter.off( 0 /* HAS_CHANGES */, this.handleHasChanges ); this.keyHandlers.clear(); } /** * Get performance metrics for monitoring and debugging */ getMetrics() { let totalHandlers = 0; for (const handlers of this.keyHandlers.values()) { totalHandlers += handlers.size; } return { totalKeys: this.keyHandlers.size, totalHandlers, averageHandlersPerKey: this.keyHandlers.size > 0 ? totalHandlers / this.keyHandlers.size : 0, queueLength: this.queue.length }; } /** * Check if there are any active subscriptions */ hasSubscriptions() { return this.keyHandlers.size > 0; } /** * Get the number of handlers for a specific key */ getHandlerCount(key) { return this.keyHandlers.get(key)?.size ?? 0; } subscribeOnKey(key, handler) { let handlers = this.keyHandlers.get(key); if (!handlers) { handlers = /* @__PURE__ */ new Set(); this.keyHandlers.set(key, handlers); } handlers.add(handler); return () => { this.unsubscribeFromKey(key, handler); }; } unsubscribeFromKey(key, handler) { const handlers = this.keyHandlers.get(key); if (!handlers) return; handlers.delete(handler); if (handlers.size === 0) { this.keyHandlers.delete(key); } } subscribeOnKeys(keys, handler) { const subscribedKeys = []; for (const key of keys) { let handlers = this.keyHandlers.get(key); if (!handlers) { handlers = /* @__PURE__ */ new Set(); this.keyHandlers.set(key, handlers); } if (!handlers.has(handler)) { handlers.add(handler); subscribedKeys.push(key); } } return () => { for (const key of subscribedKeys) { this.unsubscribeFromKey(key, handler); } }; } unsubscribeFromKeys(keys, handler) { for (const key of keys) { this.unsubscribeFromKey(key, handler); } } }; // src/core/OIMIndexManualSetBased.ts var OIMIndexManualSetBased = class extends OIMIndexSetBased { constructor(options = {}) { super(options); } /** * Set primary keys for a specific index key, replacing any existing values. * Uses optional comparator to skip updates if PKs haven't actually changed. */ setPks(key, pks) { const hasChanges = this.setPksWithComparison(key, new Set(pks)); if (hasChanges) { this.emitUpdate([key]); } } /** * Add primary keys to a specific index key */ addPks(key, pks) { if (pks.length === 0) return; let pksSet = this.store.getOneByKey(key); if (!pksSet) { pksSet = /* @__PURE__ */ new Set(); this.store.setOneByKey(key, pksSet); } let hasChanges = false; for (const pk of pks) { if (!pksSet.has(pk)) { pksSet.add(pk); hasChanges = true; } } if (hasChanges) { this.store.setOneByKey(key, pksSet); this.emitUpdate([key]); } } /** * Remove primary keys from a specific index key */ removePks(key, pks) { if (pks.length === 0) return; const pksSet = this.store.getOneByKey(key); if (!pksSet) return; let hasChanges = false; for (const pk of pks) { if (pksSet.delete(pk)) { hasChanges = true; } } if (pksSet.size === 0) { this.store.removeOneByKey(key); hasChanges = true; } else if (hasChanges) { this.store.setOneByKey(key, pksSet); } if (hasChanges) { this.emitUpdate([key]); } } /** * Clear all primary keys for a specific index key, or all keys if no key specified */ clear(key) { if (key === void 0) { const allKeys = this.store.getAllKeys(); if (allKeys.length > 0) { this.store.clear(); this.emitUpdate(allKeys); } } else { if (this.store.getOneByKey(key) !== void 0) { this.store.removeOneByKey(key); this.emitUpdate([key]); } } } }; // src/const/OIMDBSettings.ts var OIMDBSettings = class { }; /** * Fixed key used in the manual index to store updated keys set in coalescers. * This constant is exported so users can create their own indexes and pass them to coalescers. */ OIMDBSettings.UPDATED_KEYS_INDEX_KEY = "__updatedKeys__"; // src/core/OIMUpdateEventCoalescer.ts var OIMUpdateEventCoalescer = class { constructor(index) { this.emitter = new OIMEventEmitter(); this.hasEmittedChanges = false; this.updatedKeysIndex = index ?? new OIMIndexManualSetBased({ store: new OIMIndexStoreMapDrivenSetBased() }); if (!this.updatedKeysIndex.hasKey(OIMDBSettings.UPDATED_KEYS_INDEX_KEY)) { this.updatedKeysIndex.setPks( OIMDBSettings.UPDATED_KEYS_INDEX_KEY, [] ); } } /** * Get the set of keys that have been updated since last clear */ getUpdatedKeys() { return this.updatedKeysIndex.getPksByKey( OIMDBSettings.UPDATED_KEYS_INDEX_KEY ); } /** * Clear all updated keys and reset the changed state */ clearUpdatedKeys() { this.emitter.emit( 1 /* BEFORE_FLUSH */, void 0 ); this.updatedKeysIndex.setPks(OIMDBSettings.UPDATED_KEYS_INDEX_KEY, []); this.hasEmittedChanges = false; this.emitter.emit( 2 /* AFTER_FLUSH */, void 0 ); } /** * Add keys to the updated set and emit HAS_CHANGES event if needed */ addUpdatedKeys(keys) { this.updatedKeysIndex.addPks( OIMDBSettings.UPDATED_KEYS_INDEX_KEY, keys ); if (!this.hasEmittedChanges) { this.hasEmittedChanges = true; this.emitter.emit( 0 /* HAS_CHANGES */, void 0 ); } } }; // src/core/OIMUpdateEventCoalescerIndex.ts var OIMUpdateEventCoalescerIndex = class extends OIMUpdateEventCoalescer { constructor(indexEmitter) { super(); this.indexEmitter = indexEmitter; /** * When the index is updated, we keep track of the updated keys. */ this.handleUpdate = (payload) => { this.addUpdatedKeys(payload.keys); }; this.indexEmitter.on(0 /* UPDATE */, this.handleUpdate); } destroy() { this.indexEmitter.off(0 /* UPDATE */, this.handleUpdate); this.emitter.offAll(); this.clearUpdatedKeys(); } }; // src/abstract/OIMReactiveIndexSetBased.ts var OIMReactiveIndexSetBased = class { constructor(queue, opts) { this.index = opts?.index ?? this.createDefaultIndex(); this.coalescer = new OIMUpdateEventCoalescerIndex( this.index.emitter ); this.updateEventEmitter = new OIMUpdateEventEmitter({ coalescer: this.coalescer, queue }); } getPksByKey(key) { return this.index.getPksByKey(key); } getPksByKeys(keys) { return this.index.getPksByKeys(keys); } hasKey(key) { return this.index.hasKey(key); } getKeys() { return this.index.getKeys(); } getKeySize(key) { return this.index.getKeySize(key); } get size() { return this.index.size; } get isEmpty() { return this.index.isEmpty; } getMetrics() { return this.index.getMetrics(); } destroy() { this.index.destroy(); } }; // src/abstract/OIMReactiveIndexArrayBased.ts var OIMReactiveIndexArrayBased = class { constructor(queue, opts) { this.index = opts?.index ?? this.createDefaultIndex(); this.coalescer = new OIMUpdateEventCoalescerIndex( this.index.emitter ); this.updateEventEmitter = new OIMUpdateEventEmitter({ coalescer: this.coalescer, queue }); } getPksByKey(key) { return this.index.getPksByKey(key); } getPksByKeys(keys) { return this.index.getPksByKeys(keys); } hasKey(key) { return this.index.hasKey(key); } getKeys() { return this.index.getKeys(); } getKeySize(key) { return this.index.getKeySize(key); } get size() { return this.index.size; } get isEmpty() { return this.index.isEmpty; } getMetrics() { return this.index.getMetrics(); } destroy() { this.index.destroy(); } }; // src/core/OIMPkSelectorFactory.ts var OIMPkSelectorFactory = class { createIdSelector() { return (entity) => entity.id; } }; // src/core/OIMCollectionStoreMapDriven.ts var OIMCollectionStoreMapDriven = class extends OIMCollectionStore { constructor() { super(...arguments); this.entities = /* @__PURE__ */ new Map(); } setOneByPk(pk, entity) { this.entities.set(pk, entity); } setManyByPks(pks, entities) { for (let i = 0; i < pks.length; i++) { this.entities.set(pks[i], entities[i]); } } removeOneByPk(pk) { this.entities.delete(pk); } removeManyByPks(pks) { for (const pk of pks) { this.entities.delete(pk); } } getOneByPk(pk) { return this.entities.get(pk); } getManyByPks(pks) { const result = []; result.length = pks.length; let writeIndex = 0; for (let i = 0; i < pks.length; i++) { const entity = this.getOneByPk(pks[i]); if (entity !== void 0) { result[writeIndex++] = entity; } } result.length = writeIndex; return result; } getAll() { return Array.from(this.entities.values()); } countAll() { return this.entities.size; } clear() { this.entities.clear(); } getAllPks() { return Array.from(this.entities.keys()); } }; // src/core/OIMEntityUpdaterFactory.ts var OIMEntityUpdaterFactory = class { createMergeEntityUpdater() { return (draft, prev) => { return { ...prev, ...draft }; }; } }; // src/enum/EOIMCollectionEventType.ts var EOIMCollectionEventType = /* @__PURE__ */ ((EOIMCollectionEventType2) => { EOIMCollectionEventType2[EOIMCollectionEventType2["UPDATE"] = 0] = "UPDATE"; return EOIMCollectionEventType2; })(EOIMCollectionEventType || {}); // src/core/OIMCollection.ts var OIMCollection = class { constructor(opts) { this.emitter = new OIMEventEmitter(); this.selectPk = opts?.selectPk ?? new OIMPkSelectorFactory().createIdSelector(); this.store = opts?.store ?? new OIMCollectionStoreMapDriven(); this.updateEntity = opts?.updateEntity ?? new OIMEntityUpdaterFactory().createMergeEntityUpdater(); } getOneByPk(pk) { return this.store.getOneByPk(pk); } getManyByPks(pks) { return this.store.getManyByPks(pks); } upsertOneByPk(pk, entity) { this.upsertOneWithoutNotificationsByPk(pk, entity); this.emitter.emit(0 /* UPDATE */, { pks: [pk] }); } upsertOne(entity) { const pk = this.upsertOneWithoutNotifications(entity); this.emitter.emit(0 /* UPDATE */, { pks: [pk] }); } upsertMany(entities) { const pks = entities.map( (entity) => this.upsertOneWithoutNotifications(entity) ); this.emitter.emit(0 /* UPDATE */, { pks }); } removeOne(entity) { const pk = this.selectPk(entity); this.store.removeOneByPk(pk); this.emitter.emit(0 /* UPDATE */, { pks: [pk] }); } removeMany(entities) { const pks = entities.map(this.selectPk); this.store.removeManyByPks(pks); this.emitter.emit(0 /* UPDATE */, { pks }); } removeOneByPk(pk) { this.store.removeOneByPk(pk); this.emitter.emit(0 /* UPDATE */, { pks: [pk] }); } removeManyByPks(pks) { this.store.removeManyByPks(pks); this.emitter.emit(0 /* UPDATE */, { pks }); } clear() { this.store.clear(); this.emitter.emit(0 /* UPDATE */, { pks: [] }); } countAll() { return this.store.countAll(); } getAll() { return this.store.getAll(); } getAllPks() { return this.store.getAllPks(); } upsertOneWithoutNotifications(entity) { const pk = this.selectPk(entity); this.upsertOneWithoutNotificationsByPk(pk, entity); return pk; } upsertOneWithoutNotificationsByPk(pk, entity) { if (!pk) { throw new Error( `[OIMCollection]: PK is required to upsert an entity ${JSON.stringify(entity)}` ); } const existingEntity = this.store.getOneByPk(pk); if (existingEntity) { this.store.setOneByPk( pk, this.updateEntity(entity, existingEntity) ); } else { this.store.setOneByPk(pk, entity); } } }; // src/core/OIMUpdateEventCoalescerCollection.ts var OIMUpdateEventCoalescerCollection = class extends OIMUpdateEventCoalescer { constructor(collectionEmitter) { super(); this.collectionEmitter = collectionEmitter; /** * When the collection is updated, we keep track of the updated pks. */ this.handleUpdate = (payload) => { this.addUpdatedKeys(payload.pks); }; this.collectionEmitter.on( 0 /* UPDATE */, this.handleUpdate ); } destroy() { this.collectionEmitter.off( 0 /* UPDATE */, this.handleUpdate ); this.emitter.offAll(); this.clearUpdatedKeys(); } }; // src/core/OIMReactiveCollection.ts var OIMReactiveCollection = class extends OIMCollection { constructor(queue, opts) { super(opts); this.coalescer = new OIMUpdateEventCoalescerCollection( this.emitter ); this.updateEventEmitter = new OIMUpdateEventEmitter({ coalescer: this.coalescer, queue }); } }; // src/core/OIMReactiveIndexManualSetBased.ts var OIMReactiveIndexManualSetBased = class extends OIMReactiveIndexSetBased { constructor(queue, opts) { super(queue, opts); } createDefaultIndex() { return new OIMIndexManualSetBased(); } setPks(key, pks) { this.index.setPks(key, pks); } addPks(key, pks) { this.index.addPks(key, pks); } removePks(key, pks) { this.index.removePks(key, pks); } clear(key) { this.index.clear(key); } }; // src/core/OIMIndexManualArrayBased.ts var OIMIndexManualArrayBased = class extends OIMIndexArrayBased { constructor(options = {}) { super(options); } /** * Set primary keys for a specific index key, replacing any existing values. * Uses optional comparator to skip updates if PKs haven't actually changed. */ setPks(key, pks) { const hasChanges = this.setPksWithComparison(key, pks); if (hasChanges) { this.emitUpdate([key]); } } /** * Add primary keys to a specific index key */ addPks(key, pks) { if (pks.length === 0) return; let pksArray = this.store.getOneByKey(key); if (!pksArray) { pksArray = []; this.store.setOneByKey(key, pksArray); } const pksSet = new Set(pksArray); let hasChanges = false; for (const pk of pks) { if (!pksSet.has(pk)) { pksSet.add(pk); hasChanges = true; } } if (hasChanges) { this.store.setOneByKey(key, Array.from(pksSet)); this.emitUpdate([key]); } } /** * Remove primary keys from a specific index key */ removePks(key, pks) { if (pks.length === 0) return; const pksArray = this.store.getOneByKey(key); if (!pksArray) return; const pksSet = new Set(pks); const filtered = pksArray.filter((pk) => !pksSet.has(pk)); const hasChanges = filtered.length !== pksArray.length; if (hasChanges) { if (filtered.length === 0) { this.store.removeOneByKey(key); } else { this.store.setOneByKey(key, filtered); } this.emitUpdate([key]); } } /** * Clear all primary keys for a specific index key, or all keys if no key specified */ clear(key) { if (key === void 0) { const allKeys = this.store.getAllKeys(); if (allKeys.length > 0) { this.store.clear(); this.emitUpdate(allKeys); } } else { if (this.store.getOneByKey(key) !== void 0) { this.store.removeOneByKey(key); this.emitUpdate([key]); } } } }; // src/core/OIMReactiveIndexManualArrayBased.ts var OIMReactiveIndexManualArrayBased = class extends OIMReactiveIndexArrayBased { constructor(queue, opts) { super(queue, opts); } createDefaultIndex() { return new OIMIndexManualArrayBased(); } setPks(key, pks) { this.index.setPks(key, pks); } addPks(key, pks) { this.index.addPks(key, pks); } removePks(key, pks) { this.index.removePks(key, pks); } clear(key) { this.index.clear(key); } }; // src/core/OIMRICollection.ts var OIMRICollection = class extends OIMReactiveCollection { constructor(queue, opts) { super(queue, opts.collectionOpts); this.indexes = opts.indexes || {}; } }; // src/core/OIMComparatorFactory.ts var OIMComparatorFactory = class { createShallowComparator() { return (a, b) => { if (a === b) return false; const ka = Object.keys(a); const kb = Object.keys(b); if (ka.length !== kb.length) return true; for (let i = 0; i < ka.length; i++) { const k = ka[i]; if (b[k] !== a[k]) return true; } return false; }; } }; // src/enum/EOIMEventQueueEventType.ts var EOIMEventQueueEventType = /* @__PURE__ */ ((EOIMEventQueueEventType2) => { EOIMEventQueueEventType2[EOIMEventQueueEventType2["AFTER_FLUSH"] = 0] = "AFTER_FLUSH"; EOIMEventQueueEventType2[EOIMEventQueueEventType2["BEFORE_FLUSH"] = 1] = "BEFORE_FLUSH"; return EOIMEventQueueEventType2; })(EOIMEventQueueEventType || {}); // src/core/OIMEventQueue.ts var OIMEventQueue = class { constructor(options = {}) { this.emitter = new OIMEventEmitter(); this.queue = []; this.scheduler = options.scheduler; if (this.scheduler) { this.flushBound = () => this.flush(); this.scheduler.on( 0 /* FLUSH */, this.flushBound ); } } /** * Add a function to the queue. If scheduler is configured and autoSchedule is enabled, * automatically schedules a flush operation. */ enqueue(fn) { this.queue.push(fn); if (this.scheduler && this.queue.length === 1) { this.scheduler.schedule(); } } /** * Execute all queued functions and clear the queue. * This method is safe to call multiple times and handles reentrancy. */ flush() { if (this.queue.length === 0) return; this.emitter.emit(1 /* BEFORE_FLUSH */, void 0); const currentQueue = this.queue.slice(); this.queue.length = 0; for (let i = 0; i < currentQueue.length; i++) { currentQueue[i](); } this.emitter.emit(0 /* AFTER_FLUSH */, void 0); } /** * Get the current number of queued functions. */ get length() { return this.queue.length; } /** * Check if the queue is empty. */ get isEmpty() { return this.queue.length === 0; } /** * Clear the queue without executing functions and cancel any scheduled flush. */ clear() { this.queue.length = 0; this.scheduler?.cancel(); } /** * Clean up scheduler subscription when queue is no longer needed. */ destroy() { if (this.scheduler && this.flushBound) { this.scheduler.off( 0 /* FLUSH */, this.flushBound ); } this.clear(); } }; // src/core/OIMIndexComparatorFactory.ts var OIMIndexComparatorFactory = class { /** * Create an element-wise comparator that checks arrays for strict equality. * Compares array lengths and each element using strict equality (===). */ static createElementWiseComparator() { return (existingPks, newPks) => { if (existingPks.length !== newPks.length) { return false; } for (let i = 0; i < existingPks.length; i++) { if (existingPks[i] !== newPks[i]) { return false; } } return true; }; } /** * Create a set-based comparator that checks if arrays contain the same elements. * Order doesn't matter, only the presence of elements. */ static createSetBasedComparator() { return (existingPks, newPks) => { if (existingPks.length !== newPks.length) { return false; } const existingSet = new Set(existingPks); const newSet = new Set(newPks); if (existingSet.size !== newSet.size || existingSet.size !== existingPks.length) { return false; } for (const pk of existingSet) { if (!newSet.has(pk)) { return false; } } return true; }; } /** * Create a shallow comparator that only checks array references. * Fastest but only works if you're reusing the same array instances. */ static createShallowComparator() { return (existingPks, newPks) => { return existingPks === newPks; }; } /** * Create a no-comparison comparator that always returns false (always updates). * Useful for disabling comparison while keeping the same interface. */ static createAlwaysUpdateComparator() { return () => false; } }; // src/core/OIMMap2Keys.ts var OIMMap2Keys = class { constructor() { this.map = /* @__PURE__ */ new Map(); this._size = 0; } get size() { return this._size; } set(k1, k2, v) { let m = this.map.get(k1); if (!m) { m = /* @__PURE__ */ new Map(); this.map.set(k1, m); } if (!m.has(k2)) this._size++; m.set(k2, v); } get(k1, k2) { return this.map.get(k1)?.get(k2); } has(k1, k2) { return this.map.get(k1)?.has(k2); } delete(k1, k2) { const m = this.map.get(k1); if (!m) return false; const had = m.delete(k2); if (had) { this._size--; if (m.size === 0) this.map.delete(k1); } return had; } clear() { this.map.clear(); this._size = 0; } }; // src/core/event-queue-scheduler/OIMEventQueueSchedulerAnimationFrame.ts var OIMEventQueueSchedulerAnimationFrame = class extends OIMEventQueueScheduler { constructor() { super(); this.useRequestAnimationFrame = typeof requestAnimationFrame !== "undefined"; } schedule() { if (this.frameId !== void 0) return; if (this.useRequestAnimationFrame) { this.frameId = requestAnimationFrame(() => { this.frameId = void 0; this.flush(); }); } else { this.frameId = setTimeout(() => { this.frameId = void 0; this.flush(); }, 16); } } cancel() { if (this.frameId === void 0) return; if (this.useRequestAnimationFrame) { cancelAnimationFrame(this.frameId); } else { clearTimeout(this.frameId); } this.frameId = void 0; } }; // src/core/event-queue-scheduler/OIMEventQueueSchedulerMicrotask.ts var OIMEventQueueSchedulerMicrotask = class extends OIMEventQueueScheduler { constructor() { super(...arguments); this.scheduled = false; } schedule() { if (this.scheduled) return; this.scheduled = true; Promise.resolve().then(() => { if (!this.scheduled) return; this.scheduled = false; this.flush(); }); } cancel() { if (!this.scheduled) return; this.scheduled = false; } }; // src/core/event-queue-scheduler/OIMEventQueueSchedulerTimeout.ts var OIMEventQueueSchedulerTimeout = class extends OIMEventQueueScheduler { /** * @param delay - Delay in milliseconds before executing the flush (default: 0) */ constructor(delay = 0) { super(); this.delay = Math.max(0, delay); } schedule() { if (this.timeoutId !== void 0) return; this.timeoutId = setTimeout(() => { this.timeoutId = void 0; this.flush(); }, this.delay); } cancel() { if (this.timeoutId === void 0) return; clearTimeout(this.timeoutId); this.timeoutId = void 0; } }; // src/core/event-queue-scheduler/OIMEventQueueSchedulerImmediate.ts var OIMEventQueueSchedulerImmediate = class extends OIMEventQueueScheduler { constructor() { super(); this.useSetImmediate = typeof setImmediate !== "undefined"; this.useMessageChannel = !this.useSetImmediate && typeof MessageChannel !== "undefined" && typeof window !== "undefined"; if (this.useMessageChannel) { this.messageChannel = new MessageChannel(); this.messageChannel.port2.onmessage = () => { if (this.pendingCallback && this.immediateId !== void 0) { const callback = this.pendingCallback; this.pendingCallback = void 0; this.immediateId = void 0; callback(); } }; } } schedule() { if (this.immediateId !== void 0) return; const callback = () => { this.immediateId = void 0; this.flush(); }; if (this.useSetImmediate) { this.immediateId = setImmediate(callback); } else if (this.useMessageChannel && this.messageChannel) { this.immediateId = 1; this.pendingCallback = callback; this.messageChannel.port1.postMessage(null); } else { this.immediateId = setTimeout(callback, 0); } } cancel() { if (this.immediateId === void 0) return; if (this.useSetImmediate) { clearImmediate(this.immediateId); } else if (this.useMessageChannel) { this.pendingCallback = void 0; } else { clearTimeout(this.immediateId); } this.immediateId = void 0; } }; // src/core/event-queue-scheduler/OIMEventQueueSchedulerFactory.ts var OIMEventQueueSchedulerFactory = class { /** * Create a scheduler of the specified type with optional configuration. */ static create(type, ...args) { switch (type) { case "microtask": return new OIMEventQueueSchedulerMicrotask(); case "animationFrame": return new OIMEventQueueSchedulerAnimationFrame(); case "timeout": { const options = args[0]; return new OIMEventQueueSchedulerTimeout(options?.delay); } case "immediate": return new OIMEventQueueSchedulerImmediate(); default: throw new Error(`Unknown scheduler type: ${type}`); } } /** * Create a microtask scheduler (most common for general use). */ static createMicrotask() { return new OIMEventQueueSchedulerMicrotask(); } /** * Create an animation frame scheduler (ideal for UI updates). */ static createAnimationFrame() { return new OIMEventQueueSchedulerAnimationFrame(); } /** * Create a timeout scheduler with optional delay. */ static createTimeout(delay) { return new OIMEventQueueSchedulerTimeout(delay); } /** * Create an immediate scheduler (fastest execution). */ static createImmediate() { return new OIMEventQueueSchedulerImmediate(); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { EOIMCollectionEventType, EOIMEventQueueEventType, EOIMEventQueueSchedulerEventType, EOIMIndexEventType, EOIMUpdateEventCoalescerEventType, OIMCollection, OIMCollectionStore, OIMCollectionStoreMapDriven, OIMComparatorFactory, OIMDBSettings, OIMEntityUpdaterFactory, OIMEventEmitter, OIMEventQueue, OIMEventQueueScheduler, OIMEventQueueSchedulerAnimationFrame, OIMEventQueueSchedulerFactory, OIMEventQueueSchedulerImmediate, OIMEventQueueSchedulerMicrotask, OIMEventQueueSchedulerTimeout, OIMIndexArrayBased, OIMIndexComparatorFactory, OIMIndexManualArrayBased, OIMIndexManualSetBased, OIMIndexSetBased, OIMIndexStoreArrayBased, OIMIndexStoreMapDrivenArrayBased, OIMIndexStoreMapDrivenSetBased, OIMIndexStoreSetBased, OIMMap2Keys, OIMPkSelectorFactory, OIMRICollection, OIMReactiveCollection, OIMReactiveIndexArrayBased, OIMReactiveIndexManualArrayBased, OIMReactiveIndexManualSetBased, OIMReactiveIndexSetBased, OIMUpdateEventCoalescer, OIMUpdateEventCoalescerCollection, OIMUpdateEventCoalescerIndex, OIMUpdateEventEmitter });