Skip to content

Instantly share code, notes, and snippets.

@BitPhinix
Last active April 24, 2024 11:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BitPhinix/a98b5f35a0be9cd8700103c8fd406d4d to your computer and use it in GitHub Desktop.
Save BitPhinix/a98b5f35a0be9cd8700103c8fd406d4d to your computer and use it in GitHub Desktop.
yjs react binding
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,
};
}
/* 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>;
@nightire
Copy link

@BitPhinix Forgive me if I'm asking stupid questions, but I'm wondering why there are no SharedText / SharedXmlText / SharedXmlElement etc.?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment