1.0.4 • Published 5 years ago

fury-scheme v1.0.4

Weekly downloads
2
License
MIT
Repository
github
Last release
5 years ago

Fury-Scheme

An alternative to OOP without compromise.

Flexible: Fury-Scheme offers far more flexibility than traditional OOP solutions in terms of encapsulation, polymorphism, project structure, composition of complex objects, etc.

Efficient: Fury-Scheme removes a lot of the inefficiencies caused by OOP such as large call stacks due to excessive implementation chunking for the sake of encapsulation, unnecessary references for the sake of encapsulation, unused and unnecessary polymorphism for the sake of fulfilling the logical contracts of what an object actually implements, etc.

Explicit: Fury-Scheme encourages explicit declaration of all variants to allow for developers to get a more holistic view of the project and grants far better compile time knowledge of the state structure.

NOTE: Fury-Scheme is still in development. The docs are just rough drafts and the API is incomplete.

Setting Up

Fury-Scheme was written in TypeScript and is almost entirely compile time. As such, using Fury-Scheme as a normal JavaScript module isn't very useful. If you aren't already using TypeScript, please do. I recommend using Parcel if you're developing a web app and ts-node, if you're developing a node app.

One that is done, you can install Fury-Scheme: npm i --save fury-scheme.

Usage

Projects are split into three parts:

  1. Scheme, which defines the structure of the global state object.
  2. Modules, which define functions used to interact with the state. They can also contain constants for the configuration of polymorphic attributes.
  3. Entries, which define the initial state and startup procedure of the different run environments of the application.

Declaring a State Scheme

1. The basics: Scheme declarations are exclusively compile-time and composed of TypeScript type declaration statements (type =). Type declarations define what properties a given state instance will have:

import { WrapScheme } from "fury-scheme";
export type Scheme_MyFirstObject = WrapScheme<typeof MODULES, [], [], {
	my_first_property: number,
    the_sequel: string,
    our_property: {
        foo: HTMLButtonElement,
        bar: () => void,
        baz: boolean[]
    }
}>;

