@oimdb/redux-adapter
Version:
Redux adapter for OIMDB - Create Redux reducers from OIMDB collections and indexes
780 lines (772 loc) • 26 kB
JavaScript
// src/core/OIMDBReduxAdapter.ts
import {
EOIMUpdateEventCoalescerEventType,
EOIMEventQueueEventType
} from "@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(
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(
EOIMEventQueueEventType.AFTER_FLUSH,
this.queueFlushHandler
);
}
this.queue.flush();
if (this.queueFlushHandler) {
this.queue.emitter.on(
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(
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(
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);
}
export {
EOIMDBReduxReducerActionType,
OIMDBReduxAdapter,
arraysEqual,
arraysEqualPk,
defaultCollectionMapper,
defaultIndexMapper,
findUpdatedInArray,
findUpdatedInRecord
};