diff --git a/src/core/state.test.ts b/src/core/state.test.ts index 1c5f60c..ef568c8 100644 --- a/src/core/state.test.ts +++ b/src/core/state.test.ts @@ -131,6 +131,7 @@ describe("Basic Implementation of Stores & Wires", (test) => { }); expect(changes.length).toBe(2); changes.forEach((change) => { + console.log("1", change); applyStoreChange(store2, change); }); const store2Value = reify(store2); diff --git a/src/core/state/store.ts b/src/core/state/store.ts index 5b17072..0d8337d 100644 --- a/src/core/state/store.ts +++ b/src/core/state/store.ts @@ -49,8 +49,13 @@ function createStoreSubscription( set.add(encodedCursor); wire.storesRS.set(manager, set); } - const v = getValueUsingPath(manager.value as any, cursorPath); - return v; + try { + const v = getValueUsingPath(manager.value as any, cursorPath); + return v; + } catch (e) { + console.log(wire, wire.storesRS, encodedCursor, manager.value); + throw e; + } } // Handle changes in the store and trigger associated tasks and wires @@ -61,11 +66,11 @@ export function handleStoreChange( oldValue: any, changeData: ApplyData ) { + // console.log("handleStoreChange", path, newValue, oldValue, changeData); const changePath = path as string[]; + adjustCursorForArrayChange(manager, path as string[], changeData); const wiresToRun = findMatchingWires(manager, changePath, changeData); - runWires(wiresToRun); - triggerStoreTasks(manager, changePath, newValue, changeData); } @@ -124,39 +129,106 @@ function triggerStoreTasks( const isPathMatching = changePath.slice(0, path.length).join("/") === path.join("/"); if (isPathMatching) { - observor({ data: changeData, path: changePath, value: newValue }); + observor({ + data: (changeData + ? { + name: changeData.name, + args: changeData.args, + result: undefined, + } + : undefined) as any, + path: changePath, + value: newValue, + }); } }); } // // Function to adjust cursor paths for array changes -function adjustCursorForArrayChange(cursor: string[], change: any): string[] { - const newCursor = [...cursor]; - const index = parseInt(change.path[change.path.length - 1], 10); - - switch (change.type) { - case "insert": - if (index <= cursor.length) { - newCursor.push(String(index)); - } - break; - case "delete": - if (index < cursor.length) { - newCursor.splice(index, 1); +function adjustCursorForArrayChange( + manager: StoreManager, + changePath: string[], + change: ApplyData +): void { + if (!change || change.name !== "splice") { + return; + } + const args = change.args as [string, string]; + const start = parseInt(args[0]); + const deleteCount = parseInt(args[1]); + + // console.log("adjustCursorForArrayChange", { start, deleteCount }); + + manager.wires.forEach((wire) => { + wire.storesRS.forEach((cursorSet) => { + const { toRemove, toAdd } = adjustCursorsInSet( + cursorSet, + changePath, + start, + deleteCount + ); + + toRemove.forEach((cursor) => cursorSet.delete(cursor)); + toAdd.forEach((cursor) => cursorSet.add(cursor)); + + //console.log({ toRemove, toAdd }, wire.storesRS); + }); + }); + + manager.tasks.forEach((task) => { + if (isPathAffected(task.path, changePath)) { + const listenerIndex = getListenerIndex(task.path, changePath.length); + + if (start + deleteCount <= listenerIndex) { + const newIndex = listenerIndex - deleteCount; + task.path[changePath.length] = newIndex.toString(); + //console.log("Updated task path", encodeCursor(task.path)); } - break; - case "splice": - const { index: spliceIndex, removed, added } = change; - if (spliceIndex <= cursor.length) { - newCursor.splice( - spliceIndex, - removed.length, - ...added.map((_: any, i: any) => String(spliceIndex + i)) - ); + } + }); +} + +function adjustCursorsInSet( + cursorSet: Set, + changePath: string[], + start: number, + deleteCount: number +): { toRemove: string[]; toAdd: string[] } { + const toRemove: string[] = []; + const toAdd: string[] = []; + + cursorSet.forEach((cursorString) => { + const cursor = decodeCursor(cursorString); + if (isPathMatching(cursor, changePath)) { + const listenerIndex = getListenerIndex(cursor, changePath.length); + + if (start + deleteCount <= listenerIndex) { + toRemove.push(cursorString); + cursor[changePath.length] = (listenerIndex - deleteCount).toString(); + toAdd.push(encodeCursor(cursor)); } - break; - } + } + }); + + return { toRemove, toAdd }; +} + +function isPathMatching(cursor: string[], changePath: string[]): boolean { + return ( + encodeCursor(changePath) === + encodeCursor(cursor.slice(0, changePath.length)) + ); +} + +function getListenerIndex(cursor: string[], pathLength: number): number { + return parseInt(cursor[pathLength]); +} - return newCursor; +function isPathAffected(path: string[], changePath: string[]): boolean { + return ( + encodeCursor(changePath) === + encodeCursor(path.slice(0, changePath.length)) && + path.length > changePath.length + ); } diff --git a/src/core/state/types.ts b/src/core/state/types.ts index 5844034..251765a 100644 --- a/src/core/state/types.ts +++ b/src/core/state/types.ts @@ -29,13 +29,9 @@ export type Signal = SignalAPI & { }; export type ExtractElement = - ArrayType extends readonly (infer ElementType)[] - ? ElementType - : ArrayType extends { [key: string]: infer ElementType2 } - ? ElementType2 - : never; + ArrayType extends readonly (infer ElementType)[] ? ElementType : never; -export type ArrayOrObject = Array | Record; +export type ArrayOrObject = Array; export type StoreCursor = T extends ArrayOrObject ? { @@ -90,7 +86,7 @@ export type Wire = { // Signals/Stores read-subscribed last run sigRS: Set; - storesRS: WeakMap>; + storesRS: Map>; // Post-run tasks tasks: Set<(nextValue: T) => void>; diff --git a/src/core/state/wire.ts b/src/core/state/wire.ts index 7867eb3..cfd07ae 100644 --- a/src/core/state/wire.ts +++ b/src/core/state/wire.ts @@ -30,7 +30,7 @@ export const createWire: WireFactory = (arg: WireFunction): Wire => { type: Constants.WIRE, fn: arg, sigRS: new Set(), - storesRS: new WeakMap(), + storesRS: new Map(), tasks: new Set(), state: S_NEEDS_RUN, upper: undefined, @@ -120,7 +120,7 @@ const _initWire = (wire: Wire): void => { wire.lower = new Set(); // Drop all signals now that they have been unlinked wire.sigRS = new Set(); - wire.storesRS = new WeakMap(); + wire.storesRS = new Map(); }; // Pauses a wire so signal writes won't cause runs. Affects nested wires diff --git a/src/dom/api.ts b/src/dom/api.ts index 1790a9d..892e4e9 100644 --- a/src/dom/api.ts +++ b/src/dom/api.ts @@ -35,7 +35,7 @@ export const insertElement = ( el: VElement, after?: string ) => { - console.log("insert", parentPath, el, after); + // console.log("insert", parentPath, el, after); const step = parentPath.reduce((step, key) => { if (!step) return; @@ -176,6 +176,22 @@ export const removeNode = (renderCtx: RenderContext, node: TreeStep) => { const nodes = getDescendants(node); // console.log("removeNode nodes", node, nodes); nodes.forEach((step) => { + if (step.type === DOMConstants.ComponentTreeStep) { + //step.wires.length && console.log("s", step, step.wires.length); + step.wires.forEach((w) => { + w.storesRS.forEach((s, manager) => { + if (manager.wires.has(w)) { + //console.log("removing wire", s, manager); + manager.wires.delete(w); + } + }); + w.sigRS.forEach((sig) => { + sig.wires.delete(w); + }); + w.tasks.clear(); + }); + step.wires = []; + } if (step.dom) { if ( step.type === DOMConstants.ComponentTreeStep && diff --git a/src/stdlib/Each/index.tsx b/src/stdlib/Each/index.tsx index b13d847..03736fe 100644 --- a/src/stdlib/Each/index.tsx +++ b/src/stdlib/Each/index.tsx @@ -20,7 +20,11 @@ import { getValueUsingPath } from "../../utils/index"; export const Each: ( props: { cursor: StoreCursor; - renderItem: (item: ExtractElement, index: number | string) => VElement; + renderItem: ( + item: () => ExtractElement, + index: number | string, + list: T + ) => VElement; }, utils: ComponentUtils ) => VElement = component( @@ -42,128 +46,123 @@ export const Each: ( const $rootWire = wire(($: SubToken) => {}); setContext(ParentWireContext, signal("$wire", $rootWire)); - const cursor = props.cursor; - const store: StoreManager = (cursor as any)[META_FLAG]; - const eachCursorPath: string[] = getCursor(cursor); - // console.log("Each", eachCursorPath); + const listCursor = props.cursor; + const store: StoreManager = (listCursor as any)[META_FLAG]; + const listCursorPath: string[] = getCursor(listCursor); + // console.log("Each", listCursorPath); - const value: any[] = getValueUsingPath( + const listValue: typeof listCursor = getValueUsingPath( store.value as any, - eachCursorPath - ) as any[]; + listCursorPath + ) as typeof listCursor; //console.log("value", value); - const isArray = Array.isArray(value); + const isArray = Array.isArray(listValue); + if (!isArray) throw new Error(" needs array"); + + const getItemCursor = (item: ExtractElement) => { + const index = listValue.indexOf(item); + if (index > -1) { + return props.cursor[index]; + } else { + console.error("accessing no existent item", index, item, listValue); + } + }; const observor = function (change: StoreChange) { const { data, path, value } = change; - //console.log("list change", change, eachCursorPath, path); + //console.log("Each list change", change, listCursorPath, path); const pStep = parentStep.children[0]; const previousChildren = [...(pStep.children || [])]; - if (eachCursorPath.join() === path.join()) { + if (listCursorPath.join() === path.join() && !data) { previousChildren.forEach((node) => { removeNode(renderContext, node); }); //console.log("should reset list"); - if (Array.isArray(value)) { - const startIndex = 0; - value.forEach((item, index) => { - const previousChildren = [...(pStep.children || [])]; - const { treeStep, el } = renderArray( - pStep, - props.renderItem, - cursor, - value, - index - ); - const { registry, root } = reifyTree(renderContext, el, pStep); - const before = previousChildren[startIndex + index] || null; - addNode(renderContext, pStep, root, before); - }); - } else { - Object.keys(value).forEach((key) => { - const el = props.renderItem((cursor as any)[key], key); - const treeStep = getTreeStep(parentStep, undefined, el); - const { registry, root } = reifyTree(renderContext, el, pStep); - addNode(renderContext, pStep, root); - }); - } + const startIndex = 0; + (value as typeof props.cursor).forEach((item, index) => { + const previousChildren = [...(pStep.children || [])]; + const { treeStep, el } = renderArray( + pStep, + props.renderItem, + listCursor, + value, + index, + utils, + getItemCursor + ); + const { registry, root } = reifyTree(renderContext, el, pStep); + const before = previousChildren[startIndex + index] || null; + addNode(renderContext, pStep, root, before); + }); return; } - // console.log(path.slice(0, eachCursorPath.length).join("/")); + // important // filter changes so you don't try to render invalid changes - if (isArray) { - if (path.slice(0, eachCursorPath.length).join("/") !== path.join("/")) - return; - if (data?.name === "push") { - data.args.forEach((arg, i) => { - const index = previousChildren.length + i; - const { treeStep, el } = renderArray( - pStep, - props.renderItem, - cursor, - value, - index - ); - const { registry, root } = reifyTree(renderContext, el, pStep); - addNode(renderContext, pStep, root); - }); - } else if (data?.name === "pop") { - if (previousChildren.length > 0) { - const lastNode = previousChildren[previousChildren.length - 1]; - removeNode(renderContext, lastNode); - } - } else if (data?.name === "splice") { - const [startIndex, deleteCount, ...items] = data.args as [ - number, - number, - ...any - ]; - const nodesToRemove = previousChildren.slice( - startIndex, - startIndex + deleteCount - ); - - // Remove the nodes that are being spliced out - nodesToRemove.forEach((n) => removeNode(renderContext, n)); - // Add the new nodes being spliced in - items.forEach((item, i) => { - const index = startIndex + i; - const previousChildren = [...(pStep.children || [])]; - const { treeStep, el } = renderArray( - pStep, - props.renderItem, - cursor, - value, - index - ); - const { registry, root } = reifyTree(renderContext, el, pStep); - const before = previousChildren[startIndex + i] || null; - addNode(renderContext, pStep, root, before); - }); + if (path.slice(0, listCursorPath.length).join("/") !== path.join("/")) + return; + if (data?.name === "push") { + data.args.forEach((arg, i) => { + const index = previousChildren.length + i; + const { treeStep, el } = renderArray( + pStep, + props.renderItem, + listCursor, + value, + index, + utils, + getItemCursor + ); + const { registry, root } = reifyTree(renderContext, el, pStep); + addNode(renderContext, pStep, root); + }); + } else if (data?.name === "pop") { + if (previousChildren.length > 0) { + const lastNode = previousChildren[previousChildren.length - 1]; + removeNode(renderContext, lastNode); } - } else { - // console.log("path", path, eachCursorPath); - if ( - path.length === eachCursorPath.length + 1 && - path.slice(0, eachCursorPath.length).join("/") == - eachCursorPath.join("/") - ) { - // todo: handle removal - const key = path[path.length - 1]; + } else if (data?.name === "splice") { + const args = data.args as [string, string]; + const startIndex = parseInt(args[0]); + const deleteCount = parseInt(args[1]); + const [_, __, ...items] = data.args as [string, number, ...any]; + const nodesToRemove = previousChildren.slice( + startIndex, + startIndex + deleteCount + ); + + // console.log( + // "Each nodesToRemove", + // previousChildren, + // nodesToRemove, + // data + // ); - const index = previousChildren.length + 1; - const el = props.renderItem((cursor as any)[key], key); - const treeStep = getTreeStep(parentStep, undefined, el); + // Remove the nodes that are being spliced out + nodesToRemove.forEach((n) => removeNode(renderContext, n)); + // Add the new nodes being spliced in + items.forEach((item, i) => { + const index = startIndex + i; + const previousChildren = [...(pStep.children || [])]; + const { treeStep, el } = renderArray( + pStep, + props.renderItem, + listCursor, + value, + index, + utils, + getItemCursor + ); const { registry, root } = reifyTree(renderContext, el, pStep); - addNode(renderContext, pStep, root); - } + const before = previousChildren[startIndex + i] || null; + addNode(renderContext, pStep, root, before); + }); } }; - const task = { path: eachCursorPath, observor }; + const task = { path: listCursorPath, observor }; onMount(() => { store.tasks.add(task); }); @@ -171,26 +170,14 @@ export const Each: ( store.tasks.delete(task); }); - if (isArray) { - // array - return ( - - {value.map((el, index) => - props.renderItem((cursor as any)[index], index) - )} - - ); - } else { - // object - return ( - - {Object.keys(value).map((el, index) => { - // console.log("el", el, index, (cursor as any)[el]); - return props.renderItem((cursor as any)[el], el as any); - })} - - ); - } + return ( + + {listValue.map((el, index) => { + const cursor = getItemCursor.bind(null, el as any) as any; + return props.renderItem(cursor, index, listCursor); + })} + + ); } ); @@ -199,10 +186,12 @@ const renderArray = ( renderItem: Function, cursor: any, list: any[], - index: number | string + index: number, + utils: ComponentUtils, + getItemCursor: Function ) => { // console.log(getCursor(cursor)); - const vEl = renderItem((cursor as any)[index], index); + const vEl = renderItem(getItemCursor.bind(null, list[index]), index); const treeStep = getTreeStep(parentStep, undefined, vEl); return { treeStep, el: vEl }; }; diff --git a/src/utils/cursor.ts b/src/utils/cursor.ts index 9e00303..d26e785 100644 --- a/src/utils/cursor.ts +++ b/src/utils/cursor.ts @@ -37,5 +37,9 @@ export const getCursor = (cursor: StoreCursor) => [ ...(cursor as CursorProxyInternal)[PATH_FLAG], ]; +export const getReactiveCursor = (cursor: StoreCursor) => [ + ...(cursor as CursorProxyInternal)[PATH_FLAG], +]; + export const getCursorProxyMeta = (cursor: CursorProxy) => (cursor as CursorProxyInternal)[META_FLAG] as T;