@donverduyn/react-runtime
Version:
effect-ts react integration
1,176 lines (1,151 loc) • 159 kB
JavaScript
'use strict';
var uuid = require('uuid');
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var reactDom = require('react-dom');
var ReactDOM = require('react-dom/client');
var moize = require('moize');
var effect = require('effect');
var Predicate = require('effect/Predicate');
var Stream = require('effect/Stream');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
const useIsoLayoutEffect = typeof window !== 'undefined' ? React__namespace.useLayoutEffect : React__namespace.useEffect;
const useLayoutEffectOnce = (fn) => {
const disposed = React__namespace.useRef(false);
const fnRef = React__namespace.useRef(fn);
fnRef.current = fn;
React__namespace.useLayoutEffect(() => {
if (!disposed.current) {
fnRef.current();
}
else {
disposed.current = false;
}
return () => {
disposed.current = true;
};
}, []);
};
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-explicit-any */
function combineSetsFromMap(map) {
const combined = new Set();
for (const set of map.values()) {
for (const item of set) {
combined.add(item);
}
}
return combined;
}
function mergeSetsFromMaps(mapA, mapB) {
const result = new Map();
// Add all keys from mapA
for (const [key, setA] of mapA) {
const setB = mapB.get(key);
const merged = new Set(setA); // copy setA
if (setB) {
for (const item of setB) {
merged.add(item);
}
}
result.set(key, merged);
}
// Add keys that exist only in mapB
for (const [key, setB] of mapB) {
if (!mapA.has(key)) {
result.set(key, new Set(setB)); // copy setB
}
}
return result;
}
function deepMergeMapsInPlace(mapA, mapB) {
for (const [key, valueB] of mapB) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const valueA = mapA.get(key);
if (valueA instanceof Map && valueB instanceof Map) {
// Recurse for nested maps
deepMergeMapsInPlace(valueA, valueB);
}
else if (valueA instanceof Set && valueB instanceof Set) {
// Merge sets
valueB.forEach((item) => valueA.add(item));
}
else {
// Otherwise overwrite or take from mapB
mapA.set(key, valueB);
}
}
return mapA;
}
function cloneNestedMap(map) {
const result = new Map();
for (const [key, value] of map.entries()) {
if (value instanceof Map) {
result.set(key, cloneNestedMap(value));
}
else {
result.set(key, value);
}
}
return result;
}
const useComponentInstance = (_) => {
const idMap = new Map();
const moduleMap = new Map();
function getDeclarationId(id) {
return idMap.get(id) || null;
}
function getUpstreamById(id) {
const result = moduleMap.get(id);
if (!result)
moduleMap.set(id, new Map());
return moduleMap.get(id);
}
function register(id, declarationId, upstreamModules) {
idMap.set(id, declarationId);
const current = moduleMap.get(id);
const newValue = current
? deepMergeMapsInPlace(current, upstreamModules)
: upstreamModules;
moduleMap.set(id, newValue);
}
function dispose(id) {
idMap.delete(id);
moduleMap.delete(id);
}
return {
getUpstreamById,
getDeclarationId,
register,
dispose,
};
};
// export const createSingletonHook = <T, A extends [ScopeId, ...unknown[]]>(
// create: (...args: A) => T
// ) => {
// const referenceMap = new Map<ScopeId, T>();
// return (...args: A | [ScopeId]) => {
// const scopeId = args[0];
// // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
// if (!referenceMap.has(scopeId) && scopeId !== null) {
// referenceMap.set(scopeId, create(...(args as A)));
// }
// return referenceMap.get(scopeId)!;
// };
// };
function getRegistryMap(factoryKey) {
const g = globalThis;
if (!g.__singletonStores)
g.__singletonStores = {};
if (!g.__singletonStores[factoryKey])
g.__singletonStores[factoryKey] = new Map();
return g.__singletonStores[factoryKey];
}
const createSingletonHook = (create) => {
const derivedKey = `factory:${create.name || 'anon'}`; // not perfectly stable
const registry = getRegistryMap(derivedKey);
const hook = (...args) => {
const scopeId = args[0];
if (scopeId && !registry.has(scopeId))
registry.set(scopeId, create(...args));
return registry.get(scopeId);
};
hook.getByKey = (scopeId) => registry.get(scopeId);
// hook.disposeByKey = (scopeId: string) => {
// const inst = registry.get(scopeId);
// if (inst && typeof (inst as any).dispose === 'function') (inst as any).dispose();
// registry.delete(scopeId);
// };
return hook;
};
/**
* Creates a new TreeMapNode.
* @param {string} id - The unique identifier for the node.
* @param {TreeMapMeta} [meta={}] - Optional metadata for the node.
* @returns {TreeMapNode} The created TreeMapNode.
*/
function createTreeMapNode(id, meta = {}) {
return {
id,
meta,
};
}
/**
* Creates a TreeMapStore for managing TreeMap nodes and subscriptions.
* @returns {TreeMapStore} The created TreeMapStore instance.
*/
function createTreeMap(_) {
const nodeMap = new Map();
const childToParent = new Map();
// TODO: think about whether we really want to track parent to child relationships, because it makes more sense to subscribe be register id under RuntimeModule per register id. this way we can easily notify subscribers based on registered register ids if a runtime reference changes (this can happen through props change when they are passed into configure function in withRuntime).
// we also have to think about what are other sources that could require updating memoized values from runtime api, because primarily it's where anything that comes out of runtimes is consumed. At least, we have to take into account that a provider function can use any value from other injected runtimes. the current approach of mapping over instances makes sense, although it would have to be scoped per provider (i think it already does). the other source would be the props injected into the hoc. we might be able to track property access and then use each of these values as dependencies. another nice thing is that instances is populated with the actual instance, so if we use the reference for the deps, we automatically have our memoized values updated and what is even better is that it reads as it populates, so only the modules that were injected before the one for which we create the runtime api is used as a dependency, which is correct since the later injected ones cannot be used.
const parentToChildren = new Map();
const listeners = new Map();
function register(id, parentId) {
if (!nodeMap.has(id)) {
nodeMap.set(id, createTreeMapNode(id));
}
if (!childToParent.has(id)) {
childToParent.set(id, parentId);
}
if (!parentToChildren.has(parentId)) {
parentToChildren.set(parentId, []);
}
parentToChildren.get(parentId).push(id);
}
function update(id, parentId) {
const currentParentId = childToParent.get(id);
if (currentParentId) {
childToParent.set(id, parentId);
parentToChildren
.get(currentParentId)
?.splice(parentToChildren.get(currentParentId).indexOf(id), 1);
}
childToParent.set(id, parentId);
if (!parentToChildren.has(parentId)) {
parentToChildren.set(parentId, []);
}
parentToChildren.get(parentId).push(id);
}
function dispose(id) {
// Remove from childToParent and nodeMap
const parentId = childToParent.get(id);
childToParent.delete(id);
nodeMap.delete(id);
// Remove from parent's set of children
if (parentId && parentToChildren.has(parentId)) {
const children = parentToChildren.get(parentId);
parentToChildren.set(parentId, children.filter((child) => child !== id));
}
// Remove listener
listeners.delete(id);
}
// Every components cleans up after itself, based on some dispose timeout. There is also a case where we directly unregister (this happens when the id has changed, we need to handle this specifically)
// The idea is that we unregister without notification, because subtrees will be notified after the parent rerenders with a new id, or unmounted (which would clean the subtree anyway, so no need to notify).
function unregister(id) {
dispose(id);
}
function getParent(id) {
return childToParent.get(id) ?? null;
}
function getChildren(id) {
return parentToChildren.get(id) ?? [];
}
function isRoot(id) {
return getParent(id) === '__ROOT__';
}
function getRoot() {
return getChildren('__ROOT__')[0] ?? null;
}
return {
// subscribe,
// getSnapshot,
update,
register,
isRoot,
getRoot,
unregister,
getParent,
// emitById,
};
}
/**
* Singleton hook to get the TreeMapStore instance.
* @returns {TreeMapStore} The TreeMapStore instance.
*/
const useTreeMapInstance = createSingletonHook(createTreeMap);
const useTreeMap = (scopeId, id, parentId) => {
const instance = useTreeMapInstance(scopeId);
// TODO: think about moving register logic into tree map instance
if (id && parentId)
useTreeMapBinding(id, parentId, instance);
return instance;
};
const getTreeMap = (scopeId) => {
return useTreeMapInstance(scopeId);
};
const useTreeMapBinding = (id, parentId, treeMap) => {
const parentNode = treeMap.getParent(id);
// one time register on mount, at the root (first registration), parentId is __ROOT__,
// afterwards, we pull parentId from context
const register = React__namespace.useCallback(() => {
if (parentId && parentNode === null) {
treeMap.register(id, parentId);
}
}, [id, parentId, parentNode, treeMap]);
// register synchronously (registration happens only once, even with multiple calls)
register();
React__namespace.useEffect(() => {
register();
// register again on remount or if parentId changes
return () => treeMap.unregister(id);
}, [id, parentId]);
return register;
};
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
function disableAsyncGlobals() {
var _a;
const g = globalThis;
// Avoid double patching
if (g.__dryAsyncPatched) {
return () => { };
}
g.__dryAsyncPatched = true;
const originals = {
setTimeout: g.setTimeout,
clearTimeout: g.clearTimeout,
setInterval: g.setInterval,
clearInterval: g.clearInterval,
requestAnimationFrame: g.requestAnimationFrame,
cancelAnimationFrame: g.cancelAnimationFrame,
requestIdleCallback: g.requestIdleCallback,
cancelIdleCallback: g.cancelIdleCallback,
fetch: g.fetch,
WebSocket: g.WebSocket,
sendBeacon: g.navigator?.sendBeacon,
};
// Timers → no-op ids
const fakeId = (() => {
let n = 1;
return () => n++;
})();
// --- timers/raf/idle ---
if (typeof g.setTimeout === 'function')
g.setTimeout = (() => fakeId());
if (typeof g.clearTimeout === 'function')
g.clearTimeout = (() => { });
if (typeof g.setInterval === 'function')
g.setInterval = (() => fakeId());
if (typeof g.clearInterval === 'function')
g.clearInterval = (() => { });
if (typeof g.requestAnimationFrame === 'function')
g.requestAnimationFrame = (() => fakeId());
if (typeof g.cancelAnimationFrame === 'function')
g.cancelAnimationFrame = (() => { });
if (typeof g.requestIdleCallback === 'function')
g.requestIdleCallback = (() => fakeId());
if (typeof g.cancelIdleCallback === 'function')
g.cancelIdleCallback = (() => { });
// --- fetch ---
// Return a harmless 204 Response; respect AbortSignal by rejecting with AbortError.
if (typeof g.fetch === 'function') {
g.fetch = (input, init) => {
const signal = init?.signal ??
(typeof input === 'object' && input?.signal ? input.signal : undefined);
if (signal?.aborted) {
// Best-effort AbortError
const AbortErr = g.DOMException ?? Error;
return Promise.reject(new AbortErr('Aborted', 'AbortError'));
}
const body = '';
const status = 204;
const resp = typeof g.Response === 'function'
? new g.Response(body, { status, statusText: 'No Content' })
: // Minimal shim if Response is missing (older Node environments)
{
ok: true,
status,
statusText: 'No Content',
headers: new Map(),
url: '',
clone() {
return this;
},
async text() {
return body;
},
async json() {
return undefined;
},
async arrayBuffer() {
return new ArrayBuffer(0);
},
async blob() {
return new Blob([]);
},
};
return Promise.resolve(resp);
};
}
// --- WebSocket (closed inert stub) ---
if (typeof originals.WebSocket === 'function') {
// Create a guaranteed EventTarget-ish base
const BaseEventTarget = typeof g.EventTarget === 'function'
? g.EventTarget
: class {
addEventListener(_type, _listener, _opts) { }
removeEventListener(_type, _listener, _opts) { }
dispatchEvent(_evt) {
return true;
}
};
class WSStub extends BaseEventTarget {
constructor(url) {
super();
this.CONNECTING = WSStub.CONNECTING;
this.OPEN = WSStub.OPEN;
this.CLOSING = WSStub.CLOSING;
this.CLOSED = WSStub.CLOSED;
this.readyState = WSStub.CLOSED;
this.protocol = '';
this.binaryType = 'blob';
this.onopen = null;
this.onmessage = null;
this.onerror = null;
this.onclose = null;
this[_a] = 'WebSocket';
this.url = url;
}
send(_data) { }
close(_code, _reason) { }
// If BaseEventTarget is the fallback, these are already defined;
// overriding here is harmless and keeps types happy.
addEventListener(type, listener, opts) {
super.addEventListener(type, listener, opts);
}
removeEventListener(type, listener, opts) {
super.removeEventListener(type, listener, opts);
}
dispatchEvent(evt) {
return super.dispatchEvent(evt);
}
get bufferedAmount() {
return 0;
}
get extensions() {
return '';
}
}
_a = Symbol.toStringTag;
WSStub.CONNECTING = 0;
WSStub.OPEN = 1;
WSStub.CLOSING = 2;
WSStub.CLOSED = 3;
g.WebSocket = WSStub;
}
// --- navigator.sendBeacon ---
if (g.navigator && typeof originals.sendBeacon === 'function') {
g.navigator.sendBeacon = () => true;
}
// --- restore ---
return function restore() {
// Only restore what existed; avoid assigning undefined over real APIs.
if (typeof originals.setTimeout === 'function')
g.setTimeout = originals.setTimeout;
if (typeof originals.clearTimeout === 'function')
g.clearTimeout = originals.clearTimeout;
if (typeof originals.setInterval === 'function')
g.setInterval = originals.setInterval;
if (typeof originals.clearInterval === 'function')
g.clearInterval = originals.clearInterval;
if (typeof originals.requestAnimationFrame === 'function')
g.requestAnimationFrame = originals.requestAnimationFrame;
if (typeof originals.cancelAnimationFrame === 'function')
g.cancelAnimationFrame = originals.cancelAnimationFrame;
if (typeof originals.requestIdleCallback === 'function')
g.requestIdleCallback = originals.requestIdleCallback;
if (typeof originals.cancelIdleCallback === 'function')
g.cancelIdleCallback = originals.cancelIdleCallback;
if (typeof originals.fetch === 'function')
g.fetch = originals.fetch;
if (typeof originals.WebSocket === 'function')
g.WebSocket = originals.WebSocket;
if (g.navigator && typeof originals.sendBeacon === 'function')
g.navigator.sendBeacon = originals.sendBeacon;
delete g.__dryAsyncPatched;
};
}
/**
* Create a DOM container for a dry run.
* @param {Object} [opts]
* @param {'hidden'|'measurable'} [opts.mode='hidden']
* @param {HTMLElement} [opts.parent=document.body]
* @param {string} [opts.id='dry-root']
*/
function createHiddenDomRoot(opts = {}) {
if (typeof document === 'undefined') {
throw new Error('createHiddenDomRoot must run in the browser');
}
const { mode = 'hidden', parent = document.body, id = 'dry-root' } = opts;
const el = document.createElement('div');
el.id = id;
el.setAttribute('aria-hidden', 'true');
el.setAttribute('role', 'presentation');
el.setAttribute('data-dry-root', '');
el.tabIndex = -1;
try {
el.inert = true;
}
catch {
/* empty */
}
if (mode === 'hidden') {
el.hidden = true;
el.style.contain = 'layout paint style';
el.style.contentVisibility = 'hidden';
}
else {
const s = el.style;
s.position = 'fixed';
s.width = '0';
s.height = '0';
s.overflow = 'hidden';
s.opacity = '0';
s.visibility = 'hidden';
s.pointerEvents = 'none';
s.userSelect = 'none';
s.contain = 'layout paint style';
s.isolation = 'isolate';
}
parent.appendChild(el);
return {
container: el,
destroy() {
try {
document.body.removeChild(el);
}
catch {
/* empty */
}
},
};
}
function tryFnSync(cb) {
try {
return cb();
}
catch (err) {
return new Error(String(err));
}
}
const DryRunContext = React__namespace.createContext(null);
const useDryRunContext = () => {
const context = React__namespace.useContext(DryRunContext);
return context;
};
const createDryRunCandidateDto = (self, firstDescendent, depth, ancestors) => ({
id: uuid.v4(),
depth,
self,
firstDescendent,
ancestors,
});
const validateCandidates = moize((candidates) => {
if (candidates.length > 1) {
console.warn(`\x1b[38;5;208m[Warning]\x1b[0m Multiple dry run candidates found. Automatically picking one with props: ${candidates[0]
? `\x1b[36m${JSON.stringify(candidates[0].self.props)}\x1b[0m`
: ''}. \x1b[2mPlease ensure that the provided id prop matches with one of the component instances in the tree, or that the component is not rendered multiple times in the same scope with the same id or props.\x1b[0m`);
}
if (candidates.length === 0) {
console.info(`\x1b[38;5;39m[Notice]\x1b[0m \x1b[2mNo structural matches found. Resolving as a direct descendent of the provider scope root. To silence this message, use:\x1b[0m withProviderScope(Root, { structural: false })`);
}
},
// TODO: think about how we want trigger logs only once. It seems that because we use singleton hooks with multiple scopes in multiple places, we get logs multiple times.
{ isDeepEqual: true });
const createDryRunTracker = (_, treeMap) => {
const candidateMap = new Map();
// even though we could store everything in component instance api, and pull from there, we don't want to track everything by default in the live tree, since we write at every render.
const ancestorMap = new Map();
//* since dry run will only ever run before react runs, the results from this function are always deterministic and can be cached. Also this function always gets called with the same argument, so this doesn't lead to extra memory consumption. Note that we also prevent validation from running again which makes sense to do, as duplicate logs don't.
const getAuthorativeCandidate = moize((self) => {
// we reference ancestorIds from candidateMap to avoid duplication of data.
const values = Array.from(candidateMap.values()).map((dto) => Object.assign({}, dto, {
ancestors: dto.ancestors.map((id) => ancestorMap.get(id)),
}));
let result = [];
let isStructuralMatch = true;
if (!self.props.id) {
// pick the first candidate, and collect all candidates with the same id prop
const authorative = values[0];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
result = (authorative ? [authorative] : []).concat(values.filter((candidate) => candidate.self.props.id === authorative.self.props.id &&
candidate !== authorative));
}
else {
result = values.filter((candidate) => candidate.self.componentId === self.componentId && self.props.id
? candidate.self.props.id === self.props.id
: true);
}
// show warning if result > 1
validateCandidates(result);
if (result.length === 0) {
// TODO: last item in ancestor map is always the root, but we might want to use a better way to target it directly from the map.
const rootAncestor = Array.from(ancestorMap.values())[ancestorMap.size - 1];
// go for a fallback candidate and use the root as the single ancestor
// const candidateId = uuid();
const targetAncestor = {
declId: self.declarationId,
id: self.registerId,
props: self.props,
upstreamModules: new Map(),
localProviders: [],
};
const candidate = {
id: self.registerId,
depth: 1,
self: self,
firstDescendent: null,
ancestors: [targetAncestor, rootAncestor],
};
result.push(candidate);
isStructuralMatch = false;
}
return [result[0], isStructuralMatch];
});
function registerAncestor(ancestor) {
if (!ancestorMap.has(ancestor.id)) {
ancestorMap.set(ancestor.id, ancestor);
}
}
function registerCandidate(self, depth, firstDescendent) {
// TODO: consider what is the right place to obtain the ancestors, or rename resolveAncestors to resolveAncestorIds.
const ancestorIds = resolveAncestorIds(self.registerId);
const candidate = createDryRunCandidateDto(self, firstDescendent, depth, ancestorIds);
candidateMap.set(candidate.id, candidate);
// return candidate;
}
// this is used by DryRunTracker to create candidates
function resolveAncestorIds(id) {
const ancestors = [];
let currentId = id;
while (currentId && currentId !== '__ROOT__') {
ancestors.push(currentId);
currentId = treeMap.getParent(currentId);
}
return ancestors;
}
function unregisterCandidate(id) {
candidateMap.delete(id);
}
return {
getAuthorativeCandidate,
registerAncestor,
registerCandidate,
unregisterCandidate,
};
};
const useDryRunTrackerInstance = createSingletonHook(createDryRunTracker);
const useDryRunTracker = (scopeId, treeMap) => {
return useDryRunTrackerInstance(scopeId, treeMap);
};
// getter for external access
const getDryRunTracker = useDryRunTracker;
const createDryRunContextObject = (scopeId, targetId) => {
return {
scopeId: scopeId,
targetId: targetId,
};
};
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable eslint-comments/disable-enable-pair */
const createDryRunApi = (tracker) => {
let rootEdge = null;
function getRootAncestors() {
if (!rootEdge)
throw new Error('No root set!');
const [candidate] = tracker.getAuthorativeCandidate(rootEdge);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return candidate ? candidate.ancestors : [];
}
function getOffTreeData() {
// return a single result after validation
// this is used by provider tree to continue the traversal.
// We might need to inject the provider tree from the dry run scope into here, so we can pull the provider entries out that belong to the declaration ids from the candidate chains
if (!rootEdge)
throw new Error('No root set!');
const [candidate, isStructuralMatch] = tracker.getAuthorativeCandidate(rootEdge);
// const result = extractProviders(candidate, rootModules);
const result = candidate.ancestors.reduce((map, ancestor) => map.set(ancestor.id, ancestor), new Map());
return [result, candidate, isStructuralMatch];
}
// we should try to filter based on the props of the live root, when we call promoteByRoot. After that, we might be able to compare each candidate its ancestors, without risking ambiguity from a large candidate set.
// If no props are provided and there is no direct match, we can show a warning in terms of automocking props using the canonical candidate
// We can throw an error, if a missing props match results in ambiguous candidate ancestors among available candidates, specifically in terms of modules. The difference between comparing with results of resolving providers here is that we are more likely to hit ambiguity when we compare the ancestors directly. Instead we might want to go for a hybrid method, that only compares the modules based on the decl ids that were visited during traversal.
// this is called during render of the portable root, to filter out candidates that do not match the current registerId and childrenSketch
function promoteByRoot(self) {
rootEdge = self;
}
//* portable root or target
function getRoot() {
if (!rootEdge)
throw new Error('No root set!');
const [candidate, isStructuralMatch] = tracker.getAuthorativeCandidate(rootEdge);
return candidate.ancestors.find(({ id }) => id === candidate.self.registerId);
}
return {
getRoot,
getRootAncestors,
getOffTreeData,
promoteByRoot,
};
};
//
function createReactDryRunRoot(opts) {
const { container, destroy } = createHiddenDomRoot(opts);
const root = ReactDOM.createRoot(container);
return {
render: root.render.bind(root),
unmount() {
try {
root.unmount();
}
finally {
destroy();
}
},
};
}
function createDryRunFactory(scopeId, Component, props, declarationId) {
const contextObject = createDryRunContextObject(scopeId, declarationId);
const root = createReactDryRunRoot({ mode: 'hidden' });
// const reliability = computeEdgeReliability(edge);
const restoreAsync = disableAsyncGlobals();
// get results from dryRunTracker and return them
// think about when we do the filtering, we might want to return a module from useDryRun so we can call one of the methods during render to filter using the registerId and childrenSketch, and then validate the provider traversal results against eachother. we also have to think where we do this validation. we can return a module from dryRunTracker that has a method to validate the results, since we also have to do this with late subtree mounts. we also need a place to store the results, or at least give access to the provider map, so the question is do we run this separate from the provider map, or do we merge once into the existing provider map. If we have to late reconstruct, i think it makes more sense to have let the provider map read drom dryRunTracker, this way we can let the dry run interact solely with the dry run tracker, and at render let everything interact with the tracker, but this feels still like it leaks
const act = process.env.NODE_ENV === 'test' ? React__namespace.act : (fn) => fn();
act(() => {
reactDom.flushSync(() => {
root.render(React__namespace.createElement(DryRunContext.Provider, { value: contextObject }, React__namespace.createElement(Component, props)));
});
tryFnSync(() => {
root.unmount();
restoreAsync();
});
});
const treeMap = getTreeMap(scopeId);
const tracker = getDryRunTracker(scopeId, treeMap);
return createDryRunApi(tracker);
}
// technically it's a hook, but it cannot be used inside react, because it calls root.render, but we reuse createSingletonHook to obtain the api instance in the live tree. in other files we inverse it
const useDryRunInstance = createSingletonHook(createDryRunFactory);
const useDryRun = () => {
return {
getInstance: (scopeId) => {
// TODO: think about using an alternative to createSingletonHook, because we need access to the instance in non-react code too and the api is a bit misleading.
const instance = useDryRunInstance(scopeId);
if (!scopeId) {
console.warn('useDryRun called with null scopeId', instance);
}
return instance;
},
};
};
// this is called outside of react
const createDryRun = useDryRunInstance;
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable @typescript-eslint/no-explicit-any */
const createProviderMap = () => {
const map = new Map();
function register(id, entries) {
if (!map.has(id)) {
map.set(id, new Map());
}
const entryMap = map.get(id);
entries.forEach((entry) => {
if (!entryMap.has(entry.id)) {
entryMap.set(entry.id, entry);
}
});
}
// this only needs to happen on fast refresh
function unregister(id) {
if (map.has(id)) {
map.delete(id);
}
}
function getModulesById(id) {
const entryMap = map.get(id);
return Array.from(entryMap.values()).reduce((set, entry) => (entry.type === 'runtime' ? set.add(entry.module) : set), new Set());
}
return {
register,
unregister,
getModulesById,
};
};
const useProviderMapInstance = createSingletonHook(createProviderMap);
const useProviderMap = (scopeId) => {
return useProviderMapInstance(scopeId);
};
/* eslint-disable eslint-comments/disable-enable-pair */
// unique per library instance
const ROOT_NS = uuid.v4();
// one library-wide namespace (stable constant)
const LIB_NS = uuid.v5(`react-runtime@idns:${ROOT_NS}`, uuid.v5.DNS);
// isolated base for cumulative sigs
const CUM_SIG_NS = uuid.v5('CUM_SIG_ID', LIB_NS);
function createGhostId(regId) {
return uuid.v5(`ghost:${regId}`, LIB_NS);
}
function createIdFactory(declId) {
const DECL_NS = uuid.v5(declId, LIB_NS);
function baseId(instId) {
const id = uuid.v5(instId, DECL_NS);
return id;
}
function withTrail(cumSig, instId, salt) {
if (salt === null)
return uuid.v5(`${cumSig}|${declId}#${instId}`, DECL_NS);
return uuid.v5(`${cumSig}|${declId}#${instId}@${String(salt)}`, DECL_NS);
}
function createId(instId, salt) {
const base = baseId(instId);
return withTrail(base, instId, salt);
}
return Object.assign(createId, { baseId, withTrail });
}
// cumulative (namespace-chain): parent cum UUID -> child UUID
function combineV5(parentCum, decl, inst) {
const msg = `${decl}#${inst}`;
return uuid.v5(msg, parentCum); // parentCum acts as namespace
}
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable @typescript-eslint/no-explicit-any */
// TODO: make this part of useDryRun, because it only resolves off-tree data, and avoids coupling between hook modules.
const useProviderTree = (scopeId, treeMap, componentInstanceApi) => {
const providerMap = useProviderMap(scopeId);
function getRoot(dryRunInstance) {
const root = dryRunInstance.getRoot();
return Object.assign({}, root, { id: createGhostId(root.id) });
}
// TODO: getRootAncestors now returns the same as getOffTreeData, albeit in a map. we should clean this up.
function getRootAncestors(dryRunInstance) {
const root = dryRunInstance.getRoot();
const allAncestors = dryRunInstance.getRootAncestors();
const rootAncestors = allAncestors.slice(allAncestors.findIndex((a) => a.id === root.id) + 1);
return rootAncestors.reduce((map, entry) => {
const id = createGhostId(entry.id);
return map.set(id, { ...entry, id });
}, new Map());
}
// TODO: the problem with the current implementation is that it offloads traversal to the dry run api, but it works on static data, while we need to take into account that the upstreamModules can change and have to be read from componentInstanceApi. So we have to do the traversal in provider tree, but use the same logic as in dry run api
function resolveProviderData(id, dryRunInstance, initialModules) {
const unresolvedModules = collectUnresolved(id, 0, initialModules);
// this is now all the nodes
const [offTreeData, candidate, isStructuralMatch] = dryRunInstance.getOffTreeData(unresolvedModules);
const ancestors = offTreeData.values().reduce((array, ancestor) => {
const id = createGhostId(ancestor.id);
array.push(Object.assign({}, ancestor, {
upstreamModules: componentInstanceApi.getUpstreamById(id),
id,
}));
return array;
}, []);
// TODO: consider that if we don't have a structural match, that we also do not have any props to mock, which means, that all expected props must be provided by the user from the original component.
const result = isStructuralMatch
? tryFnSync(() => extractProviders(ancestors, unresolvedModules))
: extractProviders(ancestors, unresolvedModules);
if (result instanceof Error) {
throw new Error("Couldn't resolve upstream dependencies as a direct descendent of the root. If the provided root does render your component through children, you can provide children to the root, via the second argument in of withProviderScope");
}
const rootData = result.get(candidate.self.registerId);
return [result, rootData];
}
function collectUnresolved(currentId, level = 0, initialModules, unresolved = new Set(), seen = new Set()) {
if (seen.has(currentId))
return unresolved;
seen.add(currentId);
const upstreamModules = level === 0 && initialModules
? initialModules
: new Set(
// we rely on the registered upstreamModules, to traverse upwards until we reach __ROOT__. Even if modules can be resolved off-tree we treat them as unresolved, because we want a deterministic way to retrieve off-tree nodes.
// but this also makes me think, that we might not want to rely on the dry run results, itself for traversal, because off-tree nodes their modules can also change and therefore it would make more sense to also read from componentInstanceApi when we go off-tree during traversal.
(componentInstanceApi.getUpstreamById(currentId) ??
new Map())
.values()
.flatMap(effect.identity));
if (upstreamModules.size === 0)
return unresolved;
// Collect upstream maps and merge them in
for (const module of upstreamModules.values()) {
const parentId = lookAhead(currentId, module);
if (!parentId) {
throw new Error('No provider available for upstream module');
}
if (parentId === '__ROOT__') {
unresolved.add(module);
}
else {
collectUnresolved(parentId, level + 1, initialModules, unresolved, seen);
}
}
return unresolved;
}
function lookAhead(currentId, module) {
const parentId = treeMap.getParent(currentId);
if (parentId === '__ROOT__')
return parentId;
const parentDeclarationId = componentInstanceApi.getDeclarationId(parentId);
// get runtime modules
const modules = providerMap.getModulesById(parentDeclarationId);
if (modules.size === 0)
return null;
return modules.has(module)
? parentId
: lookAhead(parentId, module);
}
// root entries are collected upstream entries that reached __ROOT__ by walking up the tree map.
function extractProviders(ancestors, rootModules) {
// for every entry, we want to validate separately, because every entry needs it's own traversal. however, we can use a set for root entries instead of an array, because IF withupstream reaches __ROOT__ we know for any duplicates that they will share the first ancestor that provides it.
const modules = Array.from(rootModules.values());
const traversals = modules.map((module) => {
return resolveProvidersFromCandidate(ancestors, module);
});
// we merge the maps from different resolved start entries, by following the order of the ancestors, to build the map. We rely on insertion order later on to process.
const merged = ancestors.reduce((map, { id }) => {
const found = Array.from(traversals.values()).find((result) => result.has(id));
return found ? map.set(id, found.get(id)) : map;
}, new Map());
return merged;
}
function resolveProvidersFromCandidate(ancestors, module) {
// obtain index of the candidate self in the ancestors array, as sometimes we start at the first descendent.
const startIdx = Array.from(ancestors.values()).findIndex((a) => a.localProviders.some((p) => p.type === 'runtime' && p.module === module));
const seen = new Set();
function traverse(index) {
const ancestor = ancestors[index];
if (seen.has(ancestor.id))
return new Map();
seen.add(ancestor.id);
const upstreamModules = ancestor.upstreamModules;
if (upstreamModules.size === 0)
return new Map();
// Start with current level
const currentLevel = new Map();
currentLevel.set(ancestor.id, ancestor);
// Merge in all upstream maps
const combinedModules = new Set(...upstreamModules.values());
for (const upstreamModule of combinedModules) {
const upstreamIdx = lookAheadInAncestors(ancestors, index, upstreamModule);
if (upstreamIdx === null)
// TODO: add identifier to runtime module/context
throw new Error(`No provider available for ${String(upstreamModule.key)}`);
const upstreamMap = traverse(upstreamIdx);
for (const [k, v] of upstreamMap) {
if (!currentLevel.has(k)) {
currentLevel.set(k, v);
}
}
}
return currentLevel;
}
const result = traverse(startIdx);
return result;
}
// Helper to look upward in the ancestors array for a module
function lookAheadInAncestors(ancestors, startIdx, module) {
for (let i = startIdx + 1; i < ancestors.length; i++) {
// this returns the intantiated modules on the declId, but this is not working, as we cannot use declid anymore, in fact we need to bring this over from the ancestor data.
// TODO: consider if we want to do this on the fly or that we store a set on the ancestor data. Seems to be better to put this on the ancestor data
const modules = ancestors[i].localProviders.reduce((p, entry) => (entry.type === 'runtime' ? p.add(entry.module) : p), new Set());
if (modules.has(module))
return i;
}
return null;
}
return {
register: providerMap.register,
resolveProviderData,
getRoot,
getRootAncestors,
};
};
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable @typescript-eslint/no-explicit-any */
const link = effect.pipe;
const isRuntimeContext = (input) => {
return (typeof input === 'object' &&
input !== null &&
'key' in input &&
'layer' in input &&
typeof input.key === 'symbol' &&
typeof input.layer === 'object' &&
effect.Layer.isLayer(input.layer));
};
const isRuntimeConfig = (input) => {
return (typeof input === 'object' &&
input !== null &&
'debug' in input &&
'postUnmountTTL' in input &&
'env' in input &&
'replace' in input &&
'cleanupPolicy' in input &&
typeof input.debug === 'boolean' &&
typeof input.postUnmountTTL === 'number' &&
['prod', 'dev'].includes(input.env) &&
typeof input.replace === 'boolean' &&
['onUnmount', 'immediate'].includes(input.cleanupPolicy));
};
const isRuntimeInstance = (input) => {
return (typeof input === 'object' &&
input !== null &&
'runtime' in input &&
effect.ManagedRuntime.isManagedRuntime(input.runtime) &&
'config' in input &&
typeof input.config === 'object' &&
isRuntimeConfig(input.config));
};
const createRuntimeContext = (options) => (layer) => {
const context = {
key: Symbol('RuntimeContext'),
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
name: options?.name ?? 'runtime_name',
layer,
};
return context;
};
const getProviders = (callback) => {
const map = new Map();
const providers = callback((runtime) => ({
provide: (service) => {
map.set(service.key, runtime);
return service;
},
}));
const from = {};
for (const service of providers) {
const segments = stripAt(service.key).split('/');
let currentNode = from;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
if (i === segments.length - 1) {
currentNode[segment] = service;
}
else {
if (!currentNode[segment])
currentNode[segment] = {};
currentNode = currentNode[segment];
}
}
}
return { map, from };
};
function stripAt(s) {
return s.startsWith('@') ? s.slice(1) : s;
}
const createProxy = (value) => new Proxy(Object.assign({}, value), (() => {
const map = new Map();
return {
get: (target, prop) => {
if (prop === 'subscribe') {
return (key, fn) => {
if (!map.has(key))
map.set(key, new Set());
map.get(key).add(fn);
return () => {
map.get(key).delete(fn);
if (map.get(key).size === 0)
map.delete(key);
};
};
}
if (prop === 'value')
return target;
return Reflect.get(target, prop);
},
set: (target, prop, value) => {
const result = Reflect.set(target, prop, value);
const byProp = map.get(prop);
if (byProp)
byProp.forEach((fn) => fn(value));
return result;
},
};
})());
// assume all properties that are needed, are enumerable on proxyState, if not values later still update refs that start with undefined, but we filter undefined away.
const createProxyStreamMap = (proxyState, onKeyAccess = () => { }) => effect.Effect.gen(function* () {
const deferred = yield* effect.Deferred.make();
yield* effect.Effect.addFinalizer(() => effect.Deferred.succeed(deferred, true));
return new Proxy({}, {
get: moize.shallow((_, prop) => effect.Stream.merge(effect.Stream.fromIterable([proxyState[prop]]).pipe(effect.Stream.tap(() => effect.Effect.sync(() => onKeyAccess(prop)))), effect.Stream.asyncPush((emit) => effect.Effect.acquireRelease(effect.Effect.sync(() => proxyState.subscribe(prop, emit.single)), effect.Effect.fn((unsubscribe) => effect.Effect.sync(unsubscribe)))).pipe(effect.Stream.interruptWhenDeferred(deferred))).pipe(effect.Stream.filter((value) => value !== undefined))),
});
});
const enhanceRuntime = () => (runtime) => runtime;
const PropTagSymbol = Symbol.for('react-runtime/getPropTag/Id');
const createGetPropTag =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(tag) => () => {
const TagClass = tag(PropTagSymbol.toString());
class PropsTag extends TagClass() {
}
return { PropService: PropsTag };
};
const getPropTag = createGetPropTag(effect.Context.Tag);
// export const isStream = <U>(
// u: U
// ): u is Exclude<
// U & (U extends IsStream<U> ? IsStream<U> : never),
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// Effect.Effect<any, any, any>
// > => hasProperty(u, StreamTypeId) && !Effect.isEffect(u);
const isStream = (u) => {
return Predicate.hasProperty(u, Stream.StreamTypeId) && !effect.Effect.isEffect(u);
};
const isStreamEffect = (u) => effect.Effect.sync(() => isStream(u));
const createKey = (key) => key;
const defaultConfig = {
debug: false,
postUnmountTTL: 1000,
env: process.env.NODE_ENV === 'production' ? 'prod' : 'dev',
cleanupPolicy: 'onUnmount', // only used with replace: true
replace: false,
};
function createRuntimeRegistry(_) {
const runtimeMapping = new Map();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const registry = new Map();
const isolatedMapping = new Map();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isolatedRegistry = new Map();
const disposerMap = new Map();
const promotionMap = new Map();
// const listeners: ListenerMap = new Map();
function registerIsolated(id, payload) {
const exists = getById(id, payload.context.key, payload.index);
if (exists)
return exists;
const { context, config, providerId: entryId } = payload;
const runtimeId = entryId;
let runtimeKeyMap = isolatedMapping.get(id);
if (!runtimeKeyMap) {
runtimeKeyMap = new Map();
isolatedMapping.set(id, runtimeKeyMap);
}
if (!runtimeKeyMap.has(context.key)) {
runtimeKeyMap.set(context.key, new Map());
}
const runtimeIdMap = runtimeKeyMap.get(context.key);
runtimeIdMap.set(payload.index, runtimeId);
const { PropService } = getPropTag();
const propProxy = createProxy(payload.props);
const proxymap = createProxyStreamMap(propProxy);
const PropLayer = effect.Layer.scoped(PropService, proxymap);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const instance = {
id: uuid.v4(),
runtime: effect.ManagedRuntime.make(context.layer.pipe(effect.Layer.provide(PropLayer))),
config: Object.assign({}, defaultConfig, config),
propProxy: creat