Skip to content

Instantly share code, notes, and snippets.

@chee
Created August 6, 2024 20:49
Show Gist options
  • Save chee/23aec8446e77f787c112592fe3cc6ced to your computer and use it in GitHub Desktop.
Save chee/23aec8446e77f787c112592fe3cc6ced to your computer and use it in GitHub Desktop.
patch applier
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