mirror of
https://github.com/littensy/charm.git
synced 2026-10-23 05:46:27 +00:00
refactor: finalize updates
This commit is contained in:
parent
6dfcaa5cfa
commit
a857797465
28 changed files with 300 additions and 240 deletions
|
|
@ -55,11 +55,13 @@ Charm = "littensy/charm@VERSION"
|
|||
|
||||
#### `subscribe(atom, listener)`
|
||||
|
||||
#### `derive(atom, comparator?)`
|
||||
#### `derive(atom, equals?)`
|
||||
|
||||
#### `observe(atom, observer)`
|
||||
|
||||
#### `mapAtom(atom, mapper)`
|
||||
#### `map(atom, mapper)`
|
||||
|
||||
#### `unwrap(atom)`
|
||||
|
||||
### React
|
||||
|
||||
|
|
@ -77,7 +79,7 @@ Charm = "littensy/charm@VERSION"
|
|||
|
||||
#### `sync.server(options)`
|
||||
|
||||
#### `Server.onSync(callback)`
|
||||
#### `Server.connect(callback)`
|
||||
|
||||
#### `Server.hydrate(player)`
|
||||
|
||||
|
|
|
|||
|
|
@ -42,4 +42,18 @@ return function()
|
|||
expect(outer).to.equal(2)
|
||||
expect(inner).to.equal(1)
|
||||
end)
|
||||
|
||||
it("takes a custom comparator", function()
|
||||
local max = atom(1, {
|
||||
equals = function(prev, next)
|
||||
return next < prev
|
||||
end,
|
||||
})
|
||||
max(2)
|
||||
expect(max()).to.equal(2)
|
||||
max(1)
|
||||
expect(max()).to.equal(2)
|
||||
max(3)
|
||||
expect(max()).to.equal(3)
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,9 +29,11 @@ local benchmarks = {
|
|||
derived_memo = function(_, source: any)
|
||||
derive(function()
|
||||
return { count = 2 * source().count }
|
||||
end, function(previous, current)
|
||||
end, {
|
||||
equals = function(previous, current)
|
||||
return previous.count == current.count
|
||||
end)
|
||||
end,
|
||||
})
|
||||
for _ = 1, 500 do
|
||||
source({ count = math.random(1, 2) })
|
||||
end
|
||||
|
|
|
|||
22
src/__tests__/capture.spec.luau
Normal file
22
src/__tests__/capture.spec.luau
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local capture = require(script.Parent.Parent.capture)
|
||||
|
||||
return function()
|
||||
it("captures atoms from a function", function()
|
||||
local a, b, c = atom(1), atom(1), atom(2)
|
||||
local captured, result = capture(function()
|
||||
return a() + b()
|
||||
end)
|
||||
expect(captured[a]).to.be.ok()
|
||||
expect(captured[b]).to.be.ok()
|
||||
expect(captured[c]).to.never.be.ok()
|
||||
expect(result).to.equal(2)
|
||||
end)
|
||||
|
||||
it("captures an atom by itself", function()
|
||||
local a = atom(1)
|
||||
local captured, result = capture(a)
|
||||
expect(captured[a]).to.be.ok()
|
||||
expect(result).to.equal(1)
|
||||
end)
|
||||
end
|
||||
|
|
@ -22,19 +22,6 @@ return function()
|
|||
expect(calls).to.equal(3)
|
||||
end)
|
||||
|
||||
it("takes a custom comparator", function()
|
||||
local source = atom(1)
|
||||
local derived = derive(function()
|
||||
return source()
|
||||
end, function(prev, next)
|
||||
return next < prev
|
||||
end)
|
||||
source(2)
|
||||
expect(derived()).to.equal(2)
|
||||
source(1)
|
||||
expect(derived()).to.equal(2)
|
||||
end)
|
||||
|
||||
it("can be nested", function()
|
||||
local source = atom(1)
|
||||
local double = derive(function()
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local mapAtom = require(script.Parent.Parent.mapAtom)
|
||||
local map = require(script.Parent.Parent.map)
|
||||
local observe = require(script.Parent.Parent.observe)
|
||||
local store = require(script.Parent.Parent.store)
|
||||
|
||||
return function()
|
||||
it("returns mapped atoms", function()
|
||||
local itemsAtom = atom({ "a", "b", "c" })
|
||||
local mappedAtom = mapAtom(itemsAtom, function(value, key)
|
||||
local mappedAtom = map(itemsAtom, function(value, key)
|
||||
return string.upper(value), key
|
||||
end)
|
||||
local updates = 0
|
||||
|
|
@ -35,7 +35,7 @@ return function()
|
|||
|
||||
it("uses key extractor", function()
|
||||
local itemsAtom = atom({ { key = 1 }, { key = 2 }, { key = 3 } })
|
||||
local mappedAtom = mapAtom(itemsAtom, function(item)
|
||||
local mappedAtom = map(itemsAtom, function(item)
|
||||
return item, item.key
|
||||
end)
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ return function()
|
|||
{ key = 3, value = "c" },
|
||||
})
|
||||
|
||||
local mappedAtom = mapAtom(itemsAtom, function(item)
|
||||
local mappedAtom = map(itemsAtom, function(item)
|
||||
return item, item.key
|
||||
end)
|
||||
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
local atom = require(script.Parent.Parent.Parent.atom)
|
||||
local sync = require(script.Parent.Parent.Parent.sync)
|
||||
local types = require(script.Parent.Parent.Parent.types)
|
||||
|
||||
return function()
|
||||
local target: Player = {} :: any
|
||||
|
|
@ -7,9 +8,9 @@ return function()
|
|||
it("sends state to new players", function()
|
||||
local a = atom(1)
|
||||
local server = sync.server({ atoms = { a = a }, interval = -1 })
|
||||
local player: Player, payload: sync.SyncPayload
|
||||
local player: Player, payload: types.SyncPayload
|
||||
|
||||
server:onSync(function(...)
|
||||
local disconnect = server:connect(function(...)
|
||||
player, payload = ...
|
||||
end)
|
||||
|
||||
|
|
@ -18,14 +19,16 @@ return function()
|
|||
expect(player).to.equal(target)
|
||||
expect(payload.type).to.equal("set")
|
||||
expect(payload.data.a).to.equal(1)
|
||||
|
||||
disconnect()
|
||||
end)
|
||||
|
||||
it("sends state patches", function()
|
||||
local a = atom({ b = 1, c = 2 })
|
||||
local server = sync.server({ atoms = { a = a }, interval = -1 })
|
||||
local player: Player, payload: sync.SyncPayload
|
||||
local player: Player, payload: types.SyncPayload
|
||||
|
||||
server:onSync(function(...)
|
||||
local disconnect = server:connect(function(...)
|
||||
player, payload = ...
|
||||
end)
|
||||
|
||||
|
|
@ -36,5 +39,7 @@ return function()
|
|||
expect(payload.type).to.equal("patch")
|
||||
expect(payload.data.a.b).to.never.be.ok()
|
||||
expect(payload.data.a.c).to.equal(5)
|
||||
|
||||
disconnect()
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
23
src/__tests__/unwrap.spec.luau
Normal file
23
src/__tests__/unwrap.spec.luau
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local capture = require(script.Parent.Parent.capture)
|
||||
local unwrap = require(script.Parent.Parent.unwrap)
|
||||
|
||||
return function()
|
||||
it("calls an atom without capturing it", function()
|
||||
local a, b, c = atom(1), atom(1), atom(1)
|
||||
local captured, result = capture(function()
|
||||
return a() + unwrap(b) + c()
|
||||
end)
|
||||
expect(captured[a]).to.be.ok()
|
||||
expect(captured[b]).to.never.be.ok()
|
||||
expect(captured[c]).to.be.ok()
|
||||
expect(result).to.equal(3)
|
||||
end)
|
||||
|
||||
it("passes through arguments", function()
|
||||
expect(unwrap(1)).to.equal(1)
|
||||
expect(unwrap(function()
|
||||
return 1
|
||||
end)).to.equal(1)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type AtomOptions<T> = types.AtomOptions<T>
|
||||
|
||||
local function atom<T>(state: T, options: AtomOptions<T>?): Atom<T>
|
||||
local equals = options and options.equals
|
||||
|
||||
local function atom<T>(state: T): Atom<T>
|
||||
local function atom(...)
|
||||
if select("#", ...) == 0 then
|
||||
if store.shouldCapture() then
|
||||
|
|
@ -14,7 +17,7 @@ local function atom<T>(state: T): Atom<T>
|
|||
|
||||
local nextState = if type(...) == "function" then (...)(state) else ...
|
||||
|
||||
if state ~= nextState then
|
||||
if state ~= nextState and not (equals and equals(state, nextState)) then
|
||||
state = nextState
|
||||
|
||||
for listener in pairs(table.clone(store.listeners[atom])) do
|
||||
|
|
|
|||
25
src/capture.luau
Normal file
25
src/capture.luau
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type Source<T> = types.Source<T>
|
||||
|
||||
local function capture<T>(source: Source<T>): ({ [Atom<any>]: true }, T)
|
||||
if store.listeners[source] then
|
||||
return { [source] = true }, source()
|
||||
end
|
||||
|
||||
store.setCapturing(true)
|
||||
local success, result = pcall(source)
|
||||
store.setCapturing(false)
|
||||
|
||||
local dependencies = table.clone(store.captured)
|
||||
table.clear(store.captured)
|
||||
|
||||
if not success then
|
||||
warn("An error occurred while calling an atom:", result)
|
||||
end
|
||||
|
||||
return dependencies, result
|
||||
end
|
||||
|
||||
return capture
|
||||
|
|
@ -1,24 +1,19 @@
|
|||
local atom = require(script.Parent.atom)
|
||||
local capture = require(script.Parent.capture)
|
||||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type ReadonlyAtom<T> = types.ReadonlyAtom<T>
|
||||
type AtomOptions<T> = types.AtomOptions<T>
|
||||
type Source<T> = types.Source<T>
|
||||
|
||||
local function derive<T>(source: ReadonlyAtom<T>, comparator: ((old: T, new: T) -> boolean)?): ReadonlyAtom<T>
|
||||
local atoms, state = store.capture(source)
|
||||
local derived = atom(state)
|
||||
local function derive<T>(source: Source<T>, options: AtomOptions<T>?): Source<T>
|
||||
local atoms, state = capture(source)
|
||||
local derived = atom(state, options)
|
||||
local derivedRef = setmetatable({ current = derived }, { __mode = "v" })
|
||||
|
||||
local function listener()
|
||||
if not derivedRef.current then
|
||||
return
|
||||
end
|
||||
|
||||
local nextState = source()
|
||||
|
||||
if state ~= nextState and not (comparator and comparator(state, nextState)) then
|
||||
state = nextState
|
||||
derivedRef.current(nextState)
|
||||
if derivedRef.current then
|
||||
derivedRef.current(source())
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
local capture = require(script.Parent.capture)
|
||||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
type ReadonlyAtom<T> = types.ReadonlyAtom<T>
|
||||
|
||||
local function effect(source: ReadonlyAtom<nil>): () -> ()
|
||||
local atoms = store.capture(source)
|
||||
local function effect(source: () -> nil): () -> ()
|
||||
local atoms = capture(source)
|
||||
|
||||
local function listener()
|
||||
source()
|
||||
|
|
|
|||
138
src/index.d.ts
vendored
138
src/index.d.ts
vendored
|
|
@ -1,117 +1,109 @@
|
|||
export = Charm;
|
||||
export as namespace Charm;
|
||||
|
||||
type Cleanup = () => void;
|
||||
|
||||
type AnyMap<K, V> =
|
||||
| Map<K, V>
|
||||
| ReadonlyMap<K, V>
|
||||
| (K extends string | number | symbol ? { readonly [Key in K]: V } : never);
|
||||
|
||||
declare namespace Charm {
|
||||
interface Atom<T> extends ReadonlyAtom<T> {
|
||||
interface Atom<T> extends Source<T> {
|
||||
readonly __nominal: unique symbol;
|
||||
(state: T | ((state: T) => T)): T;
|
||||
(state: T | ((prev: T) => T)): void;
|
||||
}
|
||||
|
||||
interface ReadonlyAtom<T> {
|
||||
(): T;
|
||||
type Source<T> = () => T;
|
||||
|
||||
interface AtomOptions<State> {
|
||||
equals?: (prev: State, next: State) => boolean;
|
||||
}
|
||||
|
||||
type AtomArrayToStates<Atoms extends ReadonlyAtom<any>[]> = {
|
||||
[Key in keyof Atoms]: Atoms[Key] extends ReadonlyAtom<infer State> ? State : never;
|
||||
};
|
||||
|
||||
function atom<State>(state: State): Atom<State>;
|
||||
function atom<State>(state: State, options?: AtomOptions<State>): Atom<State>;
|
||||
|
||||
function isAtom(value: unknown): value is Atom<any>;
|
||||
|
||||
function derive<State>(
|
||||
atom: ReadonlyAtom<State>,
|
||||
comparator?: (previous: State, current: State) => boolean,
|
||||
): ReadonlyAtom<State>;
|
||||
function derive<State>(atom: Source<State>, options?: AtomOptions<State>): Source<State>;
|
||||
|
||||
function subscribe<State>(
|
||||
atom: ReadonlyAtom<State>,
|
||||
callback: (state: State, previousState: State) => void,
|
||||
): () => void;
|
||||
function subscribe<State>(atom: Source<State>, callback: (state: State, prev: State) => void): Cleanup;
|
||||
|
||||
function effect(callback: () => void): () => void;
|
||||
function effect(callback: Source<void>): Cleanup;
|
||||
|
||||
function observe<K, V>(
|
||||
atom: ReadonlyAtom<Map<K, V> | ReadonlyMap<K, V>>,
|
||||
factory: (value: V, key: K) => (() => void) | void,
|
||||
): () => void;
|
||||
function unwrap<State>(atom: State | Source<State>): State;
|
||||
|
||||
function observe(atom: ReadonlyAtom<any>, factory: (value: unknown, key: unknown) => (() => void) | void): () => void;
|
||||
function capture<State>(atom: Source<State>): LuaTuple<[captured: Set<Atom<unknown>>, state: State]>;
|
||||
|
||||
function mapAtom<V0, K1, V1>(
|
||||
atom: ReadonlyAtom<readonly V0[]> | ReadonlyAtom<V0[]>,
|
||||
mapper: (value: V0, index: number) => LuaTuple<[V1 | undefined, K1]>,
|
||||
): ReadonlyAtom<ReadonlyMap<K1, V1>>;
|
||||
function observe<Item>(
|
||||
atom: Source<readonly Item[]>,
|
||||
factory: (item: Item, index: number) => Cleanup | void,
|
||||
): Cleanup;
|
||||
|
||||
function mapAtom<V0, V1>(
|
||||
atom: ReadonlyAtom<readonly V0[]> | ReadonlyAtom<V0[]>,
|
||||
mapper: (value: V0, index: number) => V1,
|
||||
): ReadonlyAtom<readonly V1[]>;
|
||||
function observe<Key, Item>(
|
||||
atom: Source<AnyMap<Key, Item>>,
|
||||
factory: (item: Item, key: Key) => Cleanup | void,
|
||||
): Cleanup;
|
||||
|
||||
function mapAtom<K0, V0, K1 = K0, V1 = V0>(
|
||||
atom: ReadonlyAtom<ReadonlyMap<K0, V0>> | ReadonlyAtom<Map<K0, V0>>,
|
||||
mapper: (value: V0, key: K0) => LuaTuple<[V1 | undefined, K1]> | V1,
|
||||
): ReadonlyAtom<ReadonlyMap<K1, V1>>;
|
||||
function map<V0, K1, V1>(
|
||||
atom: Source<readonly V0[]>,
|
||||
mapper: (value: V0, index: number) => LuaTuple<[value: V1 | undefined, key: K1]>,
|
||||
): Source<ReadonlyMap<K1, V1>>;
|
||||
|
||||
function mapAtom<K0 extends string | number | symbol, V0, K1, V1>(
|
||||
atom: ReadonlyAtom<{ readonly [K in K0]: V0 }>,
|
||||
mapper: (value: V0, key: K0) => LuaTuple<[V1 | undefined, K1]> | V1,
|
||||
): ReadonlyAtom<ReadonlyMap<K1, V1>>;
|
||||
function map<V0, V1>(atom: Source<readonly V0[]>, mapper: (value: V0, index: number) => V1): Source<readonly V1[]>;
|
||||
|
||||
function useAtomState<State>(atom: ReadonlyAtom<State>): State;
|
||||
function map<K0, V0, K1 = K0, V1 = V0>(
|
||||
atom: Source<AnyMap<K0, V0>>,
|
||||
mapper: (value: V0, key: K0) => LuaTuple<[value: V1 | undefined, key: K1]> | V1,
|
||||
): Source<ReadonlyMap<K1, V1>>;
|
||||
|
||||
function useAtomState<State, Result>(atom: ReadonlyAtom<State>, selector: (state: State) => Result): Result;
|
||||
// React
|
||||
|
||||
function useSetAtom<State>(atom: ReadonlyAtom<State>): (state: State | ((previous: State) => State)) => void;
|
||||
function useAtomState<State>(atom: Source<State>): State;
|
||||
|
||||
function useAtomState<State, Result>(atom: Source<State>, selector: (state: State) => Result): Result;
|
||||
|
||||
function useSetAtom<State>(atom: Source<State>): (state: State | ((prev: State) => State)) => void;
|
||||
|
||||
function useAtom<State>(
|
||||
atom: ReadonlyAtom<State>,
|
||||
): LuaTuple<[state: State, setState: (state: State | ((previous: State) => State)) => void]>;
|
||||
atom: Source<State>,
|
||||
): LuaTuple<[state: State, setState: (state: State | ((prev: State) => State)) => void]>;
|
||||
|
||||
function useAtom<State, Result>(
|
||||
atom: ReadonlyAtom<State>,
|
||||
atom: Source<State>,
|
||||
selector: (state: State) => Result,
|
||||
): LuaTuple<[state: Result, setState: (state: Result | ((previous: Result) => Result)) => void]>;
|
||||
): LuaTuple<[state: Result, setState: (state: Result | ((prev: Result) => Result)) => void]>;
|
||||
|
||||
interface None {
|
||||
readonly __none: "__none";
|
||||
}
|
||||
// Sync
|
||||
|
||||
type StatePatch<States> = {
|
||||
readonly [K in keyof States]?: (States[K] extends object ? StatePatch<States[K]> : States[K]) | None;
|
||||
const sync: {
|
||||
client(options: ClientOptions): ClientSyncer;
|
||||
server(options: ServerOptions): ServerSyncer;
|
||||
};
|
||||
|
||||
type SyncPayload<States> = { type: "set"; data: States } | { type: "patch"; data: StatePatch<States> };
|
||||
|
||||
interface AtomMap {
|
||||
readonly [key: string]: Atom<any>;
|
||||
interface SyncPatch {
|
||||
[key: string | number | symbol]: SyncPatch | { __none: "__none" } | string | number | boolean | undefined;
|
||||
}
|
||||
|
||||
type AtomMapToStates<Atoms extends AtomMap> = {
|
||||
readonly [Key in keyof Atoms]: Atoms[Key] extends Atom<infer State> ? State : never;
|
||||
};
|
||||
|
||||
interface ClientSyncOptions<Atoms extends AtomMap> {
|
||||
atoms: Atoms;
|
||||
interface SyncPayload {
|
||||
type: "set" | "patch";
|
||||
data: SyncPatch;
|
||||
}
|
||||
|
||||
interface ClientSyncer<States> {
|
||||
sync(payload: SyncPayload<States>): void;
|
||||
interface ClientOptions {
|
||||
atoms: Record<string, Atom<any>>;
|
||||
}
|
||||
|
||||
interface ServerSyncOptions<Atoms extends AtomMap> {
|
||||
atoms: Atoms;
|
||||
interface ServerOptions {
|
||||
atoms: Record<string, Atom<any>>;
|
||||
interval?: number;
|
||||
}
|
||||
|
||||
interface ServerSyncer<States> {
|
||||
onSync(callback: (player: Player, payload: SyncPayload<States>) => void): () => void;
|
||||
interface ClientSyncer {
|
||||
sync(payload: SyncPayload): void;
|
||||
}
|
||||
|
||||
interface ServerSyncer {
|
||||
connect(callback: (player: Player, payload: SyncPayload) => void): Cleanup;
|
||||
hydrate(player: Player): void;
|
||||
}
|
||||
|
||||
namespace sync {
|
||||
function client<Atoms extends AtomMap>(options: ClientSyncOptions<Atoms>): ClientSyncer<AtomMapToStates<Atoms>>;
|
||||
|
||||
function server<Atoms extends AtomMap>(options: ServerSyncOptions<Atoms>): ServerSyncer<AtomMapToStates<Atoms>>;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
_G[script.modules.Promise] = _G[script]
|
||||
|
||||
local atom = require(script.atom)
|
||||
local capture = require(script.capture)
|
||||
local derive = require(script.derive)
|
||||
local effect = require(script.effect)
|
||||
local isAtom = require(script.isAtom)
|
||||
local mapAtom = require(script.mapAtom)
|
||||
local map = require(script.map)
|
||||
local observe = require(script.observe)
|
||||
local useAtom = require(script.react.useAtom)
|
||||
local useAtomState = require(script.react.useAtomState)
|
||||
|
|
@ -12,18 +13,22 @@ local useSetAtom = require(script.react.useSetAtom)
|
|||
local subscribe = require(script.subscribe)
|
||||
local sync = require(script.sync)
|
||||
local types = require(script.types)
|
||||
local unwrap = require(script.unwrap)
|
||||
|
||||
export type Atom<State> = types.Atom<State>
|
||||
export type ReadonlyAtom<State> = types.ReadonlyAtom<State>
|
||||
export type Source<State> = types.Source<State>
|
||||
export type SyncPayload = types.SyncPayload
|
||||
|
||||
return {
|
||||
atom = atom,
|
||||
derive = derive,
|
||||
effect = effect,
|
||||
isAtom = isAtom,
|
||||
mapAtom = mapAtom,
|
||||
map = map,
|
||||
observe = observe,
|
||||
subscribe = subscribe,
|
||||
unwrap = unwrap,
|
||||
capture = capture,
|
||||
sync = sync,
|
||||
useAtom = useAtom,
|
||||
useAtomState = useAtomState,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
local store = require(script.Parent.store)
|
||||
|
||||
local function isAtom(value: any)
|
||||
return type(value) == "function" and store.listeners[value] :: any ~= nil
|
||||
return value ~= nil and store.listeners[value] :: any ~= nil
|
||||
end
|
||||
|
||||
return isAtom
|
||||
|
|
|
|||
|
|
@ -2,12 +2,17 @@ local atom = require(script.Parent.atom)
|
|||
local subscribe = require(script.Parent.subscribe)
|
||||
local types = require(script.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type ReadonlyAtom<T> = types.ReadonlyAtom<T>
|
||||
type Source<T> = types.Source<T>
|
||||
|
||||
local function mapAtom<K0, V0, K1, V1>(itemsAtom: Atom<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)): ReadonlyAtom<{ [K1]: V1 }>
|
||||
type Map =
|
||||
(<K0, V0, K1, V1>(atom: Source<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1)) -> Source<{ [K1]: V1 }>)
|
||||
& (<K0, V0, V1>(atom: Source<{ [K0]: V0 }>, mapper: (V0, K0) -> V1?) -> Source<{ [K0]: V1 }>)
|
||||
& (<K0, V0, K1, V1>(atom: Source<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)) -> Source<{ [K1]: V1 }>)
|
||||
|
||||
local function map<K0, V0, K1, V1>(itemsAtom: Source<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)): Source<{ [K1]: V1 }>
|
||||
local mappedAtom = atom({})
|
||||
local mappedAtomRef = setmetatable({ current = mappedAtom }, { __mode = "v" })
|
||||
local previousMappedItems: { [K1]: V1 } = {}
|
||||
local prevMappedItems: { [K1]: V1 } = {}
|
||||
local unsubscribe
|
||||
|
||||
local function listener(items: { [K0]: V0 })
|
||||
|
|
@ -30,13 +35,13 @@ local function mapAtom<K0, V0, K1, V1>(itemsAtom: Atom<{ [K0]: V0 }>, mapper: (V
|
|||
end
|
||||
end
|
||||
|
||||
for key in previousMappedItems do
|
||||
if mappedKeys[key] == nil and mappedItems[key] == previousMappedItems[key] then
|
||||
for key in prevMappedItems do
|
||||
if mappedKeys[key] == nil and mappedItems[key] == prevMappedItems[key] then
|
||||
mappedItems[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
previousMappedItems = mappedItems
|
||||
prevMappedItems = mappedItems
|
||||
mappedAtom(mappedItems)
|
||||
end
|
||||
|
||||
|
|
@ -46,4 +51,4 @@ local function mapAtom<K0, V0, K1, V1>(itemsAtom: Atom<{ [K0]: V0 }>, mapper: (V
|
|||
return mappedAtom
|
||||
end
|
||||
|
||||
return mapAtom
|
||||
return map :: Map
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
local effect = require(script.Parent.effect)
|
||||
local types = require(script.Parent.types)
|
||||
type ReadonlyAtom<T> = types.ReadonlyAtom<T>
|
||||
type Source<T> = types.Source<T>
|
||||
|
||||
local function observe<V, K>(source: ReadonlyAtom<{ [K]: V }>, factory: (value: V, key: K) -> () -> ()): () -> ()
|
||||
local function observe<K, V>(source: Source<{ [K]: V }>, factory: (value: V, key: K) -> () -> ()): () -> ()
|
||||
local connections: { [K]: () -> () } = {}
|
||||
|
||||
local unsubscribe = effect(function()
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ type Atom<T> = types.Atom<T>
|
|||
export type SetState<T> = (T | ((T) -> T)) -> ()
|
||||
|
||||
local function useSetAtom<State>(atom: Atom<State>): SetState<State>
|
||||
return requireReact().useCallback(function(value)
|
||||
atom(value)
|
||||
return requireReact().useCallback(function(state: State | ((State) -> State)): ()
|
||||
atom(state)
|
||||
end, { atom })
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,14 @@
|
|||
local types = require(script.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type ReadonlyAtom<T> = types.ReadonlyAtom<T>
|
||||
type Source<T> = types.Source<T>
|
||||
type WeakMap<K, V> = types.WeakMap<K, V>
|
||||
|
||||
local listeners: WeakMap<Atom<any>, WeakMap<() -> (), unknown>> = setmetatable({}, { __mode = "k" })
|
||||
local captured: { [Atom<any>]: true } = {}
|
||||
local capturing = false
|
||||
|
||||
local function capture<T>(source: ReadonlyAtom<T>): ({ [Atom<any>]: true }, T)
|
||||
if listeners[source] then
|
||||
return { [source] = true }, source()
|
||||
end
|
||||
|
||||
capturing = true
|
||||
local success, result = pcall(source)
|
||||
capturing = false
|
||||
|
||||
local dependencies = table.clone(captured)
|
||||
table.clear(captured)
|
||||
|
||||
if not success then
|
||||
warn("An error occurred while calling an atom:", result)
|
||||
end
|
||||
|
||||
return dependencies, result
|
||||
local function setCapturing(value: boolean)
|
||||
capturing = value
|
||||
end
|
||||
|
||||
local function shouldCapture()
|
||||
|
|
@ -33,6 +18,6 @@ end
|
|||
return {
|
||||
listeners = listeners,
|
||||
captured = captured,
|
||||
capture = capture,
|
||||
setCapturing = setCapturing,
|
||||
shouldCapture = shouldCapture,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
local capture = require(script.Parent.capture)
|
||||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
type ReadonlyAtom<T> = types.ReadonlyAtom<T>
|
||||
type Source<T> = types.Source<T>
|
||||
|
||||
local function subscribe<T>(source: ReadonlyAtom<T>, callback: (state: T, previous: T) -> ()): () -> ()
|
||||
local atoms, state = store.capture(source)
|
||||
local function subscribe<T>(source: Source<T>, callback: (state: T, prev: T) -> ()): () -> ()
|
||||
local atoms, state = capture(source)
|
||||
|
||||
local function listener()
|
||||
local lastState = state
|
||||
local prevState = state
|
||||
state = source()
|
||||
callback(state, lastState)
|
||||
callback(state, prevState)
|
||||
end
|
||||
|
||||
for atom in atoms do
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
local atomTypes = require(script.Parent.Parent.types)
|
||||
type Atom<T> = atomTypes.Atom<T>
|
||||
local patch = require(script.Parent.patch)
|
||||
local types = require(script.Parent.types)
|
||||
local types = require(script.Parent.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type SyncPayload = types.SyncPayload
|
||||
local patch = require(script.Parent.patch)
|
||||
|
||||
type ClientSyncOptions = {
|
||||
type ClientOptions = {
|
||||
atoms: { [string]: Atom<any> },
|
||||
}
|
||||
|
||||
|
|
@ -12,7 +11,7 @@ type ClientSyncer = {
|
|||
sync: (self: ClientSyncer, payload: SyncPayload) -> (),
|
||||
}
|
||||
|
||||
local function client(options: ClientSyncOptions): ClientSyncer
|
||||
local function client(options: ClientOptions): ClientSyncer
|
||||
local atoms = options.atoms
|
||||
local self = {} :: ClientSyncer
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
local client = require(script.client)
|
||||
local server = require(script.server)
|
||||
local types = require(script.types)
|
||||
|
||||
export type SyncPayload = types.SyncPayload
|
||||
|
||||
return {
|
||||
client = client,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ local validate = require(script.Parent.validate)
|
|||
|
||||
local NONE = { __none = "__none" }
|
||||
|
||||
local function diff(previousState: { [any]: any }, nextState: { [any]: any })
|
||||
local warnings = {}
|
||||
|
||||
local function diff(prevState: { [any]: any }, nextState: { [any]: any })
|
||||
local patches = table.clone(nextState)
|
||||
|
||||
for key, previous in previousState do
|
||||
for key, previous in prevState do
|
||||
local next = nextState[key]
|
||||
|
||||
if previous == next then
|
||||
|
|
@ -19,17 +21,18 @@ local function diff(previousState: { [any]: any }, nextState: { [any]: any })
|
|||
|
||||
if _G.__DEV__ then
|
||||
local success, result = pcall(function()
|
||||
for key, value in previousState do
|
||||
for key, value in prevState do
|
||||
validate(value, key)
|
||||
end
|
||||
for key, value in nextState do
|
||||
if previousState[key] ~= value then
|
||||
if prevState[key] ~= value then
|
||||
validate(value, key)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
if not success then
|
||||
if not success and not warnings[result] then
|
||||
warnings[result] = true
|
||||
warn(result)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
local Players = game:GetService("Players")
|
||||
|
||||
local atomTypes = require(script.Parent.Parent.types)
|
||||
type Atom<T> = atomTypes.Atom<T>
|
||||
local types = require(script.Parent.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type SyncPayload = types.SyncPayload
|
||||
local setInterval = require(script.Parent.Parent.utils.setInterval)
|
||||
local patch = require(script.Parent.patch)
|
||||
local types = require(script.Parent.types)
|
||||
type SyncPayload = types.SyncPayload
|
||||
|
||||
type ServerSyncOptions = {
|
||||
type ServerOptions = {
|
||||
atoms: { [string]: Atom<any> },
|
||||
interval: number?,
|
||||
}
|
||||
|
||||
type ServerSyncer = {
|
||||
onSync: (self: ServerSyncer, callback: (player: Player, payload: SyncPayload) -> ()) -> () -> (),
|
||||
flush: (self: ServerSyncer, player: Player?) -> (),
|
||||
connect: (self: ServerSyncer, callback: (player: Player, payload: SyncPayload) -> ()) -> () -> (),
|
||||
flush: (self: ServerSyncer, player: Player) -> (),
|
||||
hydrate: (self: ServerSyncer, player: Player) -> (),
|
||||
}
|
||||
|
||||
local function server(options: ServerSyncOptions): ServerSyncer
|
||||
local function server(options: ServerOptions): ServerSyncer
|
||||
local atoms = options.atoms
|
||||
local interval = options.interval or 0
|
||||
|
||||
local self = {} :: ServerSyncer
|
||||
local sync: (player: Player, payload: SyncPayload) -> ()
|
||||
local state: {}
|
||||
|
|
@ -33,38 +33,37 @@ local function server(options: ServerSyncOptions): ServerSyncer
|
|||
return state
|
||||
end
|
||||
|
||||
function self:onSync(callback)
|
||||
function self:connect(callback)
|
||||
sync = callback
|
||||
state = getState()
|
||||
|
||||
return setInterval(function()
|
||||
self:flush()
|
||||
local previous = state
|
||||
state = getState()
|
||||
|
||||
if state ~= previous then
|
||||
for _, player in Players:GetPlayers() do
|
||||
callback(player, {
|
||||
type = "patch",
|
||||
data = patch.diff(previous, state),
|
||||
})
|
||||
end
|
||||
end
|
||||
end, interval)
|
||||
end
|
||||
|
||||
function self:flush(player: Player?)
|
||||
local current = getState()
|
||||
local previous = state or current
|
||||
state = current
|
||||
function self:flush(player: Player)
|
||||
local previous = state
|
||||
state = getState()
|
||||
|
||||
if current ~= previous then
|
||||
local payload: SyncPayload = {
|
||||
sync(player, {
|
||||
type = "patch",
|
||||
data = patch.diff(previous, current),
|
||||
}
|
||||
|
||||
if player then
|
||||
return sync(player, payload)
|
||||
end
|
||||
|
||||
for _, player in Players:GetPlayers() do
|
||||
sync(player, payload)
|
||||
end
|
||||
end
|
||||
data = patch.diff(previous, state),
|
||||
})
|
||||
end
|
||||
|
||||
function self:hydrate(player)
|
||||
assert(sync, "onSync must be called before onJoin")
|
||||
assert(sync, "connect() must be called before hydrate()")
|
||||
|
||||
sync(player, {
|
||||
type = "set",
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
export type SyncPayload = {
|
||||
type: "patch" | "set",
|
||||
data: { [any]: any },
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -1,5 +1,16 @@
|
|||
export type Atom<T> = (() -> T) & ((T | (T) -> T) -> T)
|
||||
export type ReadonlyAtom<T> = () -> T
|
||||
export type WeakMap<K, V> = typeof(setmetatable({} :: { [K]: V }, { __mode = "k" }))
|
||||
|
||||
export type Atom<T> = (() -> T) & (state: T | (T) -> T) -> T
|
||||
|
||||
export type Source<T> = () -> T
|
||||
|
||||
export type AtomOptions<T> = {
|
||||
equals: (prev: T, next: T) -> boolean,
|
||||
}
|
||||
|
||||
export type SyncPayload = {
|
||||
type: "patch" | "set",
|
||||
data: { [any]: any },
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
23
src/unwrap.luau
Normal file
23
src/unwrap.luau
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
local store = require(script.Parent.store)
|
||||
|
||||
local function unwrap<T>(atom: (() -> T) | T): T
|
||||
if type(atom) ~= "function" then
|
||||
return atom
|
||||
end
|
||||
|
||||
if not store.shouldCapture() then
|
||||
return atom()
|
||||
end
|
||||
|
||||
store.setCapturing(false)
|
||||
local success, result = pcall(atom)
|
||||
store.setCapturing(true)
|
||||
|
||||
if not success then
|
||||
error(`Failed to unwrap atom: ${result}`, 2)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return unwrap
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
local refs = {}
|
||||
|
||||
--- Prevents the garbage collector from collecting a value
|
||||
local function hold(value: unknown)
|
||||
refs[value] = true
|
||||
end
|
||||
|
||||
--- Allows the garbage collector to collect a value
|
||||
local function release(value: unknown)
|
||||
refs[value] = nil
|
||||
end
|
||||
|
||||
--- Clears all held references
|
||||
local function clear()
|
||||
table.clear(refs)
|
||||
end
|
||||
|
||||
--- Returns a function that retrieves the value through a weak reference
|
||||
local function weak<T>(value: T): () -> T?
|
||||
local set = setmetatable({ value }, { __mode = "v" })
|
||||
return function()
|
||||
return set[1]
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
hold = hold,
|
||||
release = release,
|
||||
clear = clear,
|
||||
weak = weak,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue