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}