diff --git a/src/index.d.ts b/src/index.d.ts index 16dd912..eeee84c 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -33,11 +33,24 @@ declare namespace Charm { * A function that depends on one or more atoms and produces a state. Can be * used to derive state from atoms. * - * @template State The type of the state. * @returns The current state. */ type Molecule = () => State; + /** + * Infers the type of the state produced by the given molecule. + */ + type StateOf = T extends Molecule ? State : never; + + /** + * Recursively infers the type of the state produced by a map of molecules. + */ + type StateOfMap = { + [P in keyof T]: T[P] extends Molecule ? State : never; + }; + + type AtomMap = Record>; + interface AtomOptions { /** * A function that determines whether the state has changed. By default, @@ -200,7 +213,7 @@ declare namespace Charm { * @param options The atoms to synchronize with the server. * @returns A `ClientSyncer` object. */ - client: >>(options: ClientOptions) => ClientSyncer; + client: (options: ClientOptions) => ClientSyncer; /** * Creates a `ServerSyncer` object that sends patches to the client and * hydrates the client's state. @@ -209,39 +222,58 @@ declare namespace Charm { * @param options The atoms to synchronize with the client. * @returns A `ServerSyncer` object. */ - server: >>(options: ServerOptions) => ServerSyncer; + server: (options: ServerOptions) => ServerSyncer; + /** + * Checks whether a value is `None`. If `true`, the value is scheduled to be + * removed from the state when the patch is applied. + * + * @param value The value to check. + * @returns `true` if the value is `None`, otherwise `false`. + */ + isNone: (value: unknown) => value is None; }; /** * A special value that denotes the absence of a value. Used to represent - * undefined values in patches. + * removed values in patches. */ interface None { readonly __none: "__none"; } - type SyncPatch = { - readonly [P in keyof T]?: (T[P] extends object ? SyncPatch : T[P]) | (T[P] extends undefined ? None : never); + /** + * A partial patch that can be applied to the state to update it. Represents + * the difference between the current state and the next state. + * + * If a value was removed, it is replaced with `None`. This can be checked + * using the `sync.isNone` function. + */ + type SyncPatch = { + readonly [P in keyof State]?: + | (State[P] extends object ? SyncPatch : State[P]) + | (undefined extends State[P] ? None : never); }; /** * A payload that can be sent from the server to the client to synchronize * state between the two. */ - type SyncPayload = { type: "init"; data: T } | { type: "patch"; data: SyncPatch }; + type SyncPayload = + | { type: "init"; data: StateOfMap } + | { type: "patch"; data: SyncPatch> }; - interface ClientOptions>> { + interface ClientOptions { /** * The atoms to synchronize with the server. */ - atoms: T; + atoms: Atoms; } - interface ServerOptions>> { + interface ServerOptions { /** * The atoms to synchronize with the client. */ - atoms: T; + atoms: Atoms; /** * The interval at which to send patches to the client, in seconds. * Defaults to `0` (patches are sent up to once per frame). Set to a @@ -250,17 +282,17 @@ declare namespace Charm { interval?: number; } - interface ClientSyncer>> { + interface ClientSyncer { /** * Applies a patch or initializes the state of the atoms with the given * payload from the server. * * @param payload The patch or hydration payload to apply. */ - sync(payload: SyncPayload): void; + sync(payload: SyncPayload): void; } - interface ServerSyncer>> { + interface ServerSyncer { /** * Sets up a subscription to each atom that schedules a patch to be sent to * the client whenever the state changes. When a change occurs, the `callback` @@ -272,7 +304,7 @@ declare namespace Charm { * @param callback The function to call when the state changes. * @returns A cleanup function that unsubscribes all listeners. */ - connect(callback: (player: Player, payload: SyncPayload) => void): Cleanup; + connect(callback: (player: Player, payload: SyncPayload) => void): Cleanup; /** * Hydrates the client's state with the server's state. This should be * called when a player joins the game and requires the server's state. diff --git a/src/sync/init.luau b/src/sync/init.luau index b3e4e4c..7a236f4 100644 --- a/src/sync/init.luau +++ b/src/sync/init.luau @@ -1,4 +1,5 @@ local client = require(script.client) +local patch = require(script.patch) local server = require(script.server) --[=[ @@ -8,4 +9,5 @@ local server = require(script.server) return { client = client, server = server, + isNone = patch.isNone, } diff --git a/src/sync/patch.luau b/src/sync/patch.luau index d4c9893..c06ad2b 100644 --- a/src/sync/patch.luau +++ b/src/sync/patch.luau @@ -6,6 +6,10 @@ local validate = require(script.Parent.validate) ]=] local NONE = { __none = "__none" } +local function isNone(value: any): boolean + return type(value) == "table" and value.__none == "__none" +end + local function diff(prevState: { [any]: any }, nextState: { [any]: any }) local patches = table.clone(nextState) @@ -62,6 +66,7 @@ end return { NONE = NONE, + isNone = isNone, diff = diff, apply = apply, }