@yunu-lab/rpc-react
Version:
🚀 **React + RPC = ❤️**
534 lines (533 loc) • 16.9 kB
JavaScript
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const toolkit = require("@reduxjs/toolkit");
const jsxRuntime = require("react/jsx-runtime");
const React = require("react");
const reactRedux = require("react-redux");
const rpcTs = require("@yunu-lab/rpc-ts");
const extendStore = (options) => {
const { repository, store, slices = {} } = options;
const rpcSlice = toolkit.createSlice({
name: "rpc",
initialState: {},
reducers: {
setData: (state, action) => {
const { type, data } = action.payload;
state[type] = data;
}
}
});
const newReducer = toolkit.combineReducers({
...slices,
rpc: rpcSlice.reducer
});
store.replaceReducer(newReducer);
const unsubscribe = repository.onDataChanged((events) => {
events.forEach((event) => {
const { type, payload } = event;
const storageType = repository.getStorageType(String(type));
if (storageType === "singleton") {
const nextValue = Array.isArray(payload) ? payload[0] ?? null : payload ?? null;
store.dispatch(
rpcSlice.actions.setData({
type: String(type),
data: nextValue
})
);
} else {
const nextArray = Array.isArray(payload) ? payload : Object.values(payload || {});
store.dispatch(
rpcSlice.actions.setData({
type: String(type),
data: nextArray
})
);
}
});
});
return {
store,
repository,
unsubscribe
};
};
const RpcContext = React.createContext(null);
const RpcProvider = ({
children,
repository
}) => {
return /* @__PURE__ */ jsxRuntime.jsx(RpcContext.Provider, { value: { repository }, children });
};
const useRpc = () => {
const context = React.useContext(RpcContext);
if (!context) {
throw new Error("useRpc must be used within an RpcProvider");
}
const repository = React.useMemo(
() => context.repository,
[context.repository]
);
return {
repository
};
};
const toPascalCase = (s) => {
return s.split("_").map(
(word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
).join("");
};
const createRpcHooks = (typeKeys) => {
const EMPTY_MAP = Object.freeze({});
const hooks = {};
typeKeys.forEach((typeName) => {
const hookName = `use${toPascalCase(
String(typeName)
)}`;
const typeKey = typeName;
function useHook(id) {
const { repository } = useRpc();
const allData = reactRedux.useSelector(
(state) => state.rpc[typeKey] ?? EMPTY_MAP
);
const storageType = repository.getStorageType?.(
String(typeKey)
);
const list = React.useMemo(() => {
if (storageType === "singleton") return [];
if (Array.isArray(allData)) {
return allData;
}
return Object.values(allData || {});
}, [allData, storageType]);
const findById = React.useCallback(
(id2) => repository.findById(typeKey, id2),
[repository]
);
const findAll = React.useCallback(
() => repository.findAll(typeKey),
[repository]
);
const mergeRpc = React.useCallback(
(data) => {
repository.mergeRpc(typeKey, data);
},
[repository]
);
const clear = React.useCallback(() => {
repository.clear(typeKey);
}, [repository]);
const dataForKey = storageType === "singleton" ? allData ?? null : list;
const mapForKey = React.useMemo(() => {
if (storageType === "singleton") {
return allData ?? null;
}
const record = {};
for (const item of list) {
if (item && "id" in item) {
record[String(item.id)] = item;
}
}
return record;
}, [allData, list, storageType]);
if (id !== void 0) {
return findById(id);
}
return {
[`${String(typeKey)}s`]: dataForKey,
[`${String(typeKey)}Map`]: mapForKey,
findById,
findAll,
mergeRpc,
clear
};
}
hooks[hookName] = useHook;
const fullRelatedHookName = `use${toPascalCase(
String(typeName)
)}FullRelatedData`;
function useFullRelatedHook(id) {
const { repository } = useRpc();
const allRpcData = reactRedux.useSelector(
(state) => state.rpc
);
const rpcVersion = React.useMemo(() => {
return Object.keys(allRpcData).length;
}, [allRpcData]);
const fullData = React.useMemo(() => {
try {
return repository.getFullRelatedData(
typeName,
id
);
} catch {
return null;
}
}, [repository, id, rpcVersion]);
return fullData;
}
hooks[fullRelatedHookName] = useFullRelatedHook;
});
typeKeys.forEach((typeName) => {
const listenerHookName = `use${toPascalCase(
String(typeName)
)}Listener`;
function useListenerHook(callback) {
const { repository } = useRpc();
const callbackRef = React.useRef(callback);
const listenerIdRef = React.useRef(null);
callbackRef.current = callback;
const storageType = React.useMemo(() => {
return repository.getStorageType?.(String(typeName));
}, [repository]);
const invoke = React.useCallback(
(events) => {
if (!events || events.length === 0) return;
const event = events[0];
if (storageType === "singleton" && Array.isArray(event.payload) && event.payload.length > 0) {
const singletonEvent = {
...event,
payload: event.payload[0]
};
callbackRef.current(singletonEvent);
} else {
callbackRef.current(event);
}
},
[storageType]
);
React.useEffect(() => {
if (listenerIdRef.current && typeof repository.offDataChanged === "function") {
repository.offDataChanged(listenerIdRef.current);
listenerIdRef.current = null;
}
listenerIdRef.current = repository.onDataChanged(
invoke,
{
types: [typeName]
}
);
return () => {
if (listenerIdRef.current && typeof repository.offDataChanged === "function") {
repository.offDataChanged(
listenerIdRef.current
);
listenerIdRef.current = null;
}
};
}, [repository, invoke]);
return () => {
if (listenerIdRef.current && typeof repository.offDataChanged === "function") {
repository.offDataChanged(listenerIdRef.current);
listenerIdRef.current = null;
}
};
}
hooks[listenerHookName] = useListenerHook;
});
function useDataListener(callback, options) {
const { repository } = useRpc();
const callbackRef = React.useRef(callback);
const listenerIdRef = React.useRef(null);
callbackRef.current = callback;
const types = options?.types || typeKeys;
const typesKey = React.useMemo(() => {
const sorted = [...types].sort();
return sorted.join(",");
}, [types]);
const stableTypes = React.useMemo(() => {
return options?.types || typeKeys;
}, [typesKey, options?.types]);
React.useEffect(() => {
if (listenerIdRef.current && typeof repository.offDataChanged === "function") {
repository.offDataChanged(listenerIdRef.current);
listenerIdRef.current = null;
}
listenerIdRef.current = repository.onDataChanged(
callbackRef.current,
{
types: stableTypes
}
);
return () => {
if (listenerIdRef.current && typeof repository.offDataChanged === "function") {
repository.offDataChanged(listenerIdRef.current);
listenerIdRef.current = null;
}
};
}, [repository, stableTypes]);
return () => {
if (listenerIdRef.current && typeof repository.offDataChanged === "function") {
repository.offDataChanged(listenerIdRef.current);
listenerIdRef.current = null;
}
};
}
hooks.useDataListener = useDataListener;
typeKeys.forEach((typeName) => {
const relatedHookName = `use${toPascalCase(
String(typeName)
)}Related`;
function useRelatedHook(id, targetType) {
const { repository } = useRpc();
const sourceData = reactRedux.useSelector(
(state) => state.rpc[typeName] ?? EMPTY_MAP
);
const targetData = reactRedux.useSelector(
(state) => state.rpc[targetType] ?? EMPTY_MAP
);
const dataVersion = React.useMemo(() => {
const sourceKeys = Object.keys(sourceData).length;
const targetKeys = Object.keys(targetData).length;
return `${sourceKeys}:${targetKeys}`;
}, [sourceData, targetData]);
const relatedData = React.useMemo(() => {
try {
return repository.getRelated(
typeName,
id,
targetType
);
} catch {
return [];
}
}, [repository, id, targetType, dataVersion]);
return relatedData;
}
hooks[relatedHookName] = useRelatedHook;
});
function useHandleMessages() {
const { repository } = useRpc();
const handleMessages = (messages, callbacks) => {
repository.handleMessages(messages, callbacks);
};
const handleMessagesTyped = (messages, callbacks) => {
repository.handleMessages(messages, callbacks);
};
return { handleMessages, handleMessagesTyped };
}
hooks.useHandleMessages = useHandleMessages;
return hooks;
};
function useInlineRpcStore(rpcs) {
const typeNames = React.useMemo(
() => Object.keys(rpcs),
[rpcs]
);
const typeNamesKey = React.useMemo(
() => Object.keys(rpcs).map(String).sort().join("|"),
[rpcs]
);
const repository = React.useMemo(() => {
const repo = new rpcTs.RpcRepository();
typeNames.forEach((key) => {
const { rpc, storageType } = rpcs[key];
repo.registerRpc(String(key), rpc, { storageType });
});
return repo;
}, [typeNamesKey]);
const [rpcState, setRpcState] = React.useState({});
React.useEffect(() => {
const unsubscribe = repository.onDataChanged(
(events) => {
console.log(events);
setRpcState((prev) => {
const next = { ...prev };
for (const event of events) {
const type = event.type;
const storageType = rpcs[type]?.storageType ?? repository.getStorageType?.(String(type));
if (storageType === "singleton") {
next[type] = Array.isArray(event.payload) ? event.payload[0] ?? null : event.payload ?? null;
} else {
const nextArray = Array.isArray(event.payload) ? event.payload : Object.values(event.payload || {});
next[type] = nextArray;
}
}
return next;
});
},
{
types: typeNames
}
);
return () => {
repository.offDataChanged(unsubscribe);
};
}, [repository, typeNamesKey]);
const useInlineSelector = React.useCallback(
(selector) => {
return selector(rpcState);
},
[rpcState]
);
const hooks = React.useMemo(() => {
const toPascalCase2 = (s) => s.split("_").map(
(word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
).join("");
const result = {};
Object.keys(rpcs).forEach((typeName) => {
const storageType = rpcs[typeName].storageType;
const hookName = `use${toPascalCase2(String(typeName))}`;
function useTypeHook(id) {
const allData = useInlineSelector((state) => state[typeName]);
const findById = React.useCallback(
(itemId) => repository.findById(typeName, itemId),
[]
);
const findAll = React.useCallback(
() => repository.findAll(typeName),
[]
);
const mergeRpc = React.useCallback(
(data) => {
repository.mergeRpc(typeName, data);
},
[]
);
const clear = React.useCallback(() => {
if (storageType === "singleton") {
setRpcState((prev) => ({ ...prev, [typeName]: null }));
} else {
const ids = Array.isArray(
allData
) ? allData.map(
(item) => item ? item.id : void 0
).filter((v) => v !== void 0) : Object.keys(allData || {});
const deletePayload = {};
for (const id2 of ids) deletePayload[id2] = null;
if (Object.keys(deletePayload).length > 0) {
repository.mergeRpc(typeName, deletePayload);
}
setRpcState((prev) => ({ ...prev, [typeName]: [] }));
}
}, [allData]);
const list = React.useMemo(
() => Array.isArray(allData) ? allData : Object.values(allData || {}),
[allData]
);
const collectionMap = React.useMemo(() => {
const rec = {};
if (Array.isArray(list)) {
for (const item of list) {
if (item && typeof item === "object" && !Array.isArray(item) && "id" in item) {
rec[item.id] = item;
}
}
}
return rec;
}, [list]);
if (id !== void 0) {
return findById(id);
}
if (storageType === "singleton") {
const current = allData || null;
return {
[`${String(typeName)}s`]: current,
[`${String(typeName)}Map`]: current,
findById,
findAll,
mergeRpc,
clear
};
}
return {
[`${String(typeName)}s`]: list,
[`${String(typeName)}Map`]: collectionMap,
findById,
findAll,
mergeRpc,
clear
};
}
result[hookName] = useTypeHook;
const listenerHookName = `use${toPascalCase2(
String(typeName)
)}Listener`;
function useListenerHook(callback) {
const callbackRef = React.useRef(callback);
callbackRef.current = callback;
React.useEffect(() => {
const unsub = repository.onDataChanged(
(events) => {
const filtered = events.filter(
(e) => e.type === typeName
);
if (filtered.length === 0) return;
const event = filtered[0];
const storageType2 = repository.getStorageType(
String(typeName)
);
if (storageType2 === "singleton" && Array.isArray(event.payload)) {
const payload = event.payload[0] ?? null;
if (payload) {
callbackRef.current({
type: typeName,
payload
});
}
} else {
callbackRef.current(event);
}
},
{ types: [typeName] }
);
return () => {
repository.offDataChanged(unsub);
};
}, []);
return () => {
};
}
result[listenerHookName] = useListenerHook;
});
function useDataListener(callback, options) {
const callbackRef = React.useRef(callback);
callbackRef.current = callback;
const types = options?.types ?? typeNames;
const typesKey = React.useMemo(
() => [...types].map(String).sort().join("|"),
[types]
);
React.useEffect(() => {
const unsub = repository.onDataChanged(
(events) => callbackRef.current(events),
{ types }
);
return () => {
repository.offDataChanged(unsub);
};
}, [typesKey, types]);
return () => {
};
}
result.useDataListener = useDataListener;
function useHandleMessages() {
const handleMessages = (messages, callbacks) => {
repository.handleMessages(messages, callbacks);
};
const handleMessagesTyped = (messages, callbacks) => {
repository.handleMessages(
messages,
callbacks
);
};
return { handleMessages, handleMessagesTyped };
}
result.useHandleMessages = useHandleMessages;
return result;
}, [rpcs, repository, useInlineSelector, typeNames]);
return { hooks, repository };
}
Object.defineProperty(exports, "Rpc", {
enumerable: true,
get: () => rpcTs.Rpc
});
Object.defineProperty(exports, "RpcRepository", {
enumerable: true,
get: () => rpcTs.RpcRepository
});
exports.RpcProvider = RpcProvider;
exports.createRpcHooks = createRpcHooks;
exports.extendStore = extendStore;
exports.useInlineRpcStore = useInlineRpcStore;
exports.useRpc = useRpc;
//# sourceMappingURL=index.js.map