@zafuru/craftjs-liveblocks
v0.0.0-alpha
Published
Utilities for building a Liveblocks-backed collaboration layer on top of `@zafuru/craftjs-core`.
Readme
@zafuru/craftjs-liveblocks
Utilities for building a Liveblocks-backed collaboration layer on top of @zafuru/craftjs-core.
This package depends on Liveblocks, while @zafuru/craftjs-core itself remains unaware of
Liveblocks and keeps its local-only mode unchanged.
What this package provides
- Normalized document and lock types for a node-level collaboration model
- A
createCraftLiveblocksHooks(roomContext)factory for wiring your own LiveblockscreateRoomContext(...)result into Craft - A
createNodeLockingStorewrapper that enforces:- select-before-edit locking
- guarded
setProp/setCustom/setHidden/move/delete - local selection cleanup when remote locks are lost
What this package does not do
- It does not add Liveblocks to
@zafuru/craftjs-core - It does not create rooms or presence for you
- It does not mutate Craft's core local mode
The intended usage is:
- Build a normal
EditorStorewithuseEditorStore - Create your own RoomContext with Liveblocks
createRoomContext - Call
createCraftLiveblocksHooks(roomContext) - Use the returned
useCraftLiveblocksStorehook and pass its store into<Editor store={...} />
Hook factory
import { createRoomContext } from '@liveblocks/react';
import { createCraftLiveblocksHooks } from '@zafuru/craftjs-liveblocks';
const client = createClient({
authEndpoint: '/api/liveblocks-auth',
});
const roomContext = createRoomContext(client);
const {
RoomProvider,
useMyPresence,
useOthers,
useSelf,
useStorage,
useMutation,
} = roomContext;
const { useCraftLiveblocksStore } = createCraftLiveblocksHooks(roomContext);
const { store, isReady, locksByNodeId } = useCraftLiveblocksStore({
resolver,
schemaVersion: 1,
initialDocument,
});This keeps local mode unchanged while allowing an external Liveblocks room to enforce exclusive node selection and editing.
Storage contract
createCraftLiveblocksHooks(roomContext) expects your room storage and presence to expose
at least:
type Presence = {
selectedNodeIds: string[];
hoveredNodeId?: string | null;
editingNodeId?: string | null;
cursor?: { x: number; y: number } | null;
};
type Storage = {
document: LiveblocksDocumentSnapshot | null;
locks: Record<string, LiveblocksNodeLock>;
};Complete example
import React from 'react';
import { createClient } from '@liveblocks/client';
import { createRoomContext } from '@liveblocks/react';
import { Editor, Frame, ROOT_NODE } from '@zafuru/craftjs-core';
import {
createCraftLiveblocksHooks,
LiveblocksDocumentSnapshot,
LiveblocksNodeLock,
LiveblocksPresence,
} from '@zafuru/craftjs-liveblocks';
const client = createClient({
authEndpoint: '/api/liveblocks-auth',
});
type Presence = LiveblocksPresence;
type Storage = {
document: LiveblocksDocumentSnapshot | null;
locks: Record<string, LiveblocksNodeLock>;
};
const roomContext = createRoomContext<Presence, Storage>(client);
const { RoomProvider } = roomContext;
const { useCraftLiveblocksStore } = createCraftLiveblocksHooks(roomContext);
const resolver = {};
const initialDocument = {
[ROOT_NODE]: {
type: 'div',
isCanvas: true,
props: {},
displayName: 'div',
custom: {},
parent: null,
hidden: false,
nodes: ['node-a'],
linkedNodes: {},
},
'node-a': {
type: 'div',
isCanvas: false,
props: {
text: 'Hello Liveblocks',
},
displayName: 'div',
custom: {},
parent: ROOT_NODE,
hidden: false,
nodes: [],
linkedNodes: {},
},
};
function CollaborativeEditor() {
const { store, isReady, isLockedByOther, getNodeLock } =
useCraftLiveblocksStore({
resolver,
schemaVersion: 1,
initialDocument,
enabled: true,
getUserId: (self) => self.id,
getUserName: (self) => self.info?.name,
});
if (!isReady) {
return <div>Loading...</div>;
}
return (
<Editor store={store} resolver={resolver} enabled>
<Frame />
</Editor>
);
}
export function App() {
return (
<RoomProvider
id="craft-room:demo"
initialPresence={{
selectedNodeIds: [],
hoveredNodeId: null,
editingNodeId: null,
cursor: null,
}}
initialStorage={{
document: null,
locks: {},
}}
>
<CollaborativeEditor />
</RoomProvider>
);
}In this setup:
- local edits are translated into node-level document mutations
- remote document changes are replayed back into the local Craft store incrementally
- node locks live in shared storage and are enforced before selection or mutation
