{"id":"coffilhg/namespacesync","name":"namespacesync","scope":"coffilhg","platform":"roblox","description":"This module is a great utility if you need 'singletone' Instances, e.g. your Client and Server modules need a shared folder for their Project state or your custom Plugin should always output in a predictable place.","version":"0.0.1","latest":"0.0.1","versions":["0.0.0","0.0.1"],"license":"MPL-2.0","licenseRating":"caution","licenseCaveats":["File-level copyleft: if you modify this package's own source files, those modified files must be made available under MPL-2.0. Using it unmodified in a closed-source game is fine."],"licenseVerified":true,"dependencies":{"coffilhg/classnametypepairs":{"version":">=0.0.0","alias":"ClassNameTypePairs"},"data-oriented-house/lemonsignal":{"version":"^2.0.0","alias":"LemonSignal"}},"integrity":"94825e4b125191f9b4223a9b08c68d000d164711d0b3f798e1f46439f790f315","likes":0,"downloads":0,"install":"forest install coffilhg/namespacesync","url":"https://forest.dev/p/roblox/coffilhg/namespacesync","files":"https://api.forest.dev/ai/package/roblox/coffilhg/namespacesync/files","readme":"# NamespaceSync\r\n\r\nInspired by **[CoffeeRemotesRefined](<https://github.com/Coffilhg/Useful-Modules/tree/CoffeeRemotesRefined>)**, where similar approach was first invented and used.\r\n\r\nThis module is a great utility if you need \"singleton-like\" (deduplicated) Instances, e.g. your Client and Server modules need a shared folder for their Project state or your custom Plugin should always output in a predictable place.\r\n\r\nThis module will take care of this! **[Read below...](#basic-usage)**\r\n\r\n\r\n\r\n## Available Here!\r\n- **[This repository](src/init.luau) ~ [src/init.luau](src/init.luau)**\r\n- **[Wally](<https://wally.run/package/coffilhg/namespacesync>)**\r\n\r\n    ```toml\r\n    NamespaceSync = \"coffilhg/namespacesync@0.0.1\"\r\n    ```\r\n- **[Rotriever](<https://github.com/Coffilhg/Useful-Modules/releases/tag/vNamespaceSync/0.0.1>)**\r\n\r\n    ```toml\r\n    NamespaceSync = \"github.com/Coffilhg/Useful-Modules@NamespaceSync/0.0.1\"\r\n    ```\r\n\r\n\r\n\r\n---\r\n\r\n## Basic Usage\r\n\r\n### What is a [namespace](<https://en.wikipedia.org/wiki/Namespace>)\r\nClick the link and find out.\r\n\r\nIn general this README and the module refer to namespace / descriptor as the following definition:\r\n- Name + ClassName + metadata => the Instance identity\r\n- `where` + `isRecursive` => search scope and strategy\r\n\r\nwhere metadata is either the default one provided, or a custom set via Validator and Mutator modifiers. Such custom set metadata can also be none.\r\n\r\n### Require the module\r\n> all uses of `NamespaceSync` in code examples below refer to this\r\n```luau\r\nconst NamespaceSync = require(`@game/ReplicatedStorage/Packages/NamespaceSync`)\r\n```\r\n\r\n### Use the module\r\nProvided a name and project version string, it will find an existing or create a new Folder (or instance of a given ClassName) with the following:\r\n- Name = input name\r\n- ClassName = Folder or whichever was input\r\n- Tag = `{name}_{projectVersion}`\r\n- Attribute `{name}V` = `{name}_{projectVersion}`\r\n> The attribute name will be made safe automatically to comply with [these Limits](<https://create.roblox.com/docs/en-us/reference/engine/classes/Instance#SetAttribute>)\r\n\r\nIf multiple matching Instances are found, the first match`*` is returned, all further matches are parented to nil and scheduled for destruction via `Debris:AddItem()` in 180 seconds. And a warning function (by default it's Roblox' `warn`) is called with references to the destroyed Instances. You can overwrite the warning function callback via `NamespaceSync:SetWarnCallback`.\r\n\r\n> \"the first match`*`\" - means the first encountered during `GetChildren` or `GetDescendants` iteration, not necessarily deterministic/ordered like oldest/newest/etc.  \r\n\r\n> The module doesn't enforce a universal singleton. It deduplicates matching instances within the searched `where` instance, based on name/class + optional validation.\r\n\r\nThis describes the initial idea this served, on server-side you'd do\r\n```lua\r\n-- making a Folder\r\nconst MainFolder = NamespaceSync:mkdir(\r\n    \"1.2.3\", -- projectVersion\r\n\t\"ReplicationService\", -- name\r\n\t-- where: Instance? -- this is where to search for the Instance with matching specifications and where a new one will be parented in case no matches are found; defaults to ReplicatedStorage\r\n\t-- isRecursive: boolean?, -- this is whether we should search all DESCENDANTS (:GetDescendants) of where, defaults to false and searches only the direct children (:GetChildren)\r\n\t-- debugPrefix: (string | any)? -- every warning emitted by the module starts with `{debugPrefix} `\r\n)\r\n```\r\nand then on client-side you'd indefinitely await for the folder until server creates it (this yields similarly to `:WaitForChild`, while also supporting custom validation and recursive searches)\r\n> the projectVersion and name must be matching\r\n```lua\r\n-- awaiting a Folder\r\nconst MainFolder = NamespaceSync:cd(\r\n    \"1.2.3\", -- projectVersion\r\n\t\"ReplicationService\", -- name\r\n\t-- where: Instance? -- same as above\r\n\t-- isRecursive: boolean?, -- same as above + it also determines `:FindFirstX` and `.XAdded:Connect` usage. The point is the same: if isRecursive - search and await a matching descendant of where, otherwise (false or default) - search and await a matching child of where\r\n\t-- debugPrefix: (string | any)? -- same as above\r\n    -- yieldWarningTime: number? -- imagine you made a typo somewhere and now it silently yields (waits) indefinitely (forever),\r\n    -- yieldWarningTime got you covered, if nothing is found in yieldWarningTime seconds, a warning is emitted, defaults to 15\r\n)\r\n```\r\nnow parent any instances inside, you can be almost certain this never overlaps with modules or projects of other developers and different versions of your own module/project.\r\n\r\nThis is especially good if dependencies are not deduplicated: they get automatic isolation between versions, provided their namespace descriptors do not intentionally collide.\r\n\r\nFor a further example, see **[CoffeeRemotesRefined](<https://github.com/Coffilhg/Useful-Modules/tree/CoffeeRemotesRefined>)**, the server-side init.luau and client-side init.luau (the links are there at the top of its README)\r\n> as of now, CoffeeRemotesRefined latest is not yet using NamespaceSync, but is using its prototype. This line will be removed once it is updated to use NamespaceSync.\r\n\r\n\r\n\r\nAnd maybe you don't want a **Folder**, maybe you want a **Model** or an **ArcHandles** for whichever reason, sure, anything that **Instance.new** supports is supported, just use\r\n```lua\r\n-- making your whatever ClassName\r\nconst MainWhatever = NamespaceSync:GetOrCreate(\r\n    className, -- any class name supported by Instance.new\r\n    -- everything else as in :mkdir above, i.e.\r\n    projectVersion, -- obligatory, same as above\r\n    name, -- obligatory, same as above\r\n    where, -- optional, same as above\r\n    isRecursive, -- optional, same as above\r\n    debugPrefix -- optional, same as above\r\n)\r\n```\r\nand in a different script\r\n> make sure everything is matching! (name, projectVersion, ClassName, how and where it's parented, i.e. matching `where` and `isRecursive`)\r\n```lua\r\n-- awaiting for your whatever ClassName\r\nconst MainWhatever = NamespaceSync:Await(\r\n    className, -- value .ClassName property of the object you created using :GetOrCreate\r\n    -- everything else as in :cd above, i.e.\r\n    projectVersion, -- obligatory, same as above\r\n    name, -- obligatory, same as above\r\n    where, -- optional, same as above\r\n    isRecursive, -- optional, same as above\r\n    debugPrefix, -- optional, same as above\r\n    yieldWarningTime -- optional, same as above\r\n)\r\n```\r\n\r\nSo you end up with name + ClassName + a tag + a specific attribute name with a specific value\r\n\r\nSuch combination makes it quite unique, but if that's not enough or too much (e.g. you only want the name + ClassName without tag & attribute metadata), this module still has you covered:\r\n\r\n### Use it more\r\n\r\nAll GetOrCreate and Await variants (including mkdir and cd) always validate for name + ClassName.\r\n> (And validate indirectly for the `where` parent + in some cases whether `isRecursive` is used, because these two define where and how the search is performed)\r\n\r\nYou can set custom modifiers or keep default of no modifiers to get name + ClassName, (where and how it's parented too, but) nothing else.\r\n\r\nMore in detail about modifiers below.\r\n\r\n```lua\r\nconst YouNameIt = NamespaceSync:GetOrCreateManual(\r\n    className,\r\n    name,\r\n    where,\r\n    modifiers,\r\n    isRecursive,\r\n    debugPrefix\r\n)\r\n```\r\n\r\n```lua\r\nconst YouNameIt = NamespaceSync:AwaitManual(\r\n    className,\r\n    name,\r\n    where,\r\n    modifiers,\r\n    isRecursive,\r\n    debugPrefix,\r\n    yieldWarningTime\r\n)\r\n```\r\n\r\nThere are two types of modifiers. A validator and a mutator.\r\n\r\nYou can use none, either one of or both of them for `:GetOrCreateManual`.\r\n\r\nYou can use a validator or nothing for `:AwaitManual`.\r\n\r\nThe type definitions are as follows:\r\n\r\n```lua\r\nexport type ValidatorModifier = (Instance) -> boolean\r\nexport type MutatorModifier = (Instance) -> ()\r\nexport type GetOrCreateModifiers = {\r\n\tValidator: ValidatorModifier?,\r\n\tMutator: MutatorModifier?,\r\n}\r\nexport type AwaitModifiers = {\r\n\tValidator: ValidatorModifier?,\r\n}\r\n```\r\n\r\n- **Validator** callback\r\n  Every time a match by name + ClassName is found, it is passed to the validator callback if given. A boolean return value is expected for the callback.\r\n\r\n  `true` means \"this instance matches all of our additional criteria on top of name + ClassName\", `false` means \"this isn't a match, keep searching\".\r\n  \r\n  For an example of such you can see `:GetOrCreate` ([here](<https://github.com/Coffilhg/Useful-Modules/blob/NamespaceSync/src/init.luau#:~:text=const%20function%20GetOrCreate>)) and `:Await` ([here](<https://github.com/Coffilhg/Useful-Modules/blob/NamespaceSync/src/init.luau#:~:text=const%20function%20Await>)) methods, they implement tag + attribute metadata on top of name + ClassName\r\n\r\n- **Mutator** callback\r\n  It is the opposite of the **Validator**, this one is only used by `:GetOrCreate` methods, only in case there were no matches found.\r\n  \r\n  It is called with a newly created Instance, before the Instace is parented. Mutate the Instance in a way it passes your **Validator**.\r\n\r\n  For an example of such you can see `:GetOrCreate` ([here](<https://github.com/Coffilhg/Useful-Modules/blob/NamespaceSync/src/init.luau#:~:text=const%20function%20GetOrCreate>))\r\n\r\n---\r\n\r\n## Notes\r\n\r\n1. As already stated in **[Server Unit tests of CoffeeRemotesRefined](<https://github.com/Coffilhg/Useful-Modules/blob/CoffeeRemotesRefined/src/Test/ServerScriptService/ServerMain/init.server.luau>)**:\r\n    > **things left untested**\r\n    > \r\n    > \"mkdir and mkremote tested for exact spoofing at runtime;\r\n    > That would likely fail, unless we over-engineer and introduce scoring for folders and checks for usage in existing remote objects. ...\"\r\n\r\n    > (tldr: this module does not guarantee runtime stability if it is poorly used)\r\n\r\n    i.e. if multiple scripts misuse (e.g. reuse same descriptors) or spoof the namespace metadata, the module does not guarantee logical correctness of \"keep the actually used Instance\", because\r\n    \r\n    A: that'll be overengineering\r\n\r\n    B: it isn't CoffeeRemotesRefined with a clear structure of Folders that store RemoteInstances of a specific kind, since this module allows any Instances creatable via `Instance.new`, we also have no clarity over what exactly to count toward a match's score, even if we tried to implement a scoring based deduplication.\r\n\r\n\r\n\r\n    NamespaceSync is primarily intended for startup-time creation and/or \"one creates, everyone else awaits\" pattern. Also this isn't a scary limitation:\r\n\r\n    The following two are actually resolved silently\r\n    - Same project, different versions? - each gets their own \"Main\" Instance\r\n    - Different projects, same version? - if their names don't overlap, they're isolated. If they use the same name/class and search scope, they will resolve to the same matching Instance rather than NamespaceSync destroying one of them, the module doesn't know they're different projects, it simply sees identical descriptors. Well because, just read further...\r\n    \r\n    This is only a concern when you have Spoofed the Instances, so that there are multiple matches. The module itself will never produce such deduplication, it will however eliminate such, if detected during any of GetOrCreate variations (including mkdir)\r\n\r\n2. It is advised, if you think your project name is commonly used, to include a UUID in either your project' name or projectVersion, although the latter is better kept SemVer / bare version string.\r\n\r\n    **For example, using `{projectName}_{UUID}` as the name for created Instances makes accidental collisions with other projects using NamespaceSync extremely unlikely.** Deliberate spoofing is still possible. This module just [makes risky activities less risky, because you'd do it anyways](<https://medium.com/@sdboyer/so-you-want-to-write-a-package-manager-4ae9c17d9527#:~:text=People%20are%20going%20to%20do%20risky%20activities.%20Instead%20of%20saying%20YOU%E2%80%99RE%20WRONG%20TO%20DO%20THAT%20JUST%20DON%E2%80%99T%20DO%20THAT,%20we%20can%20choose%20to%20help%20make%20those%20activities%20less%20risky.>), you keep your freedom.\r\n\r\n    This UUID idea comes from how Minecraft Bedrock Add-Ons are created.\r\n\r\n### Why do default `:GetOrCreate` and `:Await` use the metadata they use\r\n\r\nHaving an instance have a distinct (1) **Name**, (2) **ClassName**, (3) **Tag** and (4 & 5) **Attribute**, with both highly specific name and value; (6) **Parent**`*` and in some cases even (7) **whether isRecursive is used**`*` - this is more than enough to give an Instance a highly specific identity within the selected search scope, making it \"`*`*Unique*`*`\". Magic!\r\n\r\n> **Parent**`*` & **whether isRecursive is used**`*` aren't directly validated, but they define the scope and method of searching, which is important.\r\n\r\nWhy tag and attribute were chosen? They replicate with the Instance, making them useful for communicating the namespace identity between server-client and are unlikely to be changed/impacted by other scripts during runtime. If some obscure/usually-unused properties were chosen instead, there'd be a higher risk of them changing, making the initially \"Unique\" matching Instance, stop matching until the initial attributes of it's \"Uniqueness\" are fully recovered.\r\n\r\nFeels like too much or too little? Use `:GetOrCreateManual` and `:AwaitManual` with custom modifier options, this is documented [above](#use-it-more)\r\n\r\n\r\n---\r\n\r\n## DEPENDENCIES\r\n\r\n- [ClassNameTypePairs](<https://github.com/Coffilhg/ClassNameTypePairs>)\r\n- [LemonSignal](<https://github.com/Data-Oriented-House/LemonSignal>)\r\n\r\n---\r\n\r\n## License & Attribution\r\n\r\nThis module is licensed under the **Mozilla Public License 2.0 (MPL-2.0)**.\r\n\r\n#### What this means for Roblox Developers:\r\n* **Use & Modify:** You can freely use this module in any public, private, or commercial Roblox game.\r\n* **File-Level Copyleft:** If you modify the source code of this module itself, you must make your modified version of the module publicly available under the MPL 2.0.\r\n* **No Viral Code Leakage:** Including this module in your game does **not** force you to open-source your other game scripts, UI layouts, or proprietary codebase. \r\n\r\nSee the full terms in the [LICENSE](LICENSE) file.\r\n\r\nAttribution to all dependencies is included in [Notice](NOTICE)\r\n\r\nCopyright © 2026 @Coffilhg (Roblox UserId 517222346)\r\n","readmeTruncated":false}