Skip to content

Commit

Permalink
Merge branch 'master' of github.com:bitbldr/sprocket
Browse files Browse the repository at this point in the history
  • Loading branch information
eliknebel committed Jan 14, 2025
2 parents 4e06693 + cd79e69 commit 47a1af4
Show file tree
Hide file tree
Showing 14 changed files with 374 additions and 188 deletions.
3 changes: 2 additions & 1 deletion client/src/events.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { On } from "snabbdom";
import ReconnectingWebSocket from "reconnecting-websocket";

export type EventIdentifier = {
kind: string;
Expand All @@ -12,7 +13,7 @@ export type EventHandlerProvider = (

export const initEventHandlerProvider =
(
socket: WebSocket,
socket: ReconnectingWebSocket,
customEventEncoders: Record<string, any> = {}
): EventHandlerProvider =>
(elementTag, events: EventIdentifier[]) =>
Expand Down
200 changes: 136 additions & 64 deletions client/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,97 +1,169 @@
import ReconnectingWebSocket from "reconnecting-websocket";
import { Module } from "snabbdom";

type PushEvent = (event: string, payload: any) => void;

type ElementId = string;
type HookName = string;

export type Hook = {
el: Element;
el: Node;
pushEvent: PushEvent;
handleEvent: (event: string, handler: (payload: any) => any) => void;
[key: string]: any;
};

export interface HookIdentifier {
name: string;
id: string;
}

export type ClientHookProvider = (elementHooks: HookIdentifier[]) => Module;
type Emit = {
id: ElementId;
hook: HookName;
kind: string;
payload: any;
};

export type ClientHookProvider = {
hook: (elementHooks: HookIdentifier[]) => Module;
handle_emit: (emit: Emit) => void;
};

export const initClientHookProvider = (
socket: WebSocket,
socket: ReconnectingWebSocket,
hooks: Record<string, any> = {}
): ClientHookProvider => {
let clientHookMap: Record<string, any> = {};

return (elementHooks: HookIdentifier[]) => ({
create: (emptyVNode, vnode) => {
elementHooks.forEach((h) => {
const { id: hookId, name: hookName } = h;

const pushEvent = (name: string, payload: any) => {
socket.send(
JSON.stringify(["hook:event", { id: hookId, name, payload }])
);
};

const handleEvent = (event: string, handler: (payload: any) => any) => {
socket.addEventListener("message", function (msg) {
let parsed = JSON.parse(msg.data);

if (Array.isArray(parsed)) {
switch (parsed[0]) {
case "hook:event":
if (parsed[1].id === hookId && parsed[1].kind === event) {
handler(parsed[1].payload);
}
break;
}
}
});
};

clientHookMap[hookId] = {
el: vnode.elm,
name: hookName,
pushEvent,
handleEvent,
};

execClientHook(hooks, clientHookMap, hookName, hookId, "create");
});
},
insert: (vnode) => {
elementHooks.forEach((h) => {
const { id: hookId, name: hookName } = h;
execClientHook(hooks, clientHookMap, hookName, hookId, "insert");
});
},
update: (oldVNode, vnode) => {
elementHooks.forEach((h) => {
const { id: hookId, name: hookName } = h;
execClientHook(hooks, clientHookMap, hookName, hookId, "update");
});
},
destroy: (vnode) => {
elementHooks.forEach((h) => {
const { id: hookId, name: hookName } = h;
execClientHook(hooks, clientHookMap, hookName, hookId, "destroy");
let clientHookMap: Record<ElementId, Record<HookName, Hook>> = {};

return {
hook: (elementHooks: HookIdentifier[]) => ({
create: (emptyVNode, vnode) => {
const elementId = vnode.data.elementId;

elementHooks.forEach((h) => {
const { name: hookName } = h;

const pushEvent = (kind: string, payload: any) => {
socket.send(
JSON.stringify([
"hook:event",
{ id: vnode.data.elementId, hook: hookName, kind, payload },
])
);
};

const handleEvent = (
kind: string,
handler: (payload: any) => any
) => {
clientHookMap[elementId][hookName].handlers = [
...(clientHookMap[elementId][hookName].handlers || []),
{ kind, handler },
];
};

// Initialize the client hook map if it doesn't already exist and add the hook
clientHookMap[vnode.data.elementId] = {
...(clientHookMap[vnode.data.elementId] || {}),
[hookName]: {
el: vnode.elm,
pushEvent,
handleEvent,
},
};

execClientHook(hooks, clientHookMap, elementId, hookName, "create");
});
},
insert: (vnode) => {
const elementId = vnode.data.elementId;

elementHooks.forEach((h) => {
const { name: hookName } = h;

execClientHook(hooks, clientHookMap, elementId, hookName, "insert");
});
},
update: (oldVNode, vnode) => {
const elementId = vnode.data.elementId;

// If the element id has changed, we need to update the client hook map to reflect the new element id
if (oldVNode.data.elementId !== vnode.data.elementId) {
// Move the hook state to the new element id
clientHookMap[vnode.data.elementId] =
clientHookMap[oldVNode.data.elementId];

delete clientHookMap[oldVNode.data.elementId];
}

elementHooks.forEach((h) => {
const { name: hookName } = h;

// If the element id has changed, we also need to update the pushEvent function for each hook
if (oldVNode.data.elementId !== vnode.data.elementId) {
// Update the pushEvent function to use the new element id
const pushEvent = (kind: string, payload: any) => {
socket.send(
JSON.stringify([
"hook:event",
{ id: vnode.data.elementId, hook: hookName, kind, payload },
])
);
};

clientHookMap[vnode.data.elementId][hookName].pushEvent = pushEvent;
}

execClientHook(hooks, clientHookMap, elementId, hookName, "update");
});
},
destroy: (vnode) => {
const elementId = vnode.data.elementId;

elementHooks.forEach((h) => {
const { name: hookName } = h;

execClientHook(hooks, clientHookMap, elementId, hookName, "destroy");

delete clientHookMap[elementId];
});
},
}),
handle_emit: (emit) => {
// find handler by elementId
const { id: elementId, hook: hookName, kind: eventKind, payload } = emit;

const handlers =
clientHookMap[elementId] &&
clientHookMap[elementId][hookName] &&
clientHookMap[elementId][hookName].handlers;

delete clientHookMap[hookId];
});
if (handlers) {
handlers.forEach((h) => {
if (h.kind === eventKind) {
h.handler(payload);
}
});
}
},
});
};
};

function execClientHook(
hooks: Record<string, any>,
clientHookMap: Record<string, any>,
elementId: string,
hookName: string,
hookId: string,
method: string
) {
const hook = hooks[hookName];

if (hook) {
hook[method] && hook[method](clientHookMap[hookId]);
hook[method] &&
hook[method].call(
clientHookMap[elementId][hookName],
clientHookMap[elementId][hookName]
);
} else {
throw new Error(`Client hook ${hookName} not found`);
}
Expand Down
10 changes: 8 additions & 2 deletions client/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function render(

interface Element {
type: "element";
id: string;
tag: string;
attrs: Record<string, any>;
events: EventIdentifier[];
Expand All @@ -44,17 +45,22 @@ function renderElement(element: Element, providers: Providers): VNode {
let { clientHookProvider, eventHandlerProvider } = providers;
let data: VNodeData = { attrs: element.attrs };

// It's important that we set the elementId on the vnode data here
// so that we can reference it in the client hooks when we receive
// and update to check if the elementId has changed and update the
// client hook map accordingly.
data.elementId = element.id;

if (element.key) {
data.key = element.key;
}

// TODO: figure out how to actually ignore updates with snabbdom
if (element.ignore) {
data.ignore = true;
}

if (element.hooks.length > 0) {
data.hook = clientHookProvider(element.hooks);
data.hook = clientHookProvider.hook(element.hooks);
}

// wire up event handlers
Expand Down
Loading

0 comments on commit 47a1af4

Please sign in to comment.