From a857797465393504830dd723c484666a065a3770 Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 25 Mar 2024 18:57:22 -0700 Subject: [PATCH] refactor: finalize updates --- README.md | 8 +- src/__tests__/atom.spec.luau | 14 ++ src/__tests__/benchmarks/atom.bench.luau | 8 +- src/__tests__/capture.spec.luau | 22 +++ src/__tests__/derive.spec.luau | 13 -- src/__tests__/{mapAtom.spec.luau => map.luau} | 8 +- src/__tests__/sync/server.spec.luau | 13 +- src/__tests__/unwrap.spec.luau | 23 +++ src/atom.luau | 7 +- src/capture.luau | 25 ++++ src/derive.luau | 21 +-- src/effect.luau | 7 +- src/index.d.ts | 138 +++++++++--------- src/init.luau | 11 +- src/isAtom.luau | 2 +- src/{mapAtom.luau => map.luau} | 19 ++- src/observe.luau | 4 +- src/react/useSetAtom.luau | 4 +- src/store.luau | 23 +-- src/subscribe.luau | 11 +- src/sync/client.luau | 11 +- src/sync/init.luau | 3 - src/sync/patch.luau | 13 +- src/sync/server.luau | 57 ++++---- src/sync/types.luau | 6 - src/types.luau | 15 +- src/unwrap.luau | 23 +++ src/utils/reference.luau | 31 ---- 28 files changed, 300 insertions(+), 240 deletions(-) create mode 100644 src/__tests__/capture.spec.luau rename src/__tests__/{mapAtom.spec.luau => map.luau} (90%) create mode 100644 src/__tests__/unwrap.spec.luau create mode 100644 src/capture.luau rename src/{mapAtom.luau => map.luau} (58%) delete mode 100644 src/sync/types.luau create mode 100644 src/unwrap.luau delete mode 100644 src/utils/reference.luau diff --git a/README.md b/README.md index bb8ac90..1b8417b 100644 --- a/README.md +++ b/README.md @@ -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)` diff --git a/src/__tests__/atom.spec.luau b/src/__tests__/atom.spec.luau index 78cfd1b..09cfbb7 100644 --- a/src/__tests__/atom.spec.luau +++ b/src/__tests__/atom.spec.luau @@ -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 diff --git a/src/__tests__/benchmarks/atom.bench.luau b/src/__tests__/benchmarks/atom.bench.luau index 7746117..4f44024 100644 --- a/src/__tests__/benchmarks/atom.bench.luau +++ b/src/__tests__/benchmarks/atom.bench.luau @@ -29,9 +29,11 @@ local benchmarks = { derived_memo = function(_, source: any) derive(function() return { count = 2 * source().count } - end, function(previous, current) - return previous.count == current.count - end) + end, { + equals = function(previous, current) + return previous.count == current.count + end, + }) for _ = 1, 500 do source({ count = math.random(1, 2) }) end diff --git a/src/__tests__/capture.spec.luau b/src/__tests__/capture.spec.luau new file mode 100644 index 0000000..ae33d99 --- /dev/null +++ b/src/__tests__/capture.spec.luau @@ -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 diff --git a/src/__tests__/derive.spec.luau b/src/__tests__/derive.spec.luau index b7c65f0..3021a76 100644 --- a/src/__tests__/derive.spec.luau +++ b/src/__tests__/derive.spec.luau @@ -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() diff --git a/src/__tests__/mapAtom.spec.luau b/src/__tests__/map.luau similarity index 90% rename from src/__tests__/mapAtom.spec.luau rename to src/__tests__/map.luau index 4b68b5c..79b5916 100644 --- a/src/__tests__/mapAtom.spec.luau +++ b/src/__tests__/map.luau @@ -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) diff --git a/src/__tests__/sync/server.spec.luau b/src/__tests__/sync/server.spec.luau index ec00984..972d78b 100644 --- a/src/__tests__/sync/server.spec.luau +++ b/src/__tests__/sync/server.spec.luau @@ -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 diff --git a/src/__tests__/unwrap.spec.luau b/src/__tests__/unwrap.spec.luau new file mode 100644 index 0000000..41b2b12 --- /dev/null +++ b/src/__tests__/unwrap.spec.luau @@ -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 diff --git a/src/atom.luau b/src/atom.luau index 8af2c8f..3dd550d 100644 --- a/src/atom.luau +++ b/src/atom.luau @@ -1,8 +1,11 @@ local store = require(script.Parent.store) local types = require(script.Parent.types) type Atom = types.Atom +type AtomOptions = types.AtomOptions + +local function atom(state: T, options: AtomOptions?): Atom + local equals = options and options.equals -local function atom(state: T): Atom local function atom(...) if select("#", ...) == 0 then if store.shouldCapture() then @@ -14,7 +17,7 @@ local function atom(state: T): Atom 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 diff --git a/src/capture.luau b/src/capture.luau new file mode 100644 index 0000000..111e52c --- /dev/null +++ b/src/capture.luau @@ -0,0 +1,25 @@ +local store = require(script.Parent.store) +local types = require(script.Parent.types) +type Atom = types.Atom +type Source = types.Source + +local function capture(source: Source): ({ [Atom]: 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 diff --git a/src/derive.luau b/src/derive.luau index 2ebe14b..444db17 100644 --- a/src/derive.luau +++ b/src/derive.luau @@ -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 = types.Atom -type ReadonlyAtom = types.ReadonlyAtom +type AtomOptions = types.AtomOptions +type Source = types.Source -local function derive(source: ReadonlyAtom, comparator: ((old: T, new: T) -> boolean)?): ReadonlyAtom - local atoms, state = store.capture(source) - local derived = atom(state) +local function derive(source: Source, options: AtomOptions?): Source + 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 diff --git a/src/effect.luau b/src/effect.luau index 8b74b5c..8a6e685 100644 --- a/src/effect.luau +++ b/src/effect.luau @@ -1,9 +1,8 @@ +local capture = require(script.Parent.capture) local store = require(script.Parent.store) -local types = require(script.Parent.types) -type ReadonlyAtom = types.ReadonlyAtom -local function effect(source: ReadonlyAtom): () -> () - local atoms = store.capture(source) +local function effect(source: () -> nil): () -> () + local atoms = capture(source) local function listener() source() diff --git a/src/index.d.ts b/src/index.d.ts index 2b6cb9d..89ebe1c 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,117 +1,109 @@ export = Charm; export as namespace Charm; +type Cleanup = () => void; + +type AnyMap = + | Map + | ReadonlyMap + | (K extends string | number | symbol ? { readonly [Key in K]: V } : never); + declare namespace Charm { - interface Atom extends ReadonlyAtom { + interface Atom extends Source { readonly __nominal: unique symbol; - (state: T | ((state: T) => T)): T; + (state: T | ((prev: T) => T)): void; } - interface ReadonlyAtom { - (): T; + type Source = () => T; + + interface AtomOptions { + equals?: (prev: State, next: State) => boolean; } - type AtomArrayToStates[]> = { - [Key in keyof Atoms]: Atoms[Key] extends ReadonlyAtom ? State : never; - }; - - function atom(state: State): Atom; + function atom(state: State, options?: AtomOptions): Atom; function isAtom(value: unknown): value is Atom; - function derive( - atom: ReadonlyAtom, - comparator?: (previous: State, current: State) => boolean, - ): ReadonlyAtom; + function derive(atom: Source, options?: AtomOptions): Source; - function subscribe( - atom: ReadonlyAtom, - callback: (state: State, previousState: State) => void, - ): () => void; + function subscribe(atom: Source, callback: (state: State, prev: State) => void): Cleanup; - function effect(callback: () => void): () => void; + function effect(callback: Source): Cleanup; - function observe( - atom: ReadonlyAtom | ReadonlyMap>, - factory: (value: V, key: K) => (() => void) | void, - ): () => void; + function unwrap(atom: State | Source): State; - function observe(atom: ReadonlyAtom, factory: (value: unknown, key: unknown) => (() => void) | void): () => void; + function capture(atom: Source): LuaTuple<[captured: Set>, state: State]>; - function mapAtom( - atom: ReadonlyAtom | ReadonlyAtom, - mapper: (value: V0, index: number) => LuaTuple<[V1 | undefined, K1]>, - ): ReadonlyAtom>; + function observe( + atom: Source, + factory: (item: Item, index: number) => Cleanup | void, + ): Cleanup; - function mapAtom( - atom: ReadonlyAtom | ReadonlyAtom, - mapper: (value: V0, index: number) => V1, - ): ReadonlyAtom; + function observe( + atom: Source>, + factory: (item: Item, key: Key) => Cleanup | void, + ): Cleanup; - function mapAtom( - atom: ReadonlyAtom> | ReadonlyAtom>, - mapper: (value: V0, key: K0) => LuaTuple<[V1 | undefined, K1]> | V1, - ): ReadonlyAtom>; + function map( + atom: Source, + mapper: (value: V0, index: number) => LuaTuple<[value: V1 | undefined, key: K1]>, + ): Source>; - function mapAtom( - atom: ReadonlyAtom<{ readonly [K in K0]: V0 }>, - mapper: (value: V0, key: K0) => LuaTuple<[V1 | undefined, K1]> | V1, - ): ReadonlyAtom>; + function map(atom: Source, mapper: (value: V0, index: number) => V1): Source; - function useAtomState(atom: ReadonlyAtom): State; + function map( + atom: Source>, + mapper: (value: V0, key: K0) => LuaTuple<[value: V1 | undefined, key: K1]> | V1, + ): Source>; - function useAtomState(atom: ReadonlyAtom, selector: (state: State) => Result): Result; + // React - function useSetAtom(atom: ReadonlyAtom): (state: State | ((previous: State) => State)) => void; + function useAtomState(atom: Source): State; + + function useAtomState(atom: Source, selector: (state: State) => Result): Result; + + function useSetAtom(atom: Source): (state: State | ((prev: State) => State)) => void; function useAtom( - atom: ReadonlyAtom, - ): LuaTuple<[state: State, setState: (state: State | ((previous: State) => State)) => void]>; + atom: Source, + ): LuaTuple<[state: State, setState: (state: State | ((prev: State) => State)) => void]>; function useAtom( - atom: ReadonlyAtom, + atom: Source, 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 = { - readonly [K in keyof States]?: (States[K] extends object ? StatePatch : States[K]) | None; + const sync: { + client(options: ClientOptions): ClientSyncer; + server(options: ServerOptions): ServerSyncer; }; - type SyncPayload = { type: "set"; data: States } | { type: "patch"; data: StatePatch }; - - interface AtomMap { - readonly [key: string]: Atom; + interface SyncPatch { + [key: string | number | symbol]: SyncPatch | { __none: "__none" } | string | number | boolean | undefined; } - type AtomMapToStates = { - readonly [Key in keyof Atoms]: Atoms[Key] extends Atom ? State : never; - }; - - interface ClientSyncOptions { - atoms: Atoms; + interface SyncPayload { + type: "set" | "patch"; + data: SyncPatch; } - interface ClientSyncer { - sync(payload: SyncPayload): void; + interface ClientOptions { + atoms: Record>; } - interface ServerSyncOptions { - atoms: Atoms; + interface ServerOptions { + atoms: Record>; interval?: number; } - interface ServerSyncer { - onSync(callback: (player: Player, payload: SyncPayload) => 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(options: ClientSyncOptions): ClientSyncer>; - - function server(options: ServerSyncOptions): ServerSyncer>; - } } diff --git a/src/init.luau b/src/init.luau index a0b6286..9747c78 100644 --- a/src/init.luau +++ b/src/init.luau @@ -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 = types.Atom -export type ReadonlyAtom = types.ReadonlyAtom +export type Source = types.Source +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, diff --git a/src/isAtom.luau b/src/isAtom.luau index 8ba9e3a..2f269f3 100644 --- a/src/isAtom.luau +++ b/src/isAtom.luau @@ -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 diff --git a/src/mapAtom.luau b/src/map.luau similarity index 58% rename from src/mapAtom.luau rename to src/map.luau index fd63710..3024d64 100644 --- a/src/mapAtom.luau +++ b/src/map.luau @@ -2,12 +2,17 @@ local atom = require(script.Parent.atom) local subscribe = require(script.Parent.subscribe) local types = require(script.Parent.types) type Atom = types.Atom -type ReadonlyAtom = types.ReadonlyAtom +type Source = types.Source -local function mapAtom(itemsAtom: Atom<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)): ReadonlyAtom<{ [K1]: V1 }> +type Map = + ((atom: Source<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1)) -> Source<{ [K1]: V1 }>) + & ((atom: Source<{ [K0]: V0 }>, mapper: (V0, K0) -> V1?) -> Source<{ [K0]: V1 }>) + & ((atom: Source<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)) -> Source<{ [K1]: V1 }>) + +local function map(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(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(itemsAtom: Atom<{ [K0]: V0 }>, mapper: (V return mappedAtom end -return mapAtom +return map :: Map diff --git a/src/observe.luau b/src/observe.luau index 8bef5c3..7954a39 100644 --- a/src/observe.luau +++ b/src/observe.luau @@ -1,8 +1,8 @@ local effect = require(script.Parent.effect) local types = require(script.Parent.types) -type ReadonlyAtom = types.ReadonlyAtom +type Source = types.Source -local function observe(source: ReadonlyAtom<{ [K]: V }>, factory: (value: V, key: K) -> () -> ()): () -> () +local function observe(source: Source<{ [K]: V }>, factory: (value: V, key: K) -> () -> ()): () -> () local connections: { [K]: () -> () } = {} local unsubscribe = effect(function() diff --git a/src/react/useSetAtom.luau b/src/react/useSetAtom.luau index e6686fd..8d3ff7b 100644 --- a/src/react/useSetAtom.luau +++ b/src/react/useSetAtom.luau @@ -6,8 +6,8 @@ type Atom = types.Atom export type SetState = (T | ((T) -> T)) -> () local function useSetAtom(atom: Atom): SetState - return requireReact().useCallback(function(value) - atom(value) + return requireReact().useCallback(function(state: State | ((State) -> State)): () + atom(state) end, { atom }) end diff --git a/src/store.luau b/src/store.luau index abcba41..215c3d6 100644 --- a/src/store.luau +++ b/src/store.luau @@ -1,29 +1,14 @@ local types = require(script.Parent.types) type Atom = types.Atom -type ReadonlyAtom = types.ReadonlyAtom +type Source = types.Source type WeakMap = types.WeakMap local listeners: WeakMap, WeakMap<() -> (), unknown>> = setmetatable({}, { __mode = "k" }) local captured: { [Atom]: true } = {} local capturing = false -local function capture(source: ReadonlyAtom): ({ [Atom]: 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, } diff --git a/src/subscribe.luau b/src/subscribe.luau index c0e55da..e1d3786 100644 --- a/src/subscribe.luau +++ b/src/subscribe.luau @@ -1,14 +1,15 @@ +local capture = require(script.Parent.capture) local store = require(script.Parent.store) local types = require(script.Parent.types) -type ReadonlyAtom = types.ReadonlyAtom +type Source = types.Source -local function subscribe(source: ReadonlyAtom, callback: (state: T, previous: T) -> ()): () -> () - local atoms, state = store.capture(source) +local function subscribe(source: Source, 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 diff --git a/src/sync/client.luau b/src/sync/client.luau index b82ca14..d52d4fd 100644 --- a/src/sync/client.luau +++ b/src/sync/client.luau @@ -1,10 +1,9 @@ -local atomTypes = require(script.Parent.Parent.types) -type Atom = atomTypes.Atom -local patch = require(script.Parent.patch) -local types = require(script.Parent.types) +local types = require(script.Parent.Parent.types) +type Atom = types.Atom type SyncPayload = types.SyncPayload +local patch = require(script.Parent.patch) -type ClientSyncOptions = { +type ClientOptions = { atoms: { [string]: Atom }, } @@ -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 diff --git a/src/sync/init.luau b/src/sync/init.luau index d1a2d3b..ee75671 100644 --- a/src/sync/init.luau +++ b/src/sync/init.luau @@ -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, diff --git a/src/sync/patch.luau b/src/sync/patch.luau index eb5b76c..df2eda5 100644 --- a/src/sync/patch.luau +++ b/src/sync/patch.luau @@ -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 diff --git a/src/sync/server.luau b/src/sync/server.luau index e8e43a9..db6645e 100644 --- a/src/sync/server.luau +++ b/src/sync/server.luau @@ -1,26 +1,26 @@ local Players = game:GetService("Players") -local atomTypes = require(script.Parent.Parent.types) -type Atom = atomTypes.Atom +local types = require(script.Parent.Parent.types) +type Atom = types.Atom +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 }, 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 = { - 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 + sync(player, { + type = "patch", + 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", diff --git a/src/sync/types.luau b/src/sync/types.luau deleted file mode 100644 index ca27e02..0000000 --- a/src/sync/types.luau +++ /dev/null @@ -1,6 +0,0 @@ -export type SyncPayload = { - type: "patch" | "set", - data: { [any]: any }, -} - -return nil diff --git a/src/types.luau b/src/types.luau index 5992614..ee08b65 100644 --- a/src/types.luau +++ b/src/types.luau @@ -1,5 +1,16 @@ -export type Atom = (() -> T) & ((T | (T) -> T) -> T) -export type ReadonlyAtom = () -> T export type WeakMap = typeof(setmetatable({} :: { [K]: V }, { __mode = "k" })) +export type Atom = (() -> T) & (state: T | (T) -> T) -> T + +export type Source = () -> T + +export type AtomOptions = { + equals: (prev: T, next: T) -> boolean, +} + +export type SyncPayload = { + type: "patch" | "set", + data: { [any]: any }, +} + return nil diff --git a/src/unwrap.luau b/src/unwrap.luau new file mode 100644 index 0000000..390188e --- /dev/null +++ b/src/unwrap.luau @@ -0,0 +1,23 @@ +local store = require(script.Parent.store) + +local function unwrap(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 diff --git a/src/utils/reference.luau b/src/utils/reference.luau deleted file mode 100644 index de4cdca..0000000 --- a/src/utils/reference.luau +++ /dev/null @@ -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(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, -}