Skip to content

Commit

Permalink
Merge pull request #73 from Shopify/experiment/prop-as-sub-tree-2
Browse files Browse the repository at this point in the history
Experiment prop as sub tree
  • Loading branch information
henrytao-me authored May 20, 2021
2 parents f8b2cf2 + 0ff6447 commit 39be759
Show file tree
Hide file tree
Showing 14 changed files with 577 additions and 94 deletions.
13 changes: 11 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,28 @@ export {
KIND_ROOT,
KIND_COMPONENT,
KIND_TEXT,
KIND_FRAGMENT,
} from './types';
export type {
RemoteRoot,
RemoteChannel,
RemoteComponent,
RemoteText,
RemoteChild,
RemoteFragment,
Serialized,
ActionArgumentMap,
RemoteTextSerialization,
RemoteComponentSerialization,
RemoteFragmentSerialization,
} from './types';
export {createRemoteRoot} from './root';
export {createRemoteReceiver, createRemoteChannel} from './receiver';
export {
createRemoteReceiver,
createRemoteChannel,
isRemoteFragmentSerialization,
isRemoteReceiverAttachableFragment,
} from './receiver';
export type {
RemoteReceiver,
RemoteReceiverAttachment,
Expand All @@ -39,5 +47,6 @@ export type {
RemoteReceiverAttachableRoot,
RemoteReceiverAttachableComponent,
RemoteReceiverAttachableText,
RemoteReceiverAttachableFragment,
} from './receiver';
export {isRemoteComponent, isRemoteText} from './utilities';
export {isRemoteComponent, isRemoteText, isRemoteFragment} from './utilities';
111 changes: 102 additions & 9 deletions packages/core/src/receiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import {
ACTION_REMOVE_CHILD,
ACTION_UPDATE_PROPS,
ACTION_UPDATE_TEXT,
KIND_COMPONENT,
KIND_FRAGMENT,
} from './types';
import type {
ActionArgumentMap,
RemoteChannel,
RemoteTextSerialization,
RemoteComponentSerialization,
RemoteFragmentSerialization,
} from './types';
import {isRemoteFragment} from './utilities';

export const ROOT_ID = Symbol('RootId');

Expand All @@ -25,6 +29,12 @@ export interface RemoteReceiverAttachableComponent
version: number;
}

export interface RemoteReceiverAttachableFragment
extends Omit<RemoteFragmentSerialization, 'children'> {
children: RemoteReceiverAttachableChild[];
version: number;
}

