Instantly share code, notes, and snippets.
Created
September 18, 2023 05:44
-
Save colelawrence/4cdab2c5f37bed4ca0ac0a3da6669902 to your computer and use it in GitHub Desktop.
YJs, TypeScript, "Shared Types" with zod-like parser inference
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/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