Skip to content

Instantly share code, notes, and snippets.

@colelawrence
Created September 18, 2023 05:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save colelawrence/4cdab2c5f37bed4ca0ac0a3da6669902 to your computer and use it in GitHub Desktop.
Save colelawrence/4cdab2c5f37bed4ca0ac0a3da6669902 to your computer and use it in GitHub Desktop.
YJs, TypeScript, "Shared Types" with zod-like parser inference
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
import * as Y from "yjs";
export interface ZodLikeParser<T> {
_type: T;
parse: (value: unknown) => T;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyValueDesign = ZodLikeParser<any> | CollectionDef<any> | RecordDef<any>;
type AnyRawShapeDesign = Record<string, AnyValueDesign>;
const incrlen = 36 * 36;
let incr = Math.floor(Math.random() * incrlen);
const genID = (pre?: `${string}_`) =>
pre +
Date.now().toString(36).slice(0, -2) +
Math.random().toString(36).slice(2, 6).padStart(4, "0") +
(++incr % incrlen).toString(36).padStart(2, "0");
/** This is a simple value, it is synchronized by last write wins. */
export class ValueRef<T> {
constructor(
private readonly parser: ZodLikeParser<T>,
private readonly ymap: Y.Map<unknown>,
private readonly key: string
) {}
}
type InitRecord<Def extends AnyRawShapeDesign> = {
[K in keyof Def]: InitValue<Def[K]>;
};
type InitValue<Def extends AnyValueDesign> = Def extends ZodLikeParser<infer T>
? T
: Def extends CollectionDef<infer T>
? (InitRecord<T> & {
/** Set an id for this item. If it conflicts, we should throw. */
_id?: string;
})[]
: Def extends RecordDef<infer T>
? InitRecord<T>
: never;
function isParser(a: unknown): a is ZodLikeParser<unknown> {
if (
a &&
typeof a === "object" &&
"parse" in a &&
typeof a.parse === "function"
)
return true;
return false;
}
function assert<T>(x: unknown, cons: { new (): T }): T {
if (!(x instanceof cons)) {
console.error(`Expected ${cons.name}`, { expected: cons, got: x });
throw new Error(`Expected ${cons.name} but got ${x}`);
}
return x;
}
type ResolvedProp<Def extends AnyValueDesign> = Def extends ZodLikeParser<
infer T
>
? ValueRef<T>
: Def extends CollectionDef<infer T>
? CollectionRef<T>
: Def extends RecordDef<infer T>
? RecordRef<T>
: never;
/** Plain record */
export class RecordRef<
T extends AnyRawShapeDesign,
Assoc = Record<string, never>
> {
constructor(
private design: T,
private ymap: Y.Map<unknown>,
public readonly assoc: Assoc
) {}
public prop<K extends string & keyof T>(key: K): ResolvedProp<T[K]> {
const propDesign = this.design[key];
if (propDesign instanceof RecordDef) {
return new RecordRef(
propDesign.design,
assert(this.ymap.get(key), Y.Map),
{}
) as ResolvedProp<T[K]>;
} else if (propDesign instanceof CollectionDef) {
return new CollectionRef(
propDesign,
assert(this.ymap.get(key), Y.Map)
) as ResolvedProp<T[K]>;
} else if (isParser(propDesign)) {
const valueRef = new ValueRef(propDesign, this.ymap, key);
return valueRef as ResolvedProp<T[K]>;
} else {
console.error(`Unknown design type`, { design: propDesign });
throw new Error(`Unknown design type`);
}
}
}
class CollectionRef<T extends AnyRawShapeDesign> {
constructor(
private readonly def: CollectionDef<T>,
private readonly ymap: Y.Map<unknown>
) {}
public get(id: string) {
const ymap = assert(this.ymap.get(id), Y.Map);
return new RecordRef(this.def.itemDesign, ymap, { id });
}
public insert(initItem: InitRecord<T>, id: string = this.def.genID()) {
const ymap = initRecordToYMap({ design: this.def.itemDesign }, initItem);
this.ymap.set(id, ymap);
return new RecordRef(this.def.itemDesign, ymap, { id });
}
}
function initRecordToYMap<T extends AnyRawShapeDesign>(
def: { design: T },
initRecord: InitRecord<T>
): Y.Map<unknown> {
const ymap = new Y.Map();
for (const [key, init] of Object.entries(initRecord)) {
const propDesign = def.design[key];
if (propDesign instanceof RecordDef) {
ymap.set(key, initRecordToYMap(propDesign, init));
} else if (propDesign instanceof CollectionDef) {
ymap.set(key, initCollectionToYMap(propDesign, init));
} else if (isParser(propDesign)) {
ymap.set(key, propDesign.parse(init));
} else {
console.error(`Unknown design type`, { design: propDesign });
throw new Error(`Unknown design type`);
}
}
return ymap;
}
function initToValue<T extends AnyValueDesign>(
def: T,
init: InitValue<T>
): unknown {
if (def instanceof RecordDef) {
return initRecordToYMap(def, init);
} else if (def instanceof CollectionDef) {
return initCollectionToYMap(def, init);
} else if (isParser(def)) {
return def.parse(init);
} else {
console.error(`Unknown design type`, { design: def });
throw new Error(`Unknown design type`);
}
}
/** We treat arrays as dictionaries */
function initCollectionToYMap(
def: CollectionDef<AnyRawShapeDesign>,
init: InitRecord<AnyRawShapeDesign>[]
) {
const ymap = new Y.Map();
for (const { _id, ...initItem } of init) {
const id = _id ?? def.genID();
if (ymap.has(id)) {
console.error(`Duplicate id`, { id });
throw new Error(`Duplicate id (${id})`);
}
ymap.set(id, initRecordToYMap({ design: def.itemDesign }, initItem));
}
return ymap;
}
class RecordDef<T extends AnyRawShapeDesign> {
constructor(
public readonly design: T // public readonly init: () => InitRecord<T>
) {}
init(init: InitRecord<T>) {
return initRecordToYMap(this, init);
}
ref(ymap: Y.Map<unknown>) {
return new RecordRef(this.design, ymap, {});
}
}
class CollectionDef<T extends AnyRawShapeDesign> {
_itemType!: RecordRef<T, { id: string }>;
constructor(
private readonly prefix: string,
public readonly itemDesign: T // initItem: () => InitRecord<T>
) {}
public initItem(init: InitValue<RecordDef<T>>) {
return initRecordToYMap({ design: this.itemDesign }, init);
}
public ref(
ymap: Y.Map<unknown>,
id: string
): undefined | RecordRef<T, { id: string }> {
const value = ymap.get(id);
if (value === undefined) return undefined;
return new RecordRef(this.itemDesign, assert(value, Y.Map), {
id,
});
}
public genID() {
return genID(`${this.prefix}_`);
}
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace shared {
export function collection<T extends AnyRawShapeDesign>(
prefix: string,
itemShape: T
// initItem: () => InitRecord<T>
): CollectionDef<T> {
return new CollectionDef(prefix, itemShape /* initItem */);
}
export function map<T extends AnyRawShapeDesign>(design: T): RecordDef<T> {
return new RecordDef(design);
}
export function value<T>(parser: ZodLikeParser<T>): ZodLikeParser<T> {
return parser;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment