a tool for shared writing and social publishing
1import { Replicache, WriteTransaction } from "replicache"; 2import * as Y from "yjs"; 3import * as base64 from "base64-js"; 4import { FactWithIndexes, scanIndex } from "./utils"; 5import { Attribute, Attributes, FilterAttributes } from "./attributes"; 6import type { Fact, ReplicacheMutators } from "."; 7import { FactInput, MutationContext } from "./mutations"; 8import { supabaseBrowserClient } from "supabase/browserClient"; 9import { v7 } from "uuid"; 10import { UndoManager } from "src/undoManager"; 11 12export function clientMutationContext( 13 tx: WriteTransaction, 14 { 15 rep, 16 undoManager, 17 ignoreUndo, 18 defaultEntitySet, 19 permission_token_id, 20 }: { 21 undoManager: UndoManager; 22 rep: Replicache<ReplicacheMutators>; 23 ignoreUndo: boolean; 24 defaultEntitySet: string; 25 permission_token_id: string; 26 }, 27) { 28 let ctx: MutationContext = { 29 permission_token_id, 30 async runOnServer(cb) {}, 31 async runOnClient(cb) { 32 let supabase = supabaseBrowserClient(); 33 return cb({ supabase, tx }); 34 }, 35 async createEntity({ entityID }) { 36 tx.set(entityID, true); 37 return true; 38 }, 39 scanIndex: { 40 async eav(entity, attribute) { 41 return scanIndex(tx).eav(entity, attribute); 42 }, 43 }, 44 async assertFact(f) { 45 let attribute = Attributes[f.attribute as Attribute]; 46 if (!attribute) return; 47 let id = f.id || v7(); 48 let data = { ...f.data }; 49 let existingFact = [] as Fact<any>[]; 50 if (attribute.cardinality === "one") { 51 existingFact = await scanIndex(tx).eav(f.entity, f.attribute); 52 if (existingFact[0]) { 53 id = existingFact[0].id; 54 if (attribute.type === "text") { 55 const oldUpdate = base64.toByteArray( 56 ( 57 existingFact[0]?.data as Fact< 58 keyof FilterAttributes<{ type: "text" }> 59 >["data"] 60 ).value, 61 ); 62 let textData = data as Fact< 63 keyof FilterAttributes<{ type: "text" }> 64 >["data"]; 65 const newUpdate = base64.toByteArray(textData.value); 66 const updateBytes = Y.mergeUpdates([oldUpdate, newUpdate]); 67 textData.value = base64.fromByteArray(updateBytes); 68 } 69 } 70 } else if (f.id) { 71 // For cardinality "many" with an explicit ID, fetch the existing fact 72 // so undo can restore it instead of deleting 73 let fact = await tx.get(f.id); 74 if (fact) { 75 existingFact = [fact as Fact<any>]; 76 } 77 } 78 if (!ignoreUndo) 79 undoManager.add({ 80 undo: () => { 81 if (existingFact[0]) { 82 rep.mutate.assertFact({ ignoreUndo: true, ...existingFact[0] }); 83 } else { 84 if (attribute.cardinality === "one" && !f.id) 85 rep.mutate.retractAttribute({ 86 ignoreUndo: true, 87 attribute: f.attribute as keyof FilterAttributes<{ 88 cardinality: "one"; 89 }>, 90 entity: f.entity, 91 }); 92 rep.mutate.retractFact({ ignoreUndo: true, factID: id }); 93 } 94 }, 95 redo: () => { 96 rep.mutate.assertFact({ ignoreUndo: true, ...(f as Fact<any>) }); 97 }, 98 }); 99 await tx.set(id, FactWithIndexes({ id, ...f, data })); 100 }, 101 async retractFact(id) { 102 let fact = await tx.get(id); 103 if (!ignoreUndo) 104 undoManager.add({ 105 undo: () => { 106 if (fact) { 107 rep.mutate.assertFact({ 108 ignoreUndo: true, 109 ...(fact as Fact<any>), 110 }); 111 } else { 112 rep.mutate.retractFact({ ignoreUndo: true, factID: id }); 113 } 114 }, 115 redo: () => { 116 rep.mutate.retractFact({ factID: id }); 117 }, 118 }); 119 await tx.del(id); 120 }, 121 async deleteEntity(entity) { 122 let existingFacts = await tx 123 .scan<Fact<Attribute>>({ 124 indexName: "eav", 125 prefix: `${entity}`, 126 }) 127 .toArray(); 128 let references = await tx 129 .scan<Fact<Attribute>>({ 130 indexName: "vae", 131 prefix: entity, 132 }) 133 .toArray(); 134 let facts = [...existingFacts, ...references]; 135 await Promise.all(facts.map((f) => tx.del(f.id))); 136 if (!ignoreUndo && facts.length > 0) { 137 undoManager.add({ 138 undo: async () => { 139 let input: FactInput[] & { ignoreUndo?: true } = facts.map( 140 (f) => 141 ({ 142 id: f.id, 143 attribute: f.attribute, 144 entity: f.entity, 145 data: f.data, 146 }) as FactInput, 147 ); 148 input.ignoreUndo = true; 149 await rep.mutate.createEntity([ 150 { entityID: entity, permission_set: defaultEntitySet }, 151 ]); 152 await rep.mutate.assertFact(input); 153 }, 154 redo: () => { 155 rep.mutate.deleteEntity({ entity, ignoreUndo: true }); 156 }, 157 }); 158 } 159 }, 160 }; 161 return ctx; 162}