UNPKG

@oimdb/redux-adapter

Version:

Redux adapter for OIMDB - Create Redux reducers from OIMDB collections and indexes

811 lines (801 loc) 27.4 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, { EOIMDBReduxReducerActionType: () => EOIMDBReduxReducerActionType, OIMDBReduxAdapter: () => OIMDBReduxAdapter, arraysEqual: () => arraysEqual, arraysEqualPk: () => arraysEqualPk, defaultCollectionMapper: () => defaultCollectionMapper, defaultIndexMapper: () => defaultIndexMapper, findUpdatedInArray: () => findUpdatedInArray, findUpdatedInRecord: () => findUpdatedInRecord }); module.exports = __toCommonJS(index_exports); // src/core/OIMDBReduxAdapter.ts var import_core = require("@oimdb/core"); // src/enum/EOIMDBReduxReducerActionType.ts var EOIMDBReduxReducerActionType = /* @__PURE__ */ ((EOIMDBReduxReducerActionType2) => { EOIMDBReduxReducerActionType2["UPDATE"] = "OIMDB_UPDATE"; return EOIMDBReduxReducerActionType2; })(EOIMDBReduxReducerActionType || {}); // src/core/OIMDBReduxDefaultMappers.ts function defaultCollectionMapper(collection, updatedKeys, currentState) { if (!currentState) { const allPks = collection.getAllPks(); const pkCount = allPks.length; const entities = /* @__PURE__ */ Object.create(null); const ids = []; ids.length = pkCount; let writeIndex2 = 0; for (let i = 0; i < pkCount; i++) { const pk = allPks[i]; const entity = collection.getOneByPk(pk); if (entity) { entities[pk] = entity; ids[writeIndex2++] = pk; } } ids.length = writeIndex2; return { entities, ids }; } const newEntities = Object.assign({}, currentState.entities); const updatedKeysArray = Array.from(updatedKeys); const updatedKeysLength = updatedKeysArray.length; const idsToAdd = /* @__PURE__ */ new Set(); const idsToRemove = /* @__PURE__ */ new Set(); for (let i = 0; i < updatedKeysLength; i++) { const pk = updatedKeysArray[i]; const entity = collection.getOneByPk(pk); if (entity) { newEntities[pk] = entity; if (!currentState.entities[pk]) { idsToAdd.add(pk); } } else { delete newEntities[pk]; if (currentState.entities[pk]) { idsToRemove.add(pk); } } } const currentIds = currentState.ids; const currentIdsLength = currentIds.length; const newIds = []; newIds.length = currentIdsLength + idsToAdd.size; let writeIndex = 0; for (let i = 0; i < currentIdsLength; i++) { const id = currentIds[i]; if (!idsToRemove.has(id)) { newIds[writeIndex++] = id; } } const idsToAddArray = Array.from(idsToAdd); const idsToAddLength = idsToAddArray.length; for (let i = 0; i < idsToAddLength; i++) { newIds[writeIndex++] = idsToAddArray[i]; } newIds.length = writeIndex; return { entities: newEntities, ids: newIds }; } function defaultIndexMapper(index, updatedKeys, currentState) { if (!currentState) { const allKeys = index.getKeys(); const keysLength = allKeys.length; const entities = /* @__PURE__ */ Object.create(null); for (let i = 0; i < keysLength; i++) { const key = allKeys[i]; const pks = index.getPksByKey(key); const ids = pks instanceof Set ? Array.from(pks) : pks; entities[key] = { id: key, ids }; } return { entities }; } const newEntities = Object.assign({}, currentState.entities); const updatedKeysArray = Array.from(updatedKeys); const updatedKeysLength = updatedKeysArray.length; for (let i = 0; i < updatedKeysLength; i++) { const key = updatedKeysArray[i]; const pks = index.getPksByKey(key); const ids = pks instanceof Set ? Array.from(pks) : pks; newEntities[key] = { id: key, ids }; } return { entities: newEntities }; } // src/utils/findUpdatedEntities.ts function findUpdatedInRecord(oldEntities, newEntities) { const added = /* @__PURE__ */ new Set(); const updated = /* @__PURE__ */ new Set(); const removed = /* @__PURE__ */ new Set(); for (const pk in newEntities) { if (Object.prototype.hasOwnProperty.call(newEntities, pk)) { if (!(pk in oldEntities)) { added.add(pk); } else if (oldEntities[pk] !== newEntities[pk]) { updated.add(pk); } } } for (const pk in oldEntities) { if (Object.prototype.hasOwnProperty.call(oldEntities, pk)) { if (!(pk in newEntities)) { removed.add(pk); } } } const all = /* @__PURE__ */ new Set(); const addedArray = Array.from(added); const updatedArray = Array.from(updated); const removedArray = Array.from(removed); const addedLength = addedArray.length; const updatedLength = updatedArray.length; const removedLength = removedArray.length; for (let i = 0; i < addedLength; i++) { all.add(addedArray[i]); } for (let i = 0; i < updatedLength; i++) { all.add(updatedArray[i]); } for (let i = 0; i < removedLength; i++) { all.add(removedArray[i]); } return { added, updated, removed, all }; } function findUpdatedInArray(oldArray, newArray) { const oldSet = new Set(oldArray); const newSet = new Set(newArray); const oldLength = oldArray.length; const newLength = newArray.length; const added = []; const updated = []; const removed = []; for (let i = 0; i < newLength; i++) { const pk = newArray[i]; if (!oldSet.has(pk)) { added.push(pk); } else { updated.push(pk); } } for (let i = 0; i < oldLength; i++) { const pk = oldArray[i]; if (!newSet.has(pk)) { removed.push(pk); } } const allLength = added.length + updated.length + removed.length; const all = []; all.length = allLength; let writeIndex = 0; for (let i = 0; i < added.length; i++) { all[writeIndex++] = added[i]; } for (let i = 0; i < updated.length; i++) { all[writeIndex++] = updated[i]; } for (let i = 0; i < removed.length; i++) { all[writeIndex++] = removed[i]; } all.length = writeIndex; return { added, updated, removed, all }; } // src/core/OIMDBReduxLinkedIndexesUpdater.ts var OIMDBReduxLinkedIndexesUpdater = class { /** * Update linked indexes for added and updated entities */ updateLinkedIndexesForEntities(linkedIndexes, updatedPks, oldEntities, newEntities) { if (linkedIndexes.length === 0) { return; } const updatedPksLength = updatedPks.length; for (let i = 0; i < updatedPksLength; i++) { const pk = updatedPks[i]; const oldEntity = oldEntities[pk]; const newEntity = newEntities[pk]; if (!newEntity) continue; const linkedIndexesLength = linkedIndexes.length; for (let j = 0; j < linkedIndexesLength; j++) { const linkedIndex = linkedIndexes[j]; const fieldName = linkedIndex.fieldName; const oldArray = oldEntity ? oldEntity[fieldName] : void 0; const newArray = newEntity[fieldName]; if (oldArray !== newArray) { this.updateIndexForEntity( linkedIndex, pk, oldArray, newArray ); } } } } /** * Remove linked indexes for removed entities */ removeLinkedIndexesForEntities(linkedIndexes, removedPks) { if (linkedIndexes.length === 0 || removedPks.length === 0) { return; } const removedPksLength = removedPks.length; for (let i = 0; i < removedPksLength; i++) { const entityPk = removedPks[i]; const linkedIndexesLength = linkedIndexes.length; for (let j = 0; j < linkedIndexesLength; j++) { const linkedIndex = linkedIndexes[j]; const indexKey = entityPk; const existingPks = Array.from( linkedIndex.index.getPksByKey(indexKey) ); if (existingPks.length > 0) { const indexManual = linkedIndex.index; if (indexManual.removePks) { indexManual.removePks(indexKey, existingPks); } else { if (indexManual.clear) { indexManual.clear(indexKey); } else if (indexManual.setPks) { indexManual.setPks(indexKey, []); } } } } } } /** * Update a single index for a single entity */ updateIndexForEntity(linkedIndex, entityPk, oldArray, newArray) { const indexManual = linkedIndex.index; const indexKey = entityPk; if (indexManual.setPks) { indexManual.setPks(indexKey, newArray ?? []); } else if (indexManual.addPks && indexManual.removePks) { const oldArrayForIteration = oldArray ?? []; const newArrayForIteration = newArray ?? []; const oldSet = oldArrayForIteration.length > 0 ? new Set(oldArrayForIteration) : null; const newSet = newArrayForIteration.length > 0 ? new Set(newArrayForIteration) : null; const toRemove = []; const oldArrayLength = oldArrayForIteration.length; if (oldArrayLength > 0) { if (newSet) { for (let i = 0; i < oldArrayLength; i++) { const valuePk = oldArrayForIteration[i]; if (!newSet.has(valuePk)) { toRemove.push(valuePk); } } } else { for (let i = 0; i < oldArrayLength; i++) { toRemove.push(oldArrayForIteration[i]); } } } const toAdd = []; const newArrayLength = newArrayForIteration.length; if (newArrayLength > 0) { if (oldSet) { for (let i = 0; i < newArrayLength; i++) { const valuePk = newArrayForIteration[i]; if (!oldSet.has(valuePk)) { toAdd.push(valuePk); } } } else { for (let i = 0; i < newArrayLength; i++) { toAdd.push(newArrayForIteration[i]); } } } if (toRemove.length > 0) { indexManual.removePks(indexKey, toRemove); } if (toAdd.length > 0) { indexManual.addPks(indexKey, toAdd); } } } }; // src/core/OIMDBReduxReducerFactory.ts var OIMDBReduxReducerFactory = class { /** * Create Redux reducer for a collection */ createCollectionReducer(collection, reducerData, child) { const actualMapper = reducerData.mapper; let isSyncingFromChild = false; const reducer = (state, action) => { if (state === void 0) { const allPks = new Set(collection.getAllPks()); return actualMapper(collection, allPks, void 0); } if (action.type === "OIMDB_UPDATE" /* UPDATE */) { if (isSyncingFromChild) { return state; } if (reducerData.updatedKeys === null) { return state; } const updatedKeys = reducerData.updatedKeys; const newState = actualMapper( collection, updatedKeys, state ); reducerData.updatedKeys = null; if (child && child.linkedIndexes && child.linkedIndexes.length > 0) { const oldState = state; const newStateTyped = newState; if (oldState && newStateTyped && typeof oldState === "object" && typeof newStateTyped === "object" && "entities" in oldState && "entities" in newStateTyped) { const diff = findUpdatedInRecord( oldState.entities, newStateTyped.entities ); const addedArray = Array.from(diff.added); const updatedArray = Array.from(diff.updated); const removedArray = Array.from(diff.removed); const allUpdatedPksArray = []; allUpdatedPksArray.length = addedArray.length + updatedArray.length; let writeIndex = 0; for (let i = 0; i < addedArray.length; i++) { allUpdatedPksArray[writeIndex++] = addedArray[i]; } for (let i = 0; i < updatedArray.length; i++) { allUpdatedPksArray[writeIndex++] = updatedArray[i]; } const linkedIndexesUpdater = new OIMDBReduxLinkedIndexesUpdater(); linkedIndexesUpdater.updateLinkedIndexesForEntities( child.linkedIndexes, allUpdatedPksArray, oldState.entities, newStateTyped.entities ); linkedIndexesUpdater.removeLinkedIndexesForEntities( child.linkedIndexes, removedArray ); } } return newState; } if (child) { if (state === void 0) { return void 0; } const childState = child.reducer(state, action); if (childState !== state && childState !== void 0) { const getPk = child.getPk ?? ((entity) => { const entityAny = entity; if ("id" in entityAny) { return entityAny.id; } throw new Error( "Cannot determine primary key. Provide getPk in child options" ); }); if (child.extractEntities) { isSyncingFromChild = true; child.extractEntities( state, childState, collection, getPk ); isSyncingFromChild = false; } else { const defaultState = childState; if (defaultState && typeof defaultState === "object" && "entities" in defaultState && "ids" in defaultState) { const currentPks = collection.getAllPks(); const oldEntities = /* @__PURE__ */ Object.create(null); for (let i = 0; i < currentPks.length; i++) { const pk = currentPks[i]; const entity = collection.getOneByPk(pk); if (entity) { oldEntities[pk] = entity; } } const diff = findUpdatedInRecord( oldEntities, defaultState.entities ); isSyncingFromChild = true; const addedArray = Array.from(diff.added); const updatedArray = Array.from(diff.updated); const entitiesToUpsert = []; const addedLength = addedArray.length; const updatedLength = updatedArray.length; for (let i = 0; i < addedLength; i++) { const pk = addedArray[i]; if (defaultState.entities[pk]) { entitiesToUpsert.push( defaultState.entities[pk] ); } } for (let i = 0; i < updatedLength; i++) { const pk = updatedArray[i]; if (defaultState.entities[pk]) { entitiesToUpsert.push( defaultState.entities[pk] ); } } if (entitiesToUpsert.length > 0) { collection.upsertMany(entitiesToUpsert); } const removedArray = Array.from(diff.removed); const removedLength = removedArray.length; if (removedLength > 0) { const entitiesToRemove = []; for (let i = 0; i < removedLength; i++) { const pk = removedArray[i]; const entity = oldEntities[pk]; if (entity) { entitiesToRemove.push(entity); } } if (entitiesToRemove.length > 0) { collection.removeMany(entitiesToRemove); } } if (child.linkedIndexes && child.linkedIndexes.length > 0) { const allUpdatedPksArray = []; allUpdatedPksArray.length = addedArray.length + updatedArray.length; let writeIndex = 0; for (let i = 0; i < addedArray.length; i++) { allUpdatedPksArray[writeIndex++] = addedArray[i]; } for (let i = 0; i < updatedArray.length; i++) { allUpdatedPksArray[writeIndex++] = updatedArray[i]; } const linkedIndexesUpdater = new OIMDBReduxLinkedIndexesUpdater(); linkedIndexesUpdater.updateLinkedIndexesForEntities( child.linkedIndexes, allUpdatedPksArray, oldEntities, defaultState.entities ); linkedIndexesUpdater.removeLinkedIndexesForEntities( child.linkedIndexes, removedArray ); } isSyncingFromChild = false; } } return childState; } return childState; } return state; }; return reducer; } /** * Create Redux reducer for an index */ createIndexReducer(index, reducerData, child) { const actualMapper = reducerData.mapper; let isSyncingFromChild = false; const reducer = (state, action) => { if (state === void 0) { const allKeys = new Set(index.getKeys()); return actualMapper(index, allKeys, void 0); } if (action.type === "OIMDB_UPDATE" /* UPDATE */) { if (isSyncingFromChild) { return state; } if (reducerData.updatedKeys === null) { return state; } const updatedKeys = reducerData.updatedKeys; const newState = actualMapper( index, updatedKeys, state ); reducerData.updatedKeys = null; return newState; } if (child) { if (state === void 0) { return void 0; } const childState = child.reducer(state, action); if (childState !== state && childState !== void 0) { if (child.extractIndexState) { isSyncingFromChild = true; child.extractIndexState( state, childState, index ); isSyncingFromChild = false; } else { const defaultState = childState; if (defaultState && typeof defaultState === "object" && "entities" in defaultState) { const currentKeys = index.getKeys(); const oldEntities = /* @__PURE__ */ Object.create(null); for (let i = 0; i < currentKeys.length; i++) { const key = currentKeys[i]; const pks = index.getPksByKey(key); const ids = pks instanceof Set ? Array.from(pks) : pks; oldEntities[key] = { id: key, ids }; } const newEntities = defaultState.entities; const allKeys = /* @__PURE__ */ new Set([ ...Object.keys(oldEntities), ...Object.keys(newEntities) ]); isSyncingFromChild = true; const indexManual = index; const allKeysArray = Array.from(allKeys); for (let i = 0; i < allKeysArray.length; i++) { const key = allKeysArray[i]; const oldEntry = oldEntities[key]; const newEntry = newEntities[key]; if (!oldEntry && newEntry) { if (newEntry.ids.length > 0) { if (indexManual.addPks) { indexManual.addPks( key, newEntry.ids ); } else if (indexManual.setPks) { indexManual.setPks( key, newEntry.ids ); } } } else if (oldEntry && !newEntry) { const oldPks = oldEntry.ids; if (oldPks.length > 0) { if (indexManual.removePks) { indexManual.removePks(key, oldPks); } else if (indexManual.setPks) { indexManual.setPks(key, []); } } } else if (oldEntry && newEntry) { const oldPks = new Set(oldEntry.ids); const newPks = new Set(newEntry.ids); const toAdd = []; const newPksArray = Array.from(newPks); for (let i2 = 0; i2 < newPksArray.length; i2++) { const pk = newPksArray[i2]; if (!oldPks.has(pk)) { toAdd.push(pk); } } const toRemove = []; const oldPksArray = Array.from(oldPks); for (let i2 = 0; i2 < oldPksArray.length; i2++) { const pk = oldPksArray[i2]; if (!newPks.has(pk)) { toRemove.push(pk); } } if (indexManual.addPks && indexManual.removePks) { if (toRemove.length > 0) { indexManual.removePks( key, toRemove ); } if (toAdd.length > 0) { indexManual.addPks(key, toAdd); } } else if (indexManual.setPks) { indexManual.setPks(key, newEntry.ids); } } } isSyncingFromChild = false; } } return childState; } return childState; } return state; }; return reducer; } }; // src/core/OIMDBReduxAdapter.ts var OIMDBReduxAdapter = class { constructor(queue, options) { this.isFlushingSilently = false; // Track reducers and their updated keys // Using object and TOIMPk as base types to allow storing different concrete types this.collectionReducers = /* @__PURE__ */ new Map(); this.indexReducers = /* @__PURE__ */ new Map(); this.queue = queue; this.options = options ?? {}; this.reducerFactory = new OIMDBReduxReducerFactory(); this.queueFlushHandler = () => { if (this.isFlushingSilently) { return; } if (this.store) { this.store.dispatch({ type: "OIMDB_UPDATE" /* UPDATE */ }); } }; this.queue.emitter.on( import_core.EOIMEventQueueEventType.AFTER_FLUSH, this.queueFlushHandler ); } /** * Set Redux store (can be called later when store is created) */ setStore(store) { this.store = store; } /** * Flush the event queue without triggering OIMDB_UPDATE dispatch. * Useful for processing events that were triggered by Redux actions * (e.g., through child reducers) without causing unnecessary Redux updates. */ flushSilently() { if (this.isFlushingSilently) { return; } this.isFlushingSilently = true; if (this.queueFlushHandler) { this.queue.emitter.off( import_core.EOIMEventQueueEventType.AFTER_FLUSH, this.queueFlushHandler ); } this.queue.flush(); if (this.queueFlushHandler) { this.queue.emitter.on( import_core.EOIMEventQueueEventType.AFTER_FLUSH, this.queueFlushHandler ); } this.isFlushingSilently = false; } /** * Create Redux middleware that automatically flushes the event queue * after every action. This ensures that when Redux updates OIMDB collections * through child reducers, all events are processed synchronously. * * @returns Redux middleware * * @example * ```typescript * import { createStore, applyMiddleware } from 'redux'; * import { OIMDBReduxAdapter } from '@oimdb/redux-adapter'; * * const adapter = new OIMDBReduxAdapter(queue); * const middleware = adapter.createMiddleware(); * * const store = createStore( * rootReducer, * applyMiddleware(middleware) * ); * * adapter.setStore(store); * ``` */ createMiddleware() { return (_store) => (next) => (action) => { const result = next(action); this.flushSilently(); return result; }; } /** * Create Redux reducer for a collection * @param collection - The reactive collection * @param child - Optional child reducer that handles custom actions and syncs changes back to OIMDB * @param mapper - Optional mapper for converting OIMDB state to Redux state */ createCollectionReducer(collection, child, mapper) { const actualMapper = mapper ?? this.options.defaultCollectionMapper ?? defaultCollectionMapper; const reducerData = { updatedKeys: null, mapper: actualMapper }; this.collectionReducers.set( collection, reducerData ); const beforeFlushHandler = () => { const updatedKeys = collection.coalescer.getUpdatedKeys(); reducerData.updatedKeys = updatedKeys; }; collection.coalescer.emitter.on( import_core.EOIMUpdateEventCoalescerEventType.BEFORE_FLUSH, beforeFlushHandler ); return this.reducerFactory.createCollectionReducer( collection, reducerData, child ); } /** * Create Redux reducer for an index * @param index - The reactive index * @param child - Optional child reducer that handles custom actions and syncs changes back to OIMDB * @param mapper - Optional mapper for converting OIMDB state to Redux state */ createIndexReducer(index, child, mapper) { const actualMapper = mapper ?? this.options.defaultIndexMapper ?? defaultIndexMapper; const reducerData = { updatedKeys: null, mapper: actualMapper }; this.indexReducers.set( index, reducerData ); const beforeFlushHandler = () => { const updatedKeys = index.coalescer.getUpdatedKeys(); reducerData.updatedKeys = updatedKeys; }; index.coalescer.emitter.on( import_core.EOIMUpdateEventCoalescerEventType.BEFORE_FLUSH, beforeFlushHandler ); return this.reducerFactory.createIndexReducer( index, reducerData, child ); } }; // src/utils/arraysEqual.ts function arraysEqual(a, b) { if (a.length !== b.length) { return false; } for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) { return false; } } return true; } function arraysEqualPk(a, b) { return arraysEqual(a, b); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { EOIMDBReduxReducerActionType, OIMDBReduxAdapter, arraysEqual, arraysEqualPk, defaultCollectionMapper, defaultIndexMapper, findUpdatedInArray, findUpdatedInRecord });