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)`
|
#### `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)`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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)
|
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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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 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
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 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
138
src/index.d.ts
vendored
|
|
@ -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>>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 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
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