refactor: finalize updates

This commit is contained in:
Richard 2024-03-25 18:57:22 -07:00
parent 6dfcaa5cfa
commit a857797465
28 changed files with 300 additions and 240 deletions

View file

@ -55,11 +55,13 @@ Charm = "littensy/charm@VERSION"
#### `subscribe(atom, listener)` #### `subscribe(atom, listener)`
#### `derive(atom, comparator?)` #### `derive(atom, equals?)`
#### `observe(atom, observer)` #### `observe(atom, observer)`
#### `mapAtom(atom, mapper)` #### `map(atom, mapper)`
#### `unwrap(atom)`
### React ### React
@ -77,7 +79,7 @@ Charm = "littensy/charm@VERSION"
#### `sync.server(options)` #### `sync.server(options)`
#### `Server.onSync(callback)` #### `Server.connect(callback)`
#### `Server.hydrate(player)` #### `Server.hydrate(player)`

View file

@ -42,4 +42,18 @@ return function()
expect(outer).to.equal(2) expect(outer).to.equal(2)
expect(inner).to.equal(1) expect(inner).to.equal(1)
end) 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 end

View file

@ -29,9 +29,11 @@ local benchmarks = {
derived_memo = function(_, source: any) derived_memo = function(_, source: any)
derive(function() derive(function()
return { count = 2 * source().count } return { count = 2 * source().count }
end, function(previous, current) end, {
equals = function(previous, current)
return previous.count == current.count return previous.count == current.count
end) end,
})
for _ = 1, 500 do for _ = 1, 500 do
source({ count = math.random(1, 2) }) source({ count = math.random(1, 2) })
end end

View 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

View file

@ -22,19 +22,6 @@ return function()
expect(calls).to.equal(3) expect(calls).to.equal(3)
end) 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() it("can be nested", function()
local source = atom(1) local source = atom(1)
local double = derive(function() local double = derive(function()

View file

@ -1,12 +1,12 @@
local atom = require(script.Parent.Parent.atom) 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 observe = require(script.Parent.Parent.observe)
local store = require(script.Parent.Parent.store) local store = require(script.Parent.Parent.store)
return function() return function()
it("returns mapped atoms", function() it("returns mapped atoms", function()
local itemsAtom = atom({ "a", "b", "c" }) 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 return string.upper(value), key
end) end)
local updates = 0 local updates = 0
@ -35,7 +35,7 @@ return function()
it("uses key extractor", function() it("uses key extractor", function()
local itemsAtom = atom({ { key = 1 }, { key = 2 }, { key = 3 } }) 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 return item, item.key
end) end)
@ -61,7 +61,7 @@ return function()
{ key = 3, value = "c" }, { key = 3, value = "c" },
}) })
local mappedAtom = mapAtom(itemsAtom, function(item) local mappedAtom = map(itemsAtom, function(item)
return item, item.key return item, item.key
end) end)

View file

@ -1,5 +1,6 @@
local atom = require(script.Parent.Parent.Parent.atom) local atom = require(script.Parent.Parent.Parent.atom)
local sync = require(script.Parent.Parent.Parent.sync) local sync = require(script.Parent.Parent.Parent.sync)
local types = require(script.Parent.Parent.Parent.types)
return function() return function()
local target: Player = {} :: any local target: Player = {} :: any
@ -7,9 +8,9 @@ return function()
it("sends state to new players", function() it("sends state to new players", function()
local a = atom(1) local a = atom(1)
local server = sync.server({ atoms = { a = a }, interval = -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 = ... player, payload = ...
end) end)
@ -18,14 +19,16 @@ return function()
expect(player).to.equal(target) expect(player).to.equal(target)
expect(payload.type).to.equal("set") expect(payload.type).to.equal("set")
expect(payload.data.a).to.equal(1) expect(payload.data.a).to.equal(1)
disconnect()
end) end)
it("sends state patches", function() it("sends state patches", function()
local a = atom({ b = 1, c = 2 }) local a = atom({ b = 1, c = 2 })
local server = sync.server({ atoms = { a = a }, interval = -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 = ... player, payload = ...
end) end)
@ -36,5 +39,7 @@ return function()
expect(payload.type).to.equal("patch") expect(payload.type).to.equal("patch")
expect(payload.data.a.b).to.never.be.ok() expect(payload.data.a.b).to.never.be.ok()
expect(payload.data.a.c).to.equal(5) expect(payload.data.a.c).to.equal(5)
disconnect()
end) end)
end end

View 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

View file

