Packages
React inside Phoenix LiveView — async code splitting, client-side props diffing, and zero-config component discovery.
Retired package: Release invalid - Not production-ready, LiveView 1.0.x compatibility issue
Current section
Files
Jump to
Current section
Files
assets/dist/jsonPatch.js
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Split a JSON Pointer path into its segments (skipping the leading empty string). */
function parsePath(path) {
if (path === "" || path === "/")
return [];
return path
.split("/")
.slice(1)
.map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
}
/** Walk into `obj` following all segments except the last, returning the
* parent object and the final key. Creates intermediate objects as needed. */
function getParent(obj, segments, create) {
if (segments.length === 0)
return null;
let current = obj;
for (let i = 0; i < segments.length - 1; i++) {
const seg = segments[i];
if (current[seg] === undefined ||
current[seg] === null ||
typeof current[seg] !== "object" ||
Array.isArray(current[seg])) {
if (!create)
return null;
current[seg] = {};
}
current = current[seg];
}
return { parent: current, key: segments[segments.length - 1] };
}
/** Get the value at a JSON Pointer path inside obj. */
function getAt(obj, segments) {
let current = obj;
for (const seg of segments) {
if (current === null || typeof current !== "object")
return undefined;
current = current[seg];
}
return current;
}
/** Deep equality check. */
function deepEqual(a, b) {
if (a === b)
return true;
if (a === null || b === null)
return false;
if (typeof a !== typeof b)
return false;
if (typeof a !== "object")
return false;
if (Array.isArray(a) !== Array.isArray(b))
return false;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length)
return false;
return a.every((v, i) => deepEqual(v, b[i]));
}
const aObj = a;
const bObj = b;
const aKeys = Object.keys(aObj);
const bKeys = Object.keys(bObj);
if (aKeys.length !== bKeys.length)
return false;
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bObj, k) && deepEqual(aObj[k], bObj[k]));
}
// ---------------------------------------------------------------------------
// applyPatch
// ---------------------------------------------------------------------------
/**
* Mutates `obj` in place by applying an array of patch operations.
*
* Supports standard RFC 6902 ops: add, remove, replace, test.
* Supports custom Phoenix Stream ops: upsert, delete_by_id, limit.
*/
export function applyPatch(obj, patches) {
for (const patch of patches) {
const segments = parsePath(patch.path);
switch (patch.op) {
case "add":
case "replace": {
const result = getParent(obj, segments, true);
if (result) {
result.parent[result.key] = patch.value;
}
break;
}
case "remove": {
const result = getParent(obj, segments, false);
if (result) {
delete result.parent[result.key];
}
break;
}
case "test": {
// No-op — used for cache-busting only.
break;
}
case "upsert": {
const arr = getAt(obj, segments);
if (!Array.isArray(arr))
break;
const typedArr = arr;
const match = patch.match;
const value = patch.value;
const existingIdx = typedArr.findIndex((item) => item.__dom_id === match);
if (existingIdx !== -1) {
// Update in place.
typedArr[existingIdx] = value;
}
else {
// Insert at given position (from patch.at) or append.
const at = typeof patch.at === "number" ? patch.at : typedArr.length;
const insertAt = Math.min(at, typedArr.length);
typedArr.splice(insertAt, 0, value);
}
break;
}
case "delete_by_id": {
const arr = getAt(obj, segments);
if (!Array.isArray(arr))
break;
const typedArr = arr;
const match = patch.match;
const idx = typedArr.findIndex((item) => item.__dom_id === match);
if (idx !== -1) {
typedArr.splice(idx, 1);
}
break;
}
case "limit": {
const arr = getAt(obj, segments);
if (!Array.isArray(arr))
break;
const n = patch.value;
const result2 = getParent(obj, segments, false);
if (!result2)
break;
if (n === 0) {
result2.parent[result2.key] = [];
}
else if (n > 0) {
result2.parent[result2.key] = arr.slice(0, n);
}
else {
result2.parent[result2.key] = arr.slice(n);
}
break;
}
}
}
}
// ---------------------------------------------------------------------------
// computeDiff
// ---------------------------------------------------------------------------
/**
* Returns an array of PatchOp describing changes from `prev` to `next`.
*
* - Added keys → `add` op
* - Changed vals → `replace` op (recurses into nested objects)
* - Removed keys → `remove` op
* - Arrays → compared with deepEqual, replaced wholesale if different
*/
export function computeDiff(prev, next, basePath = "") {
const ops = [];
const prevKeys = Object.keys(prev);
const nextKeys = Object.keys(next);
// Removed keys
for (const key of prevKeys) {
if (!Object.prototype.hasOwnProperty.call(next, key)) {
ops.push({ op: "remove", path: `${basePath}/${key}` });
}
}
// Added or changed keys
for (const key of nextKeys) {
const path = `${basePath}/${key}`;
if (!Object.prototype.hasOwnProperty.call(prev, key)) {
// Added
ops.push({ op: "add", path, value: next[key] });
}
else {
const prevVal = prev[key];
const nextVal = next[key];
if (deepEqual(prevVal, nextVal))
continue;
// Both plain objects (not arrays, not null) → recurse
if (prevVal !== null &&
nextVal !== null &&
typeof prevVal === "object" &&
typeof nextVal === "object" &&
!Array.isArray(prevVal) &&
!Array.isArray(nextVal)) {
const nested = computeDiff(prevVal, nextVal, path);
ops.push(...nested);
}
else {
ops.push({ op: "replace", path, value: nextVal });
}
}
}
return ops;
}