This repository has been archived by the owner on Feb 22, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Framework Blocking * Lint * Switch workspaces to glob pattern * Export transformers from @seventv/theming * Additional UI components * Target common ES2020 across TS packages * Additional asyncronous utilities * Remove old unreachable code from mediaQueryTransformer * More resiliant unique route key serialization * Move RouterView for async pages into util * Add type utilities to util * Use strict typescript in all packages * Add prototype helpers to util * Basic API Hydrator implementation * Clonability for hydrator values
- Loading branch information
Showing
82 changed files
with
3,479 additions
and
303 deletions.
There are no files selected for viewing
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
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
This package requires typescript transpiling by the end user, ensure that your typescript config is compatable with this package's `tsconfig.json`. |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{ | ||
"name": "@seventv/api", | ||
"version": "0.1.0", | ||
"scripts": { | ||
"type-check": "tsc --noEmit --composite false" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/SevenTV/WebComponents.git" | ||
}, | ||
"main": "src/index.ts", | ||
"files": [ | ||
"src/", | ||
"tsconfig.json" | ||
], | ||
"dependencies": { | ||
"typescript": "^5.1.6", | ||
"@seventv/util": "~0" | ||
}, | ||
"devDependencies": { | ||
"@tsconfig/node18": "^18.2.0", | ||
"@types/node": "^20.4.9" | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { cloneValue, hydrateValue } from "./hydration"; | ||
import type { HydratorSchema, HydratorValue } from "../types/hydrator"; | ||
import type { DefiniteKey } from "@seventv/util/modules/types"; | ||
|
||
type BaseHydratedObjectConstructor = typeof BaseHydratedObject; | ||
export class BaseHydratedObject { | ||
static SCHEMA: HydratedObjectSchema = {}; | ||
|
||
constructor(input: object | BaseHydratedObject | HydratedObjectCloningContext) { | ||
const cls = <BaseHydratedObjectConstructor>this.constructor; | ||
|
||
if (input instanceof HydratedObjectCloningContext) { | ||
cls.fromClone(this, input.source, input.seen); | ||
} else if (input instanceof cls) { | ||
cls.fromClone(this, input, new WeakMap()); | ||
} else { | ||
cls.fromJSON(this, input); | ||
} | ||
} | ||
|
||
protected static fromJSON(instance: BaseHydratedObject, json: object) { | ||
const cls = <BaseHydratedObjectConstructor>instance.constructor; | ||
|
||
for (const [prop, schema] of Object.entries(cls.SCHEMA)) { | ||
const isOwn = Object.prototype.hasOwnProperty.call(json, prop); | ||
const raw = isOwn ? Reflect.get(json, prop) : undefined; | ||
|
||
Reflect.set(instance, prop, hydrateValue(raw, schema, prop, [json])); | ||
} | ||
} | ||
|
||
protected static fromClone( | ||
instance: BaseHydratedObject, | ||
source: BaseHydratedObject, | ||
seen: WeakMap<object, object>, | ||
) { | ||
const cls = <BaseHydratedObjectConstructor>instance.constructor; | ||
|
||
seen.set(source, instance); | ||
|
||
for (const prop of Object.keys(cls.SCHEMA)) { | ||
const raw = Reflect.get(source, prop); | ||
|
||
Reflect.set(instance, prop, cloneValue(raw, seen)); | ||
} | ||
} | ||
|
||
clone() { | ||
const cls = <new (...args: unknown[]) => typeof this>this.constructor; | ||
|
||
return new cls(this); | ||
} | ||
} | ||
|
||
export class HydratedObjectCloningContext { | ||
constructor( | ||
public source: BaseHydratedObject, | ||
public seen: WeakMap<object, object>, | ||
) {} | ||
} | ||
|
||
/* Assert to TypeScript that the implementation will always define props from the schema | ||
* | ||
* It is impossible to genericly extend the final type, as that would require the compiler to know ahead of time what generic the constructor | ||
* would be instantiated with, so we export a mapper type with a generic so downstream abstract classes can also exhibit this property | ||
* This does not affect implementors, only classes which wish to "pass along" the final implementor to the constructor's generic if they are abstract | ||
*/ | ||
type HydratedObjectSchema = Record<string, HydratorSchema>; | ||
|
||
type HydratedObjectSchemaFields<S extends HydratedObjectSchema> = { | ||
[Key in keyof S as DefiniteKey<Key>]: HydratorValue<S[Key]>; | ||
}; | ||
|
||
type MappedHydratedObjectClass<Inst extends BaseHydratedObject, Impl extends BaseHydratedObjectConstructor> = Inst & | ||
HydratedObjectSchemaFields<Impl["SCHEMA"]>; | ||
|
||
export type MappedHydratedObjectConstructor< | ||
Ctor extends BaseHydratedObjectConstructor = BaseHydratedObjectConstructor, | ||
> = Ctor & { | ||
new <Impl extends Ctor>(...args: ConstructorParameters<Ctor>): MappedHydratedObjectClass<InstanceType<Ctor>, Impl>; | ||
}; | ||
|
||
export const HydratedObject = <MappedHydratedObjectConstructor>BaseHydratedObject; | ||
export type HydratedObject = InstanceType<MappedHydratedObjectConstructor>; | ||
|
||
export default HydratedObject; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
import HydratedObject, { HydratedObjectCloningContext } from "./HydratedObject"; | ||
import type { HydratedObjectConstructor, HydratorSchema, HydratorSchemaFull, HydratorValue } from "../types/hydrator"; | ||
import { isPrototypeOf } from "@seventv/util/modules/prototype"; | ||
|
||
export function hydrateValue<S extends HydratorSchema>( | ||
input: unknown, | ||
schema: S, | ||
throwPath?: string, | ||
ancestors?: unknown[], | ||
): HydratorValue<S> { | ||
const resolved = resolveHydratorSchema(schema, input, ancestors); | ||
|
||
try { | ||
const type = resolved.type; | ||
switch (typeof type) { | ||
case "string": { | ||
switch (type) { | ||
case "object": { | ||
if (typeof input !== "object" || !input) throw void 0; | ||
|
||
const hydrated = {}; | ||
const newAncestors = ancestors ? [input, ...ancestors] : [input]; | ||
|
||
if ("schema" in resolved) { | ||
for (const [prop, schema] of Object.entries(resolved.schema)) { | ||
const isOwn = Object.prototype.hasOwnProperty.call(input, prop); | ||
const raw = isOwn ? Reflect.get(input, prop) : undefined; | ||
|
||
Reflect.set(hydrated, prop, hydrateValue(raw, schema, prop, newAncestors)); | ||
} | ||
} else if ("children" in resolved) { | ||
for (const prop of Reflect.ownKeys(input)) { | ||
const raw = Reflect.get(input, prop); | ||
|
||
Reflect.set( | ||
hydrated, | ||
prop, | ||
hydrateValue(raw, resolved.children, prop.toString(), newAncestors), | ||
); | ||
} | ||
} else { | ||
throw void 0; | ||
} | ||
|
||
return <HydratorValue<S>>hydrated; | ||
} | ||
|
||
case "array": { | ||
if (input instanceof Array) { | ||
const hydrated: unknown[] = []; | ||
const newAncestors = ancestors ? [input, ...ancestors] : [input]; | ||
|
||
for (let x = 0; x < input.length; x++) { | ||
try { | ||
hydrated.push(hydrateValue(input[x], resolved.children, `[${x}]`, newAncestors)); | ||
} catch (err) { | ||
if (resolved.skipInvalid) continue; | ||
throw err; | ||
} | ||
} | ||
|
||
return <HydratorValue<S>>hydrated; | ||
} | ||
|
||
throw void 0; | ||
} | ||
|
||
case "never": { | ||
throw void 0; | ||
} | ||
|
||
case "null": { | ||
if (input === null) return <HydratorValue<S>>input; | ||
throw void 0; | ||
} | ||
|
||
default: { | ||
if (typeof input === type) return <HydratorValue<S>>input; | ||
throw void 0; | ||
} | ||
} | ||
} | ||
|
||
case "function": { | ||
if (isPrototypeOf<HydratedObjectConstructor>(HydratedObject, type)) { | ||
if (typeof input !== "object" || !input) throw void 0; | ||
|
||
return <HydratorValue<S>>new type(input); | ||
} | ||
|
||
throw void 0; | ||
} | ||
} | ||
} catch (err) { | ||
if (resolved.required === false) { | ||
if (resolved.default !== undefined) return <HydratorValue<S>>resolved.default; | ||
return <HydratorValue<S>>undefined; | ||
} | ||
|
||
if (err instanceof HydrationError) { | ||
if (throwPath) { | ||
if (err.throwPath) err.throwPath = `${throwPath}.${err.throwPath}`; | ||
else err.throwPath = throwPath; | ||
} | ||
|
||
throw err; | ||
} else { | ||
throw new HydrationError(throwPath, err); | ||
} | ||
} | ||
} | ||
|
||
export function resolveHydratorSchema( | ||
schema: HydratorSchema, | ||
current: unknown, | ||
ancestors?: unknown[], | ||
): HydratorSchemaFull { | ||
if (typeof schema == "function" && !isPrototypeOf<HydratedObjectConstructor>(HydratedObject, schema)) { | ||
schema = schema(current, ancestors ?? []); | ||
} | ||
|
||
if (typeof schema === "string" || typeof schema === "function") { | ||
return { type: schema }; | ||
} | ||
|
||
return schema; | ||
} | ||
|
||
export class HydrationError extends Error { | ||
constructor( | ||
public throwPath?: string, | ||
public subError?: unknown, | ||
) { | ||
super(); | ||
this.name = "Hydration Error"; | ||
} | ||
|
||
get message() { | ||
let msg = "Failed to parse required property"; | ||
if (this.throwPath) msg += ` at path '${this.throwPath}'`; | ||
if (this.subError instanceof Error) msg += ": " + this.subError.message; | ||
|
||
return msg; | ||
} | ||
} | ||
|
||
export function cloneValue<V extends HydratorValue>(value: V, seen?: WeakMap<object, object>): V { | ||
// Primitives always share identity | ||
if (typeof value !== "object" || value === null) return value; | ||
|
||
// Clones of objects that share identity, will share identity | ||
seen = seen ?? new WeakMap(); | ||
if (seen.has(value)) return <V>seen.get(value); | ||
|
||
if (value instanceof HydratedObject) { | ||
const cls = <HydratedObjectConstructor>value.constructor; | ||
const context = new HydratedObjectCloningContext(value, seen); | ||
|
||
return <V>new cls(context); | ||
} | ||
|
||
if (value instanceof Array) { | ||
const cloned: HydratorValue[] = []; | ||
seen.set(value, cloned); | ||
|
||
for (let x = 0; x < value.length; x++) { | ||
cloned[x] = cloneValue(value[x], seen); | ||
} | ||
|
||
return <V>cloned; | ||
} else { | ||
const cloned: Record<PropertyKey, HydratorValue> = {}; | ||
seen.set(value, cloned); | ||
|
||
for (const prop of Reflect.ownKeys(value)) { | ||
const raw = <HydratorValue>Reflect.get(value, prop); | ||
|
||
Reflect.set(cloned, prop, cloneValue(raw, seen)); | ||
} | ||
|
||
return <V>cloned; | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export { HydratedObject } from "./hydrator/HydratedObject"; | ||
|
||
export * from "./hydrator/hydration"; | ||
|
||
export type * from "./types/hydrator"; |
Oops, something went wrong.