forked from
leaflet.pub/leaflet
a tool for shared writing and social publishing
1"use client";
2import {
3 createContext,
4 useCallback,
5 useContext,
6 useEffect,
7 useMemo,
8 useRef,
9 useState,
10} from "react";
11import { useSubscribe } from "src/replicache/useSubscribe";
12import {
13 DeepReadonlyObject,
14 PushRequest,
15 PushRequestV1,
16 Replicache,
17 WriteTransaction,
18} from "replicache";
19import { mutations } from "./mutations";
20import { Attributes } from "./attributes";
21import { Attribute, Data, FilterAttributes } from "./attributes";
22import { clientMutationContext } from "./clientMutationContext";
23import { supabaseBrowserClient } from "supabase/browserClient";
24import { callRPC } from "app/api/rpc/client";
25import { UndoManager } from "@rocicorp/undo";
26import { addShortcut } from "src/shortcuts";
27import { createUndoManager } from "src/undoManager";
28import { RealtimeChannel } from "@supabase/supabase-js";
29
30export type Fact<A extends Attribute> = {
31 id: string;
32 entity: string;
33 attribute: A;
34 data: Data<A>;
35};
36
37let ReplicacheContext = createContext({
38 undoManager: createUndoManager(),
39 rootEntity: "" as string,
40 rep: null as null | Replicache<ReplicacheMutators>,
41 initialFacts: [] as Fact<Attribute>[],
42 permission_token: {} as PermissionToken,
43});
44export function useReplicache() {
45 return useContext(ReplicacheContext);
46}
47export type ReplicacheMutators = {
48 [k in keyof typeof mutations]: (
49 tx: WriteTransaction,
50 args: Parameters<(typeof mutations)[k]>[0],
51 ) => Promise<void>;
52};
53
54export type PermissionToken = {
55 id: string;
56 root_entity: string;
57 permission_token_rights: {
58 created_at: string;
59 entity_set: string;
60 change_entity_set: boolean;
61 create_token: boolean;
62 token: string;
63 read: boolean;
64 write: boolean;
65 }[];
66};
67export function ReplicacheProvider(props: {
68 rootEntity: string;
69 initialFacts: Fact<Attribute>[];
70 token: PermissionToken;
71 name: string;
72 children: React.ReactNode;
73 initialFactsOnly?: boolean;
74 disablePull?: boolean;
75}) {
76 let [rep, setRep] = useState<null | Replicache<ReplicacheMutators>>(null);
77 let [undoManager] = useState(createUndoManager());
78 useEffect(() => {
79 return addShortcut([
80 {
81 metaKey: true,
82 key: "z",
83 handler: () => {
84 undoManager.undo();
85 },
86 },
87 {
88 metaKey: true,
89 shift: true,
90 key: "z",
91 handler: () => {
92 undoManager.redo();
93 },
94 },
95 {
96 metaKey: true,
97 shift: true,
98 key: "Z",
99 handler: () => {
100 undoManager.redo();
101 },
102 },
103 ]);
104 }, [undoManager]);
105 useEffect(() => {
106 if (props.initialFactsOnly) return;
107 let supabase = supabaseBrowserClient();
108 let newRep = new Replicache({
109 pullInterval: props.disablePull ? null : undefined,
110 pushDelay: 500,
111 mutators: Object.fromEntries(
112 Object.keys(mutations).map((m) => {
113 return [
114 m,
115 async (tx: WriteTransaction, args: any) => {
116 await mutations[m as keyof typeof mutations](
117 args,
118 clientMutationContext(tx, {
119 permission_token_id: props.token.id,
120 undoManager,
121 rep: newRep,
122 ignoreUndo: args.ignoreUndo || tx.reason !== "initial",
123 defaultEntitySet:
124 props.token.permission_token_rights[0]?.entity_set,
125 }),
126 );
127 },
128 ];
129 }),
130 ) as ReplicacheMutators,
131 licenseKey: "l381074b8d5224dabaef869802421225a",
132 pusher: async (pushRequest) => {
133 const batchSize = 250;
134 let smolpushRequest = {
135 ...pushRequest,
136 mutations: pushRequest.mutations.slice(0, batchSize),
137 } as PushRequest;
138 let response = (
139 await callRPC("push", {
140 pushRequest: smolpushRequest,
141 token: props.token,
142 rootEntity: props.name,
143 })
144 ).result;
145 if (pushRequest.mutations.length > batchSize)
146 setTimeout(() => {
147 newRep.push();
148 }, 50);
149 return {
150 response,
151 httpRequestInfo: { errorMessage: "", httpStatusCode: 200 },
152 };
153 },
154 puller: async (pullRequest) => {
155 let res = await callRPC("pull", {
156 pullRequest,
157 token_id: props.token.id,
158 });
159 return {
160 response: res,
161 httpRequestInfo: { errorMessage: "", httpStatusCode: 200 },
162 };
163 },
164 name: props.name,
165 indexes: {
166 eav: { jsonPointer: "/indexes/eav", allowEmpty: true },
167 vae: { jsonPointer: "/indexes/vae", allowEmpty: true },
168 },
169 });
170
171 setRep(newRep);
172 let channel: RealtimeChannel | null = null;
173 if (!props.disablePull) {
174 channel = supabase.channel(`rootEntity:${props.name}`);
175
176 channel.on("broadcast", { event: "poke" }, () => {
177 newRep.pull();
178 });
179 channel.subscribe();
180 }
181 return () => {
182 newRep.close();
183 setRep(null);
184 channel?.unsubscribe();
185 };
186 }, [props.name, props.initialFactsOnly, props.token, props.disablePull]);
187 return (
188 <ReplicacheContext.Provider
189 value={{
190 undoManager,
191 rep,
192 rootEntity: props.rootEntity,
193 initialFacts: props.initialFacts,
194 permission_token: props.token,
195 }}
196 >
197 {props.children}
198 </ReplicacheContext.Provider>
199 );
200}
201
202type CardinalityResult<A extends Attribute> =
203 (typeof Attributes)[A]["cardinality"] extends "one"
204 ? DeepReadonlyObject<Fact<A>> | null
205 : DeepReadonlyObject<Fact<A>>[];
206export function useEntity<A extends Attribute>(
207 entity: string | null,
208 attribute: A,
209): CardinalityResult<A> {
210 let { rep, initialFacts } = useReplicache();
211 let fallbackData = useMemo(
212 () =>
213 initialFacts.filter(
214 (f) => f.entity === entity && f.attribute === attribute,
215 ),
216 [entity, attribute, initialFacts],
217 );
218 let data = useSubscribe(
219 rep,
220 async (tx) => {
221 if (entity === null) return null;
222 let initialized = await tx.get("initialized");
223 if (!initialized) return null;
224 return (
225 (
226 await tx
227 .scan<Fact<A>>({
228 indexName: "eav",
229 prefix: `${entity}-${attribute}`,
230 })
231 // hack to handle rich bluesky-post type
232 .toArray()
233 ).filter((f) => f.attribute === attribute)
234 );
235 },
236 {
237 default: null,
238 dependencies: [entity, attribute],
239 },
240 );
241 let d = data || fallbackData;
242 let a = Attributes[attribute];
243 return a.cardinality === "many"
244 ? ((a.type === "ordered-reference"
245 ? d.sort((a, b) => {
246 return (
247 a as Fact<keyof FilterAttributes<{ type: "ordered-reference" }>>
248 ).data.position >
249 (b as Fact<keyof FilterAttributes<{ type: "ordered-reference" }>>)
250 .data.position
251 ? 1
252 : -1;
253 })
254 : d) as CardinalityResult<A>)
255 : d.length === 0 && data === null
256 ? (null as CardinalityResult<A>)
257 : (d[0] as CardinalityResult<A>);
258}
259
260export function useReferenceToEntity<
261 A extends keyof FilterAttributes<{ type: "reference" | "ordered-reference" }>,
262>(attribute: A, entity: string) {
263 let { rep, initialFacts } = useReplicache();
264 let fallbackData = useMemo(
265 () =>
266 initialFacts.filter(
267 (f) =>
268 (f as Fact<A>).data.value === entity && f.attribute === attribute,
269 ),
270 [entity, attribute, initialFacts],
271 );
272 let data = useSubscribe(
273 rep,
274 async (tx) => {
275 if (entity === null) return null;
276 let initialized = await tx.get("initialized");
277 if (!initialized) return null;
278 return (
279 await tx
280 .scan<Fact<A>>({ indexName: "vae", prefix: `${entity}-${attribute}` })
281 .toArray()
282 ).filter((f) => f.attribute === attribute);
283 },
284 {
285 default: null,
286 dependencies: [entity, attribute],
287 },
288 );
289 return data || (fallbackData as Fact<A>[]);
290}