export interface RemoteReceiverAttachableRoot {
id: typeof ROOT_ID;
children: RemoteReceiverAttachableChild[];
Expand All @@ -37,7 +47,8 @@ export type RemoteReceiverAttachableChild =

export type RemoteReceiverAttachable =
| RemoteReceiverAttachableChild
| RemoteReceiverAttachableRoot;
| RemoteReceiverAttachableRoot
| RemoteReceiverAttachableFragment;

interface RemoteChannelRunner {
mount(...args: ActionArgumentMap[typeof ACTION_MOUNT]): void;
Expand Down Expand Up @@ -112,7 +123,9 @@ export function createRemoteReceiver(): RemoteReceiver {
mount: (children) => {
const root = attachedNodes.get(ROOT_ID) as RemoteReceiverAttachableRoot;

const normalizedChildren = children.map(addVersion);
const normalizedChildren = children.map((child) =>
normalizeNode(child, addVersion),
);

root.version += 1;
root.children = normalizedChildren;
Expand All @@ -130,14 +143,14 @@ export function createRemoteReceiver(): RemoteReceiver {
});
},
insertChild: (id, index, child) => {
const normalizedChild = addVersion(child);
retain(normalizedChild);
attach(normalizedChild);

const attached = attachedNodes.get(
id ?? ROOT_ID,
) as RemoteReceiverAttachableRoot;

const normalizedChild = normalizeNode(child, addVersion);
retain(normalizedChild);
attach(normalizedChild);

const {children} = attached;

if (index === children.length) {
Expand All @@ -154,6 +167,7 @@ export function createRemoteReceiver(): RemoteReceiver {
const attached = attachedNodes.get(
id ?? ROOT_ID,
) as RemoteReceiverAttachableRoot;

const {children} = attached;

const [removed] = children.splice(index, 1);
Expand All @@ -170,10 +184,23 @@ export function createRemoteReceiver(): RemoteReceiver {
const component = attachedNodes.get(
id,
) as RemoteReceiverAttachableComponent;

const oldProps = {...(component.props as any)};

retain(newProps);

Object.keys(newProps).forEach((key) => {
const newProp = (newProps as any)[key];
const oldProp = (oldProps as any)[key];
if (isRemoteReceiverAttachableFragment(oldProp)) {
detach(oldProp);
}
if (isRemoteFragmentSerialization(newProp)) {
const attachableNewProp = addVersion(newProp);
attach(attachableNewProp);
}
});

Object.assign(component.props, newProps);
component.version += 1;

Expand All @@ -186,6 +213,7 @@ export function createRemoteReceiver(): RemoteReceiver {
},
updateText: (id, newText) => {
const text = attachedNodes.get(id) as RemoteReceiverAttachableText;

text.text = newText;
text.version += 1;
enqueueUpdate(text);
Expand Down Expand Up @@ -293,19 +321,41 @@ export function createRemoteReceiver(): RemoteReceiver {
return timeout;
}

function attach(child: RemoteReceiverAttachableChild) {
function attach(
child: RemoteReceiverAttachableChild | RemoteReceiverAttachableFragment,
) {
attachedNodes.set(child.id, child);

if (child.kind === KIND_COMPONENT && 'props' in child) {
const {props = {}} = child as any;
Object.keys(props).forEach((key) => {
const prop = props[key];
if (!isRemoteReceiverAttachableFragment(prop)) return;
attach(prop);
});
}

if ('children' in child) {
for (const grandChild of child.children) {
attach(grandChild);
}
}
}

function detach(child: RemoteReceiverAttachableChild) {
function detach(
child: RemoteReceiverAttachableChild | RemoteReceiverAttachableFragment,
) {
attachedNodes.delete(child.id);

if (child.kind === KIND_COMPONENT && 'props' in child) {
const {props = {}} = child as any;
Object.keys(props).forEach((key) => {
const prop = props[key];
if (!isRemoteReceiverAttachableFragment(prop)) return;
detach(prop);
});
}

if ('children' in child) {
for (const grandChild of child.children) {
detach(grandChild);
Expand All @@ -314,7 +364,50 @@ export function createRemoteReceiver(): RemoteReceiver {
}
}

function addVersion(value: any): RemoteReceiverAttachableChild {
function addVersion<T>(
value: T,
): T extends RemoteTextSerialization
? RemoteReceiverAttachableText
: T extends RemoteComponentSerialization
? RemoteReceiverAttachableChild
: T extends RemoteFragmentSerialization
? RemoteReceiverAttachableFragment
: never {
(value as any).version = 0;
return value as any;
}

function normalizeNode<
T extends
| RemoteTextSerialization
| RemoteComponentSerialization
| RemoteFragmentSerialization,
R
>(node: T, normalizer: (node: T) => R) {
if (node.kind === KIND_FRAGMENT || node.kind === KIND_COMPONENT) {
(node as any).children.forEach((child: T) =>
normalizeNode(child, normalizer),
);
}
if (node.kind === KIND_COMPONENT && 'props' in node) {
const {props} = node as any;
for (const key of Object.keys(props)) {
const prop = props[key];
if (!isRemoteFragmentSerialization(prop)) continue;
props[key] = normalizeNode(prop as any, normalizer);
}
}
return normalizer(node);
}

export function isRemoteFragmentSerialization(
object: unknown,
): object is RemoteFragmentSerialization {
return isRemoteFragment(object) && 'id' in object && 'children' in object;
}

export function isRemoteReceiverAttachableFragment(
object: unknown,
): object is RemoteReceiverAttachableFragment {
return isRemoteFragmentSerialization(object) && 'version' in object;
}
Loading

0 comments on commit 39be759

Please sign in to comment.