diff --git a/packages/charm-sync/src/flatten.luau b/packages/charm-sync/src/flatten.luau new file mode 100644 index 0000000..9c8d012 --- /dev/null +++ b/packages/charm-sync/src/flatten.luau @@ -0,0 +1,28 @@ +local types = require(script.Parent.types) +type AtomMap = types.AtomMap + +type NestedAtomMap = { + [string]: NestedAtomMap | () -> (), +} + +local function flatten(atoms: NestedAtomMap): AtomMap + local result: AtomMap = {} + + local function visit(node: NestedAtomMap, path: string) + for key, value in node do + local location = if path == "" then key else path .. "/" .. key + + if type(value) == "table" then + visit(value, location) + else + result[location] = value + end + end + end + + visit(atoms, "") + + return result +end + +return flatten diff --git a/packages/charm-sync/src/index.d.ts b/packages/charm-sync/src/index.d.ts index 41e88a5..dfd8abf 100644 --- a/packages/charm-sync/src/index.d.ts +++ b/packages/charm-sync/src/index.d.ts @@ -5,6 +5,8 @@ export as namespace CharmSync; type Cleanup = () => void; +type Key = string | number | symbol; + declare namespace CharmSync { type AtomMap = Record>; @@ -60,6 +62,39 @@ declare namespace CharmSync { */ function isNone(value: unknown): value is None; + interface NestedAtomMap { + readonly [key: string]: NestedAtomMap | Atom; + } + + type AppendPath = undefined extends Path + ? Name + : Name extends string + ? Path extends string + ? `${Path}/${Name}` + : never + : never; + + type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + + type IntersectValues = UnionToIntersection<{ readonly [K in keyof T]: T[K] }[keyof T]>; + + type FlattenNestedAtoms = IntersectValues<{ + readonly [Name in keyof Atoms as AppendPath]: Atoms[Name] extends Atom + ? { readonly [K in AppendPath]: Atoms[Name] } + : Atoms[Name] extends NestedAtomMap + ? FlattenNestedAtoms> + : never; + }>; + + /** + * Flattens a nested atom map into a single object with slash-separated + * keys. Useful for recursively collecting atoms returned by modules. + * + * @param atoms The nested atom map to flatten. + * @return A flattened atom map. + */ + function flatten(atoms: Atoms): FlattenNestedAtoms; + type DataTypes = { [P in keyof CheckableTypes as P extends keyof CheckablePrimitives ? never : P]: CheckableTypes[P]; }; diff --git a/packages/charm-sync/src/init.luau b/packages/charm-sync/src/init.luau index b57c18f..5fc9171 100644 --- a/packages/charm-sync/src/init.luau +++ b/packages/charm-sync/src/init.luau @@ -1,4 +1,5 @@ local client = require(script.client) +local flatten = require(script.flatten) local patch = require(script.patch) local server = require(script.server) local types = require(script.types) @@ -12,5 +13,6 @@ export type SyncPayload = types.SyncPayload return { client = client, server = server, + flatten = flatten, isNone = patch.isNone, } diff --git a/tests/charm-sync/flatten.spec.luau b/tests/charm-sync/flatten.spec.luau new file mode 100644 index 0000000..c274d05 --- /dev/null +++ b/tests/charm-sync/flatten.spec.luau @@ -0,0 +1,24 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Charm = require(ReplicatedStorage.DevPackages.Charm) +local atom = Charm.atom +local CharmSync = require(ReplicatedStorage.DevPackages.CharmSync) +local flatten = CharmSync.flatten + +return function() + it("flattens atoms", function() + local input = { + a = atom(0), + b = { c = atom(0) }, + d = { e = { f = atom(0) } }, + } + local result = flatten(input) + + expect(result.a).to.equal(input.a) + expect(result["b/c"]).to.equal(input.b.c) + expect(result["d/e/f"]).to.equal(input.d.e.f) + + expect(result.b).to.never.be.ok() + expect(result.d).to.never.be.ok() + end) +end