Skip to content

feat(collaborative-vue): optional collaborate() proxy for v-model (stacked on #1041)#1042

Open
mmamedel wants to merge 2 commits into
streamich:masterfrom
mmamedel:feat/collaborative-vue-proxy
Open

feat(collaborative-vue): optional collaborate() proxy for v-model (stacked on #1041)#1042
mmamedel wants to merge 2 commits into
streamich:masterfrom
mmamedel:feat/collaborative-vue-proxy

Conversation

@mmamedel

@mmamedel mmamedel commented Jun 6, 2026

Copy link
Copy Markdown

Important

Stacked on #1041 (the collaborative-vue composables). This branch contains #1041's commit as a dependency, so the diff currently shows both. Please review/merge #1041 first; the only new commit here is ed998ec (the optional collaborate() proxy). Once #1041 lands, this reduces to just that commit.

What

Adds an optional high-level collaborate(model) to @jsonjoy.com/collaborative-vue — a single fine-grained reactive state proxy you read and mutate like a plain object, purpose-built for Vue v-model two-way binding, which the low-level composables (#1041) serve awkwardly.

<script setup lang="ts">
import {Model} from 'json-joy/lib/json-crdt';
import {collaborate} from '@jsonjoy.com/collaborative-vue';

const {state} = collaborate<Board>(Model.create({title: 'Untitled', notes: []}));
</script>

<template>
  <input v-model="state.title" />        <!-- two-way bound to the CRDT -->
  <li v-for="(n, i) in state.notes" :key="i"><input v-model="n.text" /></li>
</template>

Why a separate, additive layer

This keeps the project's layering — node API ← composables ← optional sugar — rather than competing with it:

  • It is sugar over the composables: reactivity uses the same per-node onNodeChange('self') subscription; writes dispatch through json-joy's existing node verbs (ObjApi.set/del, ArrApi.ins/upd/del/merge, VecApi.set).
  • No additions to json-joy core (this is the rewrite of the originally-proposed .w API, now living entirely in the binding).
  • Reads subscribe per node, so a remote applyPatch re-renders only the nodes that changed — no reconcile pass, no echo guard, no second store.
  • For precise control (in-place text ops, merge vs set, explicit self/child/subtree granularity) you drop down to the composables; the proxy doesn't hide them.

API

  • collaborate<T>(model): { state: T; dispose(): void }
  • dispose() runs automatically when called inside a component setup() (via onScopeDispose).

Tests

5 tests (jest + watchEffect), all green: reads mirror the view; plain assignment + array methods record ops; fine-grained (a title edit doesn't re-run an effect reading the notes); remote-patch convergence re-renders the affected node; dispose() stops reacting. Builds via the root solution tsc -b.

mmamedel and others added 2 commits June 6, 2026 09:58
… json-joy CRDTs

Adds @jsonjoy.com/collaborative-vue — the Vue 3 counterpart of
@jsonjoy.com/collaborative-react, mirroring its surface and names:

- context.ts  — provide/inject helpers (createNodeCtx, provideModel/provideNode,
  useCtx{Model,Node}[Strict]) and <ModelProvider>/<NodeProvider>
- composables.ts — useModelTick/View/Model/ModelTry, useNodeEvents/Effect/
  Change/Node/NodeView, usePath/PathView/Obj/Arr/Str
- components.ts — <UseModel>/<UseNode> render-prop components via scoped slots

Reads return Vue refs/computed; writes use json-joy's existing node verbs
(obj.set, str.ins, arr.push, …) — no additions to json-joy core. Node/path
composables force-trigger on every matching change so stable node handles still
re-notify, matching React's re-render-on-change semantics.

Builds entirely on the existing public API. Tested with jest (composables via
effectScope, context/components mounted in jsdom).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…omposables

Adds a high-level `collaborate(model)` that returns a single fine-grained
reactive `state` proxy — plain-object reads and `v-model` two-way binding, the
Vue-idiomatic ergonomics the low-level composables serve awkwardly.

It is sugar over the composables: reactivity uses the same per-node
`onNodeChange('self')` subscription, and writes dispatch through json-joy's
existing node verbs (ObjApi.set/del, ArrApi.ins/upd/del/merge, VecApi.set).
No additions to json-joy core.

5 tests (jest, watchEffect): reads mirror the view; assignment + array methods
record ops; fine-grained (sibling edits don't cross-trigger); remote-patch
convergence; dispose() teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant