Created
August 6, 2024 20:49
-
-
Save chee/23aec8446e77f787c112592fe3cc6ced to your computer and use it in GitHub Desktop.
patch applier
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Patch = [path: PathPart[], obj: any, range: PatchRange, val?: any] | |
/** | |
* walk a path in an obj, optionally mutating it to insert missing parts | |
* @see https://github.com/braid-org/braid-spec/blob/aea85367d60793c113bdb305a4b4ecf55d38061d/draft-toomim-httpbis-range-patch-01.txt | |
* | |
* to insert a string in an array, you need to wrap it in [] | |
* | |
* to insert an array in an array you need to wrap it in [] | |
*/ | |
export function patch<T>( | |
path: PathPart[], | |
obj: any, | |
range: PatchRange, | |
val?: any, | |
reviver?: ( | |
value: any, | |
key: any, | |
parent: any, | |
path: PathPart[], | |
obj: T, | |
range: PatchRange, | |
) => void, | |
) { | |
let originalObject = obj | |
let p = [...path] | |
while (true) { | |
let key = p.shift() | |
if (!p.length) { | |
if (typeof reviver == "function") { | |
val = reviver(val, key, obj, path, originalObject, range) | |
} | |
if (Array.isArray(range) || typeof range == "number") { | |
if (typeof key == "undefined") { | |
throw new Error("cant treat top level as a seq") | |
} | |
key = key! | |
// splice | |
let [start, end] = Array.isArray(range) ? range : [range, range + 1] | |
const ZERO_LENGTH = Array.isArray(range) && range.length == 0 | |
if (!ZERO_LENGTH && (start == null || end == null)) { | |
throw new RangeError("it's all or nothing, no half measures") | |
} | |
const DELETE = typeof val == "undefined" | |
const INSERT = start === end && !DELETE | |
const APPEND = ZERO_LENGTH && !DELETE | |
let op = DELETE | |
? ("del" as const) | |
: APPEND | |
? ("add" as const) | |
: INSERT | |
? ("ins" as const) | |
: ("replace" as const) | |
if (typeof obj[key] == "undefined") { | |
// todo what if it's a function that would return a string? | |
if (typeof val == "string") { | |
obj[key] = "" | |
} else { | |
obj[key] = [] | |
} | |
} | |
let seq = obj[key] | |
if (Array.isArray(seq)) { | |
switch (op) { | |
case "add": { | |
Array.isArray(val) ? seq.push(...val) : seq.push(val) | |
return | |
} | |
case "replace": | |
case "ins": { | |
Array.isArray(val) | |
? seq.splice(start!, end! - 1, ...val) | |
: seq.splice(start!, end! - 1, val) | |
return | |
} | |
case "del": { | |
seq.splice(start!, end! - start!) | |
return | |
} | |
default: { | |
throw new Error("i don't know what happened") | |
} | |
} | |
} | |
if (typeof seq == "string") { | |
switch (op) { | |
case "add": { | |
obj[key] = seq + val | |
return | |
} | |
case "replace": | |
case "ins": { | |
obj[key] = seq.slice(0, start) + val + seq.slice(end) | |
return | |
} | |
case "del": { | |
obj[key] = seq.slice(0, start) + seq.slice(end) | |
return | |
} | |
default: { | |
throw new Error("i don't know what happened") | |
} | |
} | |
} | |
// todo should impl for typed arrays? | |
throw new Error("not implemented") | |
} | |
if (typeof key == "undefined") { | |
if (typeof range != "string") { | |
throw new Error(`can't index top-level map with ${range}`) | |
} | |
obj[range] = val | |
return | |
} | |
if (typeof obj[key] == "undefined") { | |
obj[key] = {} | |
} | |
// put/delete | |
if (typeof val == "undefined") { | |
delete obj[key][range] | |
} else { | |
obj[key][range] = val | |
} | |
return | |
} | |
if (typeof key == "undefined") { | |
throw new Error("cant treat top level as a seq") | |
} | |
key = key! | |
let nextkey = p[0] | |
if (typeof obj[key] == "undefined") { | |
if (typeof nextkey == "string") { | |
obj[key] = {} | |
} else if (typeof nextkey == "number") { | |
obj[key] = [] | |
} else { | |
throw new Error(`can't go down this road ${obj}.${key}.${nextkey}`) | |
} | |
} | |
obj = obj[key] | |
} | |
} | |
export function fromAutomerge(autopatch: AutomergePatch, obj: any): Patch { | |
let path = autopatch.path.slice(0, -1) | |
let key = autopatch.path[autopatch.path.length - 1] | |
// @ts-expect-error | |
let [range, val]: PatchRange = (() => { | |
switch (autopatch.action) { | |
case "conflict": { | |
return [ | |
key!, | |
{ | |
$type: type("automerge:conflict"), | |
}, | |
] | |
} | |
case "inc": { | |
return [ | |
key as number, | |
{ | |
$type: type("automerge:inc"), | |
$value: autopatch.value, | |
}, | |
] | |
} | |
case "mark": { | |
return [ | |
key, | |
{ | |
$type: type("automerge:mark"), | |
$meta: { | |
marks: autopatch.marks, | |
}, | |
}, | |
] | |
} | |
case "unmark": { | |
return [ | |
key!, | |
{ | |
$type: type("automerge:unmark"), | |
$meta: { | |
start: autopatch.start, | |
end: autopatch.end, | |
name: autopatch.name, | |
}, | |
}, | |
] | |
} | |
case "del": { | |
return [[key as number, +key + (autopatch.length || 0)]] | |
} | |
case "insert": { | |
if (autopatch.marks || autopatch.conflicts) { | |
return { | |
$type: type("automerge:insert"), | |
$value: autopatch.values, | |
$meta: { | |
marks: autopatch.marks, | |
conflicts: autopatch.conflicts, | |
}, | |
} | |
} | |
return [[key as number, key as number], autopatch.values] | |
} | |
case "splice": { | |
if (autopatch.marks) { | |
return { | |
$type: type("automerge:splice"), | |
$value: autopatch.value, | |
$meta: { | |
marks: autopatch.marks, | |
}, | |
} | |
} | |
return [[key as number, key as number], [autopatch.value]] | |
} | |
case "put": { | |
return [key!, autopatch.value] | |
} | |
} | |
})() | |
return [path, obj, range, val] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment