/ cabbages.ts
cabbages.ts
1 import type {Patch as AutomergePatch} from "@automerge/automerge-repo/slim" 2 3 /** 4 * A way to describe move, copy, wrap, cherry-pick and rich text 5 * @see https://braid.org/meeting-62/portals 6 */ 7 export interface Portal { 8 start: number 9 end: number 10 path: PathPart[] 11 referencing: PatchVersion 12 } 13 14 /* 15 * potential for confusion: a PatchVersion is also the document version with 16 * that patch applied? 17 */ 18 19 export type PatchVersion = string & {"patch-version": true} 20 export interface PatchSet { 21 version: PatchVersion 22 parents: PatchVersion[] 23 mergeType: string 24 patches: Patch[] 25 meta: Record<string, string | number | (string | number)[]> 26 } 27 28 export type PathPart = number | string 29 30 type PatchType<T extends string> = T & {"patch-type": T} 31 function type<T extends string>(string: T) { 32 return string as PatchType<T> 33 } 34 35 export type PatchRange = [number?, number?] | PathPart 36 37 type Patch = [path: PathPart[], range: PatchRange, val?: any] 38 39 /** 40 * walk a path in an obj, optionally mutating it to insert missing parts 41 * for the inspiration of @see https://github.com/braid-org/braid-spec/blob/aea85367d60793c113bdb305a4b4ecf55d38061d/draft-toomim-httpbis-range-patch-01.txt 42 * 43 * to insert a string in an array, you need to wrap it in [] 44 * to insert an array in an array you need to wrap it in [] 45 */ 46 47 export function apply<T>( 48 path: PathPart[], 49 target: any, 50 range: PatchRange, 51 val?: any, 52 reviver?: ( 53 value: any, 54 key: any, 55 parent: any, 56 path: PathPart[], 57 obj: T, 58 range: PatchRange 59 ) => void 60 ) { 61 let originalObject = target 62 let p = [...path] 63 64 while (true) { 65 let key = p.shift() 66 if (!p.length) { 67 if (typeof reviver == "function") { 68 val = reviver(val, key, target, path, originalObject, range) 69 } 70 if (Array.isArray(range) || typeof range == "number") { 71 if (typeof key == "undefined") { 72 throw new Error("cant treat top level as a seq") 73 } 74 75 key = key! 76 // splice 77 let [start, end] = Array.isArray(range) ? range : [range, range + 1] 78 const ZERO_LENGTH = Array.isArray(range) && range.length == 0 79 80 if (!ZERO_LENGTH && (start == null || end == null)) { 81 throw new RangeError("it's all or nothing, no half measures") 82 } 83 const DELETE = typeof val == "undefined" 84 const INSERT = start === end && !DELETE 85 const APPEND = ZERO_LENGTH && !DELETE 86 let op = DELETE 87 ? ("del" as const) 88 : APPEND 89 ? ("add" as const) 90 : INSERT 91 ? ("ins" as const) 92 : ("replace" as const) 93 94 if (typeof target[key] == "undefined") { 95 // todo what if it's a function that would return a string? 96 if (typeof val == "string") { 97 target[key] = "" 98 } else { 99 target[key] = [] 100 } 101 } 102 let seq = target[key] 103 104 if (Array.isArray(seq)) { 105 switch (op) { 106 case "add": { 107 Array.isArray(val) ? seq.push(...val) : seq.push(val) 108 return 109 } 110 case "replace": 111 case "ins": { 112 Array.isArray(val) 113 ? seq.splice(start!, end! - 1, ...val) 114 : seq.splice(start!, end! - 1, val) 115 return 116 } 117 case "del": { 118 seq.splice(start!, end! - start!) 119 return 120 } 121 default: { 122 throw new Error("i don't know what happened") 123 } 124 } 125 } 126 127 if (typeof seq == "string") { 128 switch (op) { 129 case "add": { 130 target[key] = seq + val 131 return 132 } 133 case "replace": 134 case "ins": { 135 target[key] = seq.slice(0, start) + val + seq.slice(end) 136 return 137 } 138 case "del": { 139 target[key] = seq.slice(0, start) + seq.slice(end) 140 return 141 } 142 default: { 143 throw new Error("i don't know what happened") 144 } 145 } 146 } 147 // todo should impl for typed arrays? 148 throw new Error("not implemented") 149 } 150 151 if (typeof key == "undefined") { 152 if (typeof range != "string") { 153 throw new Error(`can't index top-level map with ${range}`) 154 } 155 target[range] = val 156 return 157 } 158 if (typeof target[key] == "undefined") { 159 target[key] = {} 160 } 161 // put/delete 162 if (typeof val == "undefined") { 163 delete target[key][range] 164 } else { 165 target[key][range] = val 166 } 167 168 return 169 } 170 171 if (typeof key == "undefined") { 172 throw new Error("cant treat top level as a seq") 173 } 174 175 key = key! 176 let nextkey = p[0] 177 if (typeof target[key] == "undefined") { 178 if (typeof nextkey == "string") { 179 target[key] = {} 180 } else if (typeof nextkey == "number") { 181 target[key] = [] 182 } else { 183 throw new Error(`can't go down this road ${target}.${key}.${nextkey}`) 184 } 185 } 186 187 target = target[key] 188 } 189 } 190 191 export const patch = apply 192 193 class OperationError extends Error {} 194 195 export function fromAutomerge(autopatch: AutomergePatch): Patch 196 export function fromAutomerge( 197 autopatch: AutomergePatch, 198 cb: (...args: Patch) => void 199 ): void 200 export function fromAutomerge( 201 autopatch: AutomergePatch, 202 cb?: (...args: Patch) => void 203 ) { 204 let path = autopatch.path.slice(0, -1) 205 let key = autopatch.path[autopatch.path.length - 1] 206 207 switch (autopatch.action) { 208 case "conflict": 209 case "inc": 210 case "mark": 211 case "unmark": 212 throw new OperationError(`can't handle this: ${autopatch.action}`) 213 case "del": { 214 return cb 215 ? cb(path, [key as number, +key + (autopatch.length || 0)]) 216 : [path, [key as number, +key + (autopatch.length || 0)]] 217 } 218 case "insert": { 219 return cb 220 ? cb(path, [key as number, key as number], autopatch.values) 221 : [path, [key as number, key as number], autopatch.values] 222 } 223 case "splice": { 224 return cb 225 ? cb(path, [key as number, key as number], [autopatch.value]) 226 : [path, [key as number, key as number], [autopatch.value]] 227 } 228 case "put": { 229 return cb 230 ? cb(path, key!, autopatch.value) 231 : [path, key!, autopatch.value] 232 } 233 } 234 }