Notice the use of WrapScheme. WrapScheme is the recommended way to create valid scheme declarations succinctly with local error checking. The first argument tells the scheme declaration what modules in the package are available to it. The 2nd and 3rd are extra metadata used exclusively during compile-time. They are covered in Linking Scheme and Modules(#Linking Scheme and Modules).

2. Creating References to Other Scheme Instances: Creating references to other scheme instances is a bit different than creating one to any other object. This is because scheme declarations contain a lot of metadata that isn't part of an actual instance and as such, a wrapper was needed. In order to reference another scheme instance, we use the class SchemeInstance<Scheme_XYZ>:

import { WrapScheme, SchemeInstance } from "fury-scheme";
export type Scheme_Object1 = WrapScheme<typeof MODULES, [], [], {
    reference_1: SchemeInstance<Scheme_Object2>,
    reference_2: Record<keyof any, SchemeInstance<Scheme_Object1>>
};

export type Scheme_Object2 = WrapScheme<typeof MODULES, [], [], {
	reference_3: SchemeInstance<Scheme_Object1>[]
}>;

Creating Multiple Variants Using Generics

3. Simple generic conditions: Scheme declarations can accept generic parameters, not only to specify generic type information but also to configure the scheme in a more limited and explicit manner using TypeScript ternaries and type literals:

import { WrapScheme } from "fury-scheme";
type RuntimeType = "server" | "client";

export type Scheme_Player<Runtime extends RuntimeType> = WrapScheme<typeof MODULES, [], [], { // Shared state
    name: string,
    position: [number, number]
}> & (Runtime extends "client" ? WrapScheme<typeof MODULES, [], [], {
    animation_state: { /* ... */ },
    color: string
}> : {}) & (Runtime extends "server" ? WrapScheme<typeof MODULES, [], [], {
    /* ... */
}> : {});

export type Scheme_Bomb<IsDud extends boolean> = WrapScheme<typeof MODULES, [], [], {
   	is_dud: IsDud,
    position: [number, number],
    explosion_timer: number
}> & (IsDud extends false ? WrapScheme<typeof MODULES, [], [], {
    explosion_radius: number
}> : {});

We can use {} as the fallback when nothing gets added to the scheme because, under the hood, WrapScheme converts the generic parameters into an object. If we were to type, WrapScheme<typeof MODULES, [], [], {}> instead of {}, no properties would be added and any added metadata would have no effect on the behavior of the scheme. Also note, while RuntimeType is a union of string literals, other types of literals can be used, most notably Symbols and numbers. Symbols become a very helpful optimization tool when the same enum values are stored during runtime, taking up less memory and being faster to compare. TypeScript enums work as well however they add a bit of extra runtime data that might not be particularly useful, especially if the enum is used exclusively at compile time.

4. Multiple truthy ternary values: Ternaries can treat multiple input values as truthy using unions:

// TODO: Replace "..." with example properties 
import { WrapScheme } from "fury-scheme";

type RuntimeModes = "server-dedicated" | "client-host" | "client-puppet" | "editor";
type RuntimeModes_Client = "client-host" | "client-puppet";
type RuntimeModes_Server = "server-dedicated" | "client-host";

export type Scheme_Player<Runtime extends RuntimeType> = WrapScheme<typeof MODULES, [], [], {
    /* ... */
}> & (Runtime extends RuntimeModes_Client ? WrapScheme<typeof MODULES, [], [], {
    /* ... */
}> : {}) & (Runtime extends RuntimeModes_Client | "editor" ? WrapScheme<typeof MODULES, [], [], {
    /* ... */
}> : {}) & (Runtime extends "editor" ? WrapScheme<typeof MODULES, [], [], {
    /* ... */
}> : {});

5. Multiple condition ternaries: Ternaries can check for multiple conditions using tuple inheritance checks:

// TODO: Replace "..." with example properties
import { WrapScheme } from "fury-scheme";

type RuntimeModes = "server-dedicated" | "client-host" | "client-puppet" | "editor";
type RuntimeModes_Client = "client-host" | "client-puppet";
type RuntimeModes_Server = "server-dedicated" | "client-host";

export type Scheme_Bomb<Runtime extends RuntimeModes, IsDud extends boolean> = WrapScheme<typeof MODULES, [], [], {
    /* ... */
}> & ([Runtime, IsDud] extends [RuntimeModes_Client | "editor", true] ? WrapScheme<typeof MODULES, [], [], {
    /* ... */
}> : {}) & ([Runtime, IsDud] extends ["editor", false] ? WrapScheme<typeof MODULES, [], [], {
    /* ... */
}> : {});

TODO: Ternary switch statements.

7. Generic context types: In order to simplify generic signatures, compile-time exclusive context types can be used:

import { WrapScheme, SchemeInstance } from "fury-scheme";
type RuntimeModes = "server-dedicated" | "client-host" | "client-puppet" | "editor";
type RuntimeModes_Client = "client-host" | "client-puppet";
type RuntimeModes_Server = "server-dedicated" | "client-host";
type GameMode = "classic" | "deathmatch";

type GameConfiguration = { runtime: RuntimeModes } & (
	{ game_mode: "classic" }
	| { game_mode: "deathmatch", weapon_roster: "classic" | "expanded" }
);

export type Scheme_World<Ctx extends GameConfiguration> = WrapScheme<typeof MODULES, [], [], {
    config: { game_max_time: number } & {
        classic: {
            mode: "classic",
            round_count: number
        },
        deathmatch: {
            mode: "deathmatch",
            kill_goal: number
        }
    }[GameConfiguration["game_mode"]]
}>;

export type Scheme_Player<Ctx extends GameConfiguration, SomethingElse extends boolean> = WrapScheme<typeof MODULES, [], [], {
    name: string,
    health: number
}> & (Ctx["game_mode"] extends "classic" ? WrapScheme<typeof MODULES, [], [], {
    team: "blue" | "red"
}> : {}) & (Ctx["runtime"] extends RuntimeModes_Server ? WrapScheme<typeof MODULES, [], [], {
    anticheat_warnings: SchemeInstance<Scheme_AnticheatWarning<Ctx, "some argument">>[]
}> : {}) & ([Ctx, SomethingElse] extends [{ runtime: RuntimeModes_Server, game_mode: "deathmatch", weapon_roster: any}, false] ? WrapScheme<typeof MODULES, [], [], {
    damage_dealers: SchemeInstance<Scheme_Player<Ctx, SomethingElse>>[]
}> : {});

8. Context type variants: Sometimes, it is necessary to limit an entire scheme instance to only specific context type variants. Most of the techniques used to create multiple variants of schemes are applicable here too:

import { WrapScheme, UnionIntersection } from "fury-scheme";
type RuntimeModes = "server-dedicated" | "client-host" | "client-puppet" | "editor";
type RuntimeModes_Client = "client-host" | "client-puppet";
type RuntimeModes_Server = "server-dedicated" | "client-host";
type GameMode = "classic" | "deathmatch";

type GameConfiguration<ApplicableRuntimes = RuntimeModes> = (
		{ runtime: UnionIntersection<RuntimeModes_Server, ApplicableRuntimes>, anticheat_allows_reporting: boolean }
		| { runtime: UnionIntersection<RuntimeModes_Client, ApplicableRuntimes> }
		| { runtime: "editor", not_readonly: boolean }
	) & (
	{ game_mode: "classic" } |
	{ game_mode: "deathmatch", weapon_roster: "classic" | "expanded" }
);

export type Scheme_AnticheatWarning<Ctx extends GameConfiguration<RuntimeModes_Server>> = WrapScheme<typeof MODULES, [], [], {
    case_id: string,
    cheat_id: "no-fall" | "blink" | "timer" | "fly" | "kill-aura"
}> & (Ctx["anticheat_allows_reporting"] extends true ? WrapScheme<typeof MODULES, [], [], {
    reporter_uuid: string,
    report_reason: string,
    report_time: number
}> : {});

9. Using Boolean Algebra Helpers: Fury-Scheme comes a set of helpers which allow for more succinct variant composition through conditions:

// TODO: Show off "ConditionallyCompose" and related.

Combining Types

9. Flat type composition: Fury-Scheme allows "multiple inheritance" through TypeScript type unions. This works well in Fury-Scheme because polymorphic method implementations aren't linked to the scheme but rather to an explicit polymorphic behavior configuration object passed at each polymorphic method invocation. As such, the ambiguities of polymorphism with multiple inheritance disappear. See more at Accepting Multiple Different Absolute Types.

import { WrapScheme, SchemeInstance } from "fury-scheme";
type RuntimeModes = "server-dedicated" | "client-host" | "client-puppet" | "editor";
type RuntimeModes_Client = "client-host" | "client-puppet";
type RuntimeModes_Server = "server-dedicated" | "client-host";

export type Scheme_Particles<Runtime extends RuntimeModes> = Runtime extends RuntimeModes_Client ? WrapScheme<typeof MODULES, [], [], {
    particles: SchemeInstance<SchemeH_Particles<Runtime>>
}> : {};

export type Scheme_Explosive<Runtime extends RuntimeModes> = WrapScheme<typeof MODULES, [], [], {
    ticks_left: number,
    is_active: boolean,
    explosion_seed: number
}> & Scheme_Particles<Runtime>;

export type Scheme_ProjectileBase<Runtime extends RuntimeModes> = WrapScheme<typeof MODULES, [], [], {
    direction: [number, number],
    air_time: number
}>  & Scheme_Particles<Runtime>;

export type Scheme_ProjectileNormal<Runtime extends RuntimeModes> = Scheme_ProjectileBase<Runtime> & WrapScheme<typeof MODULES, [], [], {
    draw_damage_multiplier: number
}>;

export type Scheme_ProjectileExplosive<Runtime extends RuntimeModes> = Scheme_ProjectileBase<Runtime> & Scheme_Explosive<Runtime>;

export type Scheme_Bomb<Runtime extends RuntimeModes> = Scheme_Explosive<Runtime> & WrapScheme<typeof MODULES, [], [], {
    detonator: Scheme_Instance<Scheme_Player<Runtime>>
}>;

TODO: Required siblings

TODO: Using symbol identified properties to avoid property name conflicts

Differentiating Types

TODO: Literal identified type unions

Implementing Modules

TODO: Declaring modules

TODO: Constructing scheme instances

Accepting Scheme Instances

TODO: Absolute types

TODO: Base types

TODO: Using generics to enforce type consistency

Accepting Multiple Different Absolute Types

TODO: Naïve if statements

TODO: Polymorphic configuration records

Dealing with static data

TODO: As an argument

TODO: Polymorphic

Working with Abstraction

TODO: Passing polymorphic configuration objects

TODO: Adding typed handlers

TODO: Required minimum types based on a configuration

Linking Scheme and Modules

TODO: Relevant modules

TODO: Encapsulation

Best Practices

TODO

API Reference

TODO

Examples

TODO

Contributing

TODO

License

TODO

1.0.4

5 years ago

1.0.3

5 years ago

1.0.2

5 years ago

1.0.1

5 years ago

1.0.0

5 years ago