@ -1,8 +1,11 @@
local store = require(script.Parent.store) local store = require(script.Parent.store)
local types = require(script.Parent.types) local types = require(script.Parent.types)
type Atom<T> = types.Atom<T> 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(...) local function atom(...)
if select("#", ...) == 0 then if select("#", ...) == 0 then
if store.shouldCapture() 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 ... 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 state = nextState
for listener in pairs(table.clone(store.listeners[atom])) do for listener in pairs(table.clone(store.listeners[atom])) do

25
src/capture.luau Normal file
View 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

View file

@ -1,24 +1,19 @@
local atom = require(script.Parent.atom) local atom = require(script.Parent.atom)
local capture = require(script.Parent.capture)
local store = require(script.Parent.store) local store = require(script.Parent.store)
local types = require(script.Parent.types) local types = require(script.Parent.types)
type Atom<T> = types.Atom<T> 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 function derive<T>(source: Source<T>, options: AtomOptions<T>?): Source<T>
local atoms, state = store.capture(source) local atoms, state = capture(source)
local derived = atom(state) local derived = atom(state, options)
local derivedRef = setmetatable({ current = derived }, { __mode = "v" }) local derivedRef = setmetatable({ current = derived }, { __mode = "v" })
local function listener() local function listener()
if not derivedRef.current then if derivedRef.current then
return derivedRef.current(source())
end
local nextState = source()
if state ~= nextState and not (comparator and comparator(state, nextState)) then
state = nextState
derivedRef.current(nextState)
end end
end end

View file

@ -1,9 +1,8 @@
local capture = require(script.Parent.capture)
local store = require(script.Parent.store) 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 function effect(source: () -> nil): () -> ()
local atoms = store.capture(source) local atoms = capture(source)
local function listener() local function listener()
source() source()

138
src/index.d.ts vendored
View file

