Last active
May 30, 2024 20:16
-
-
Save BitPhinix/a98b5f35a0be9cd8700103c8fd406d4d to your computer and use it in GitHub Desktop.
yjs react binding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useMemo, useSyncExternalStore } from "react"; | |
import { | |
type SharedArray, | |
type SharedDoc, | |
type SharedMap, | |
type SharedType, | |
type ToJson, | |
type ToJsonDeep, | |
} from "./types.ts"; | |
type Store<T> = { | |
subscribe: (callback: () => void) => () => void; | |
getSnapshot: () => T; | |
}; | |
function makePlainValueStore<T extends SharedType>( | |
sharedType: T, | |
deep: false, | |
): Store<ToJson<T>>; | |
function makePlainValueStore<T extends SharedType>( | |
sharedType: T | undefined, | |
deep: false, | |
): Store<ToJson<T> | undefined>; | |
function makePlainValueStore<T extends SharedType>( | |
sharedType: T, | |
deep?: true, | |
): Store<ToJsonDeep<T>>; | |
function makePlainValueStore<T extends SharedType>( | |
sharedType: T | undefined, | |
deep?: true, | |
): Store<ToJsonDeep<T> | undefined>; | |
function makePlainValueStore( | |
sharedType: SharedType, | |
deep = true, | |
): Store<unknown> { | |
return { | |
subscribe: (callback: () => void) => { | |
sharedType?.[deep ? "observeDeep" : "observe"](callback); | |
return () => sharedType?.[deep ? "unobserveDeep" : "unobserve"](callback); | |
}, | |
getSnapshot: () => { | |
// TODO: Don't deeply convert when not deep | |
return sharedType?.toJSON() as unknown; | |
}, | |
}; | |
} | |
function useTypeValue<T extends SharedType>(type: T, deep: false): ToJson<T>; | |
function useTypeValue<T extends SharedType>( | |
type: T | undefined, | |
deep: false, | |
): ToJson<T> | undefined; | |
function useTypeValue<T extends SharedType>( | |
type: T, | |
deep?: true, | |
): ToJsonDeep<T>; | |
function useTypeValue<T extends SharedType>( | |
type: T | undefined, | |
deep?: true, | |
): ToJsonDeep<T> | undefined; | |
function useTypeValue<T extends SharedType>( | |
type: T | undefined, | |
deep = true, | |
): unknown { | |
const store = useMemo( | |
() => makePlainValueStore(type, deep as true), | |
[type, deep], | |
); | |
return useSyncExternalStore(store.subscribe, store.getSnapshot) as unknown; | |
} | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
export type ProviderProps<TDocument extends SharedDoc<any>> = { | |
children: React.ReactNode; | |
doc: TDocument; | |
}; | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
export function createYjsContext<TDocument extends SharedDoc<any>>() { | |
const Context = React.createContext<TDocument | null>(null); | |
const Provider = ({ children, doc }: ProviderProps<TDocument>) => { | |
return <Context.Provider value={doc}>{children}</Context.Provider>; | |
}; | |
const useDoc = () => { | |
const doc = React.useContext(Context); | |
if (!doc) { | |
throw new Error( | |
"useDoc must be used within the corresponding <createYjsContext.Provider/>", | |
); | |
} | |
return doc; | |
}; | |
function useType<T extends SharedType>( | |
get: (doc: TDocument) => T, | |
deep: false, | |
): [ToJson<T>, T]; | |
function useType<T extends SharedType>( | |
get: (doc: TDocument) => T | undefined, | |
deep: false, | |
): [ToJson<T> | undefined, T | undefined]; | |
function useType<T extends SharedType>( | |
get: (doc: TDocument) => T, | |
deep?: true, | |
): [ToJsonDeep<T>, T]; | |
function useType<T extends SharedType>( | |
get: (doc: TDocument) => T | undefined, | |
deep?: true, | |
): [ToJsonDeep<T> | undefined, T | undefined]; | |
function useType(get: (doc: TDocument) => SharedType, deep = true): unknown { | |
const doc = useDoc(); | |
const type = get(doc); | |
return [useTypeValue(type, deep as true) as unknown, type]; | |
} | |
return { | |
Provider, | |
useDoc, | |
useType, | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/no-explicit-any */ | |
import * as Y from "yjs"; | |
export type SharedType = SharedMap<any> | SharedArray<any>; | |
type SharedTypeLike = SharedMapTyping<any> | SharedArrayTyping<any>; | |
export type Value = SharedTypeLike | Json; | |
export type Json = JsonScalar | JsonArray | JsonObject; | |
export type JsonScalar = string | number | boolean | null; | |
export type JsonArray = Json[]; | |
export type JsonObject = { [key: string]: Json | undefined }; | |
type ToJsonValue<T extends Value> = T extends SharedType ? undefined : T; | |
type MapJsonValue<TData extends Record<string, Value>> = { | |
[K in keyof TData]: ToJsonValue<TData[K]>; | |
}; | |
export type ToJson<T extends Value> = T extends SharedMap<infer TData> | |
? { | |
[K in keyof TData as K extends Json ? K : never]: ToJsonValue<TData[K]>; | |
} | |
: T extends SharedArray<infer TData> | |
? (TData extends Json ? TData : never)[] | |
: T extends Json | |
? T | |
: never; | |
export type ToJsonDeep<T extends Value> = T extends SharedArray<infer TValue> | |
? ToJsonDeep<TValue> | |
: T extends SharedMap<infer TData> | |
? { | |
[K in keyof TData]: ToJsonDeep<TData[K]>; | |
} | |
: T extends Json | |
? T | |
: never; | |
export type SharedTypeConstructor<T extends SharedType> = | |
new () => T extends SharedMap<any> ? SharedMap<any> : SharedArray<any>; | |
type KeysThatExtend<T, V> = keyof { | |
[K in keyof T as T[K] extends V ? K : never]: T[K]; | |
}; | |
type EntryType<T extends Record<string, unknown>> = { | |
[key in keyof T]: [key, T[key]]; | |
}[keyof T]; | |
type OptionalKeys<T> = { | |
[K in keyof T]-?: undefined extends T[K] ? K : never; | |
}[keyof T]; | |
type SharedDocTypings<TData extends Record<string, SharedType>> = { | |
get<TKey extends keyof TData, TValue extends TData[TKey]>( | |
key: TKey, | |
typeConstructor: SharedTypeConstructor<TValue>, | |
): TData[TKey]; | |
get<TKey extends keyof TData>(key: TKey): TData[TKey] | undefined; | |
getMap<TKey extends KeysThatExtend<TData, SharedType>>( | |
key: TKey, | |
): TData[TKey]; | |
getArray<TKey extends KeysThatExtend<TData, SharedType>>( | |
key: TKey, | |
): TData[TKey]; | |
}; | |
export type SharedDoc<TData extends Record<string, SharedType>> = Omit< | |
Y.Doc, | |
keyof SharedDocTypings<TData> | |
> & | |
SharedDocTypings<TData>; | |
export const SharedDoc = Y.Doc as new < | |
TData extends Record<string, SharedType>, | |
>() => SharedDoc<TData>; | |
type SharedMapTyping<TData extends Record<string, Value>> = { | |
clone(): SharedMap<TData>; | |
keys(): IterableIterator<keyof TData>; | |
values(): IterableIterator<TData[keyof TData]>; | |
entries(): IterableIterator<EntryType<TData>>; | |
forEach( | |
fn: ( | |
key: keyof TData, | |
value: TData[keyof TData], | |
self: SharedMap<TData>, | |
) => void, | |
): void; | |
delete(key: OptionalKeys<TData>): void; | |
set<TKey extends keyof TData, TValue extends TData[TKey]>( | |
key: TKey, | |
value: TValue, | |
): TValue; | |
get<TKey extends keyof TData>(key: TKey): TData[TKey]; | |
has<TKey extends keyof TData>(key: TKey): boolean; | |
toJSON(): MapJsonValue<TData>; | |
[Symbol.iterator](): IterableIterator<EntryType<TData>>; | |
}; | |
export type SharedMap<TData extends Record<string, Value>> = Omit< | |
Y.Map<TData>, | |
keyof SharedMapTyping<TData> | |
> & | |
SharedMapTyping<TData>; | |
export const SharedMap = Y.Map as new <TData extends Record<string, Value>>( | |
entries?: [keyof TData, TData[keyof TData]][], | |
) => SharedMap<TData>; | |
type SharedArrayTyping<T extends Value> = { | |
get(index: number): T | undefined; | |
toJSON(): ToJson<T>; | |
}; | |
export type SharedArray<T extends Value> = Omit< | |
Y.Array<T>, | |
keyof SharedArrayTyping<T> | |
> & | |
SharedArrayTyping<T>; | |
export const SharedArray = Y.Array as new <T extends Value>( | |
entries?: T[], | |
) => SharedArray<T>; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@BitPhinix Forgive me if I'm asking stupid questions, but I'm wondering why there are no
SharedText / SharedXmlText / SharedXmlElement
etc.?