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)`
#### `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)`

View file

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

View file

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

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)
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()

View file

@ -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)

View file

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

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 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
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 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

View file

@ -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
View file

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

View file

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

View file

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

View file

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

View file

@ -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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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