@ -1,117 +1,109 @@
export = Charm; export = Charm;
export as namespace 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 { declare namespace Charm {
interface Atom<T> extends ReadonlyAtom<T> { interface Atom<T> extends Source<T> {
readonly __nominal: unique symbol; readonly __nominal: unique symbol;
(state: T | ((state: T) => T)): T; (state: T | ((prev: T) => T)): void;
} }
interface ReadonlyAtom<T> { type Source<T> = () => T;
(): T;
interface AtomOptions<State> {
equals?: (prev: State, next: State) => boolean;
} }
type AtomArrayToStates<Atoms extends ReadonlyAtom<any>[]> = { function atom<State>(state: State, options?: AtomOptions<State>): Atom<State>;
[Key in keyof Atoms]: Atoms[Key] extends ReadonlyAtom<infer State> ? State : never;
};
function atom<State>(state: State): Atom<State>;
function isAtom(value: unknown): value is Atom<any>; function isAtom(value: unknown): value is Atom<any>;
function derive<State>( function derive<State>(atom: Source<State>, options?: AtomOptions<State>): Source<State>;
atom: ReadonlyAtom<State>,
comparator?: (previous: State, current: State) => boolean,
): ReadonlyAtom<State>;
function subscribe<State>( function subscribe<State>(atom: Source<State>, callback: (state: State, prev: State) => void): Cleanup;
atom: ReadonlyAtom<State>,
callback: (state: State, previousState: State) => void,
): () => void;
function effect(callback: () => void): () => void; function effect(callback: Source<void>): Cleanup;
function observe<K, V>( function unwrap<State>(atom: State | Source<State>): State;
atom: ReadonlyAtom<Map<K, V> | ReadonlyMap<K, V>>,
factory: (value: V, key: K) => (() => void) | void,
): () => void;
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>( function observe<Item>(
atom: ReadonlyAtom<readonly V0[]> | ReadonlyAtom<V0[]>, atom: Source<readonly Item[]>,
mapper: (value: V0, index: number) => LuaTuple<[V1 | undefined, K1]>, factory: (item: Item, index: number) => Cleanup | void,
): ReadonlyAtom<ReadonlyMap<K1, V1>>; ): Cleanup;
function mapAtom<V0, V1>( function observe<Key, Item>(
atom: ReadonlyAtom<readonly V0[]> | ReadonlyAtom<V0[]>, atom: Source<AnyMap<Key, Item>>,
mapper: (value: V0, index: number) => V1, factory: (item: Item, key: Key) => Cleanup | void,
): ReadonlyAtom<readonly V1[]>; ): Cleanup;
function mapAtom<K0, V0, K1 = K0, V1 = V0>( function map<V0, K1, V1>(
atom: ReadonlyAtom<ReadonlyMap<K0, V0>> | ReadonlyAtom<Map<K0, V0>>, atom: Source<readonly V0[]>,
mapper: (value: V0, key: K0) => LuaTuple<[V1 | undefined, K1]> | V1, mapper: (value: V0, index: number) => LuaTuple<[value: V1 | undefined, key: K1]>,
): ReadonlyAtom<ReadonlyMap<K1, V1>>; ): Source<ReadonlyMap<K1, V1>>;
function mapAtom<K0 extends string | number | symbol, V0, K1, V1>( function map<V0, V1>(atom: Source<readonly V0[]>, mapper: (value: V0, index: number) => V1): Source<readonly V1[]>;
atom: ReadonlyAtom<{ readonly [K in K0]: V0 }>,
mapper: (value: V0, key: K0) => LuaTuple<[V1 | undefined, K1]> | V1,
): ReadonlyAtom<ReadonlyMap<K1, 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>( function useAtom<State>(
atom: ReadonlyAtom<State>, atom: Source<State>,
): LuaTuple<[state: State, setState: (state: State | ((previous: State) => State)) => void]>; ): LuaTuple<[state: State, setState: (state: State | ((prev: State) => State)) => void]>;
function useAtom<State, Result>( function useAtom<State, Result>(
atom: ReadonlyAtom<State>, atom: Source<State>,
selector: (state: State) => Result, 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 { // Sync
readonly __none: "__none";
}
type StatePatch<States> = { const sync: {
readonly [K in keyof States]?: (States[K] extends object ? StatePatch<States[K]> : States[K]) | None; client(options: ClientOptions): ClientSyncer;
server(options: ServerOptions): ServerSyncer;
}; };
type SyncPayload<States> = { type: "set"; data: States } | { type: "patch"; data: StatePatch<States> }; interface SyncPatch {
[key: string | number | symbol]: SyncPatch | { __none: "__none" } | string | number | boolean | undefined;
interface AtomMap {
readonly [key: string]: Atom<any>;
} }
type AtomMapToStates<Atoms extends AtomMap> = { interface SyncPayload {
readonly [Key in keyof Atoms]: Atoms[Key] extends Atom<infer State> ? State : never; type: "set" | "patch";
}; data: SyncPatch;
interface ClientSyncOptions<Atoms extends AtomMap> {
atoms: Atoms;
} }
interface ClientSyncer<States> { interface ClientOptions {
sync(payload: SyncPayload<States>): void; atoms: Record<string, Atom<any>>;
} }
interface ServerSyncOptions<Atoms extends AtomMap> { interface ServerOptions {
atoms: Atoms; atoms: Record<string, Atom<any>>;
interval?: number; interval?: number;
} }
interface ServerSyncer<States> { interface ClientSyncer {
onSync(callback: (player: Player, payload: SyncPayload<States>) => void): () => void; sync(payload: SyncPayload): void;
}
interface ServerSyncer {
connect(callback: (player: Player, payload: SyncPayload) => void): Cleanup;
hydrate(player: Player): void; 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>>;
}
} }

View file

@ -1,10 +1,11 @@
_G[script.modules.Promise] = _G[script] _G[script.modules.Promise] = _G[script]
local atom = require(script.atom) local atom = require(script.atom)
local capture = require(script.capture)
local derive = require(script.derive) local derive = require(script.derive)
local effect = require(script.effect) local effect = require(script.effect)
local isAtom = require(script.isAtom) local isAtom = require(script.isAtom)
local mapAtom = require(script.mapAtom) local map = require(script.map)
local observe = require(script.observe) local observe = require(script.observe)
local useAtom = require(script.react.useAtom) local useAtom = require(script.react.useAtom)
local useAtomState = require(script.react.useAtomState) local useAtomState = require(script.react.useAtomState)
@ -12,18 +13,22 @@ local useSetAtom = require(script.react.useSetAtom)
local subscribe = require(script.subscribe) local subscribe = require(script.subscribe)
local sync = require(script.sync) local sync = require(script.sync)
local types = require(script.types) local types = require(script.types)
local unwrap = require(script.unwrap)
export type Atom<State> = types.Atom<State> 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 { return {
atom = atom, atom = atom,
derive = derive, derive = derive,
effect = effect, effect = effect,
isAtom = isAtom, isAtom = isAtom,
mapAtom = mapAtom, map = map,
observe = observe, observe = observe,
subscribe = subscribe, subscribe = subscribe,
unwrap = unwrap,
capture = capture,
sync = sync, sync = sync,
useAtom = useAtom, useAtom = useAtom,
useAtomState = useAtomState, useAtomState = useAtomState,

View file

@ -1,7 +1,7 @@
local store = require(script.Parent.store) local store = require(script.Parent.store)
local function isAtom(value: any) 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 end
return isAtom return isAtom

View file

@ -2,12 +2,17 @@ local atom = require(script.Parent.atom)
local subscribe = require(script.Parent.subscribe) local subscribe = require(script.Parent.subscribe)
local types = require(script.Parent.types) local types = require(script.Parent.types)
type Atom<T> = types.Atom<T> 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 mappedAtom = atom({})
local mappedAtomRef = setmetatable({ current = mappedAtom }, { __mode = "v" }) local mappedAtomRef = setmetatable({ current = mappedAtom }, { __mode = "v" })
local previousMappedItems: { [K1]: V1 } = {} local prevMappedItems: { [K1]: V1 } = {}
local unsubscribe local unsubscribe
local function listener(items: { [K0]: V0 }) 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
end end
for key in previousMappedItems do for key in prevMappedItems do
if mappedKeys[key] == nil and mappedItems[key] == previousMappedItems[key] then if mappedKeys[key] == nil and mappedItems[key] == prevMappedItems[key] then
mappedItems[key] = nil mappedItems[key] = nil
end end
end end
previousMappedItems = mappedItems prevMappedItems = mappedItems
mappedAtom(mappedItems) mappedAtom(mappedItems)
end end
@ -46,4 +51,4 @@ local function mapAtom<K0, V0, K1, V1>(itemsAtom: Atom<{ [K0]: V0 }>, mapper: (V
return mappedAtom return mappedAtom
end end
return mapAtom return map :: Map

View file

@ -1,8 +1,8 @@
local effect = require(script.Parent.effect) local effect = require(script.Parent.effect)
local types = require(script.Parent.types) 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 connections: { [K]: () -> () } = {}
local unsubscribe = effect(function() local unsubscribe = effect(function()

View file

@ -6,8 +6,8 @@ type Atom<T> = types.Atom<T>
export type SetState<T> = (T | ((T) -> T)) -> () export type SetState<T> = (T | ((T) -> T)) -> ()
local function useSetAtom<State>(atom: Atom<State>): SetState<State> local function useSetAtom<State>(atom: Atom<State>): SetState<State>
return requireReact().useCallback(function(value) return requireReact().useCallback(function(state: State | ((State) -> State)): ()
atom(value) atom(state)
end, { atom }) end, { atom })
end end

View file

@ -1,29 +1,14 @@
local types = require(script.Parent.types) local types = require(script.Parent.types)
type Atom<T> = types.Atom<T> 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> type WeakMap<K, V> = types.WeakMap<K, V>
local listeners: WeakMap<Atom<any>, WeakMap<() -> (), unknown>> = setmetatable({}, { __mode = "k" }) local listeners: WeakMap<Atom<any>, WeakMap<() -> (), unknown>> = setmetatable({}, { __mode = "k" })
local captured: { [Atom<any>]: true } = {} local captured: { [Atom<any>]: true } = {}
local capturing = false local capturing = false
local function capture<T>(source: ReadonlyAtom<T>): ({ [Atom<any>]: true }, T) local function setCapturing(value: boolean)
if listeners[source] then capturing = value
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
end end
local function shouldCapture() local function shouldCapture()
@ -33,6 +18,6 @@ end
return { return {
listeners = listeners, listeners = listeners,
captured = captured, captured = captured,
capture = capture, setCapturing = setCapturing,
shouldCapture = shouldCapture, shouldCapture = shouldCapture,
} }

View file

@ -1,14 +1,15 @@
local capture = require(script.Parent.capture)
local store = require(script.Parent.store) local store = require(script.Parent.store)
local types = require(script.Parent.types) 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 function subscribe<T>(source: Source<T>, callback: (state: T, prev: T) -> ()): () -> ()
local atoms, state = store.capture(source) local atoms, state = capture(source)
local function listener() local function listener()
local lastState = state local prevState = state
state = source() state = source()
callback(state, lastState) callback(state, prevState)
end end
for atom in atoms do for atom in atoms do

View file

@ -1,10 +1,9 @@
local atomTypes = require(script.Parent.Parent.types) local types = require(script.Parent.Parent.types)
type Atom<T> = atomTypes.Atom<T> type Atom<T> = types.Atom<T>
local patch = require(script.Parent.patch)
local types = require(script.Parent.types)
type SyncPayload = types.SyncPayload type SyncPayload = types.SyncPayload
local patch = require(script.Parent.patch)
type ClientSyncOptions = { type ClientOptions = {
atoms: { [string]: Atom<any> }, atoms: { [string]: Atom<any> },
} }
@ -12,7 +11,7 @@ type ClientSyncer = {
sync: (self: ClientSyncer, payload: SyncPayload) -> (), sync: (self: ClientSyncer, payload: SyncPayload) -> (),
} }
local function client(options: ClientSyncOptions): ClientSyncer local function client(options: ClientOptions): ClientSyncer
local atoms = options.atoms local atoms = options.atoms
local self = {} :: ClientSyncer local self = {} :: ClientSyncer

View file

@ -1,8 +1,5 @@
local client = require(script.client) local client = require(script.client)
local server = require(script.server) local server = require(script.server)
local types = require(script.types)
export type SyncPayload = types.SyncPayload
return { return {
client = client, client = client,

View file

@ -2,10 +2,12 @@ local validate = require(script.Parent.validate)
local NONE = { __none = "__none" } 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) local patches = table.clone(nextState)
for key, previous in previousState do for key, previous in prevState do
local next = nextState[key] local next = nextState[key]
if previous == next then if previous == next then
@ -19,17 +21,18 @@ local function diff(previousState: { [any]: any }, nextState: { [any]: any })
if _G.__DEV__ then if _G.__DEV__ then
local success, result = pcall(function() local success, result = pcall(function()
for key, value in previousState do for key, value in prevState do
validate(value, key) validate(value, key)
end end
for key, value in nextState do for key, value in nextState do
if previousState[key] ~= value then if prevState[key] ~= value then
validate(value, key) validate(value, key)
end end
end end
end) end)
if not success then if not success and not warnings[result] then
warnings[result] = true
warn(result) warn(result)
end end
end end

View file

@ -1,26 +1,26 @@
local Players = game:GetService("Players") local Players = game:GetService("Players")
local atomTypes = require(script.Parent.Parent.types) local types = require(script.Parent.Parent.types)
type Atom<T> = atomTypes.Atom<T> type Atom<T> = types.Atom<T>
type SyncPayload = types.SyncPayload
local setInterval = require(script.Parent.Parent.utils.setInterval) local setInterval = require(script.Parent.Parent.utils.setInterval)
local patch = require(script.Parent.patch) local patch = require(script.Parent.patch)
local types = require(script.Parent.types)
type SyncPayload = types.SyncPayload
type ServerSyncOptions = { type ServerOptions = {
atoms: { [string]: Atom<any> }, atoms: { [string]: Atom<any> },
interval: number?, interval: number?,
} }
type ServerSyncer = { type ServerSyncer = {
onSync: (self: ServerSyncer, callback: (player: Player, payload: SyncPayload) -> ()) -> () -> (), connect: (self: ServerSyncer, callback: (player: Player, payload: SyncPayload) -> ()) -> () -> (),
flush: (self: ServerSyncer, player: Player?) -> (), flush: (self: ServerSyncer, player: Player) -> (),
hydrate: (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 atoms = options.atoms
local interval = options.interval or 0 local interval = options.interval or 0
local self = {} :: ServerSyncer local self = {} :: ServerSyncer
local sync: (player: Player, payload: SyncPayload) -> () local sync: (player: Player, payload: SyncPayload) -> ()
local state: {} local state: {}
@ -33,38 +33,37 @@ local function server(options: ServerSyncOptions): ServerSyncer
return state return state
end end
function self:onSync(callback) function self:connect(callback)
sync = callback sync = callback
state = getState() state = getState()
return setInterval(function() 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, interval)
end end
function self:flush(player: Player?) function self:flush(player: Player)
local current = getState() local previous = state
local previous = state or current state = getState()
state = current
if current ~= previous then sync(player, {
local payload: SyncPayload = {
type = "patch", type = "patch",
data = patch.diff(previous, current), data = patch.diff(previous, state),
} })
if player then
return sync(player, payload)
end
for _, player in Players:GetPlayers() do
sync(player, payload)
end
end
end end
function self:hydrate(player) function self:hydrate(player)
assert(sync, "onSync must be called before onJoin") assert(sync, "connect() must be called before hydrate()")
sync(player, { sync(player, {
type = "set", type = "set",

View file

@ -1,6 +0,0 @@
export type SyncPayload = {
type: "patch" | "set",
data: { [any]: any },
}
return nil

View file

@ -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 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 return nil

23
src/unwrap.luau Normal file
View 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

View file

@ -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,
}