npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hexium-softworks/replicationservice

v0.1.0

Published

Game-agnostic Nevermore service for server-authoritative structured state replication.

Readme

ReplicationService

Server-authoritative state replication for Nevermore Roblox games.

ReplicationService gives gameplay packages a clean state API instead of making each package create RemoteEvents, send snapshots, track audiences, validate payloads, and clean everything up by hand. The server owns the truth. Clients receive read-only snapshots and patches for the states they are allowed to see.

Good fits:

  • player data shown to the owning player
  • round, match, or lobby state
  • objectives, quests, tasks, and tutorials
  • cooldowns and timers
  • hotbar, loadout, and inventory UI state
  • status effects and buffs
  • interactable and world-object state
  • VFX/world cues such as positions, transforms, colors, and ranges
  • public player info
  • global or team UI state

Not this package:

  • persistence
  • DataStore/ProfileService integration
  • combat, inventory, currency, or quest rules
  • UI rendering
  • client prediction
  • cross-server messaging
  • general RPC or command handling

Gameplay requests still belong to the owning services. A client should ask ShopService to buy an item; ShopService validates the request, updates authoritative state, and then ReplicationService replicates the result.

Install

pnpm add @hexium-softworks/replicationservice

This package follows Nevermore conventions:

  • https://quenty.github.io/NevermoreEngine/docs/architecture/
  • https://quenty.github.io/NevermoreEngine/docs/architecture/servicebag/
  • https://quenty.github.io/NevermoreEngine/api/Remoting/
  • https://quenty.github.io/NevermoreEngine/api/Observable/
  • https://quenty.github.io/NevermoreEngine/api/Promise/

Setup

Server service:

local require = require(script.Parent.loader).load(script)

local ReplicationService = require("ReplicationService")

local PlayerDataService = {}
PlayerDataService.ServiceName = "PlayerDataService"

function PlayerDataService:Init(serviceBag)
	self._replicationService = serviceBag:GetService(ReplicationService)
end

return PlayerDataService

Client service:

local require = require(script.Parent.loader).load(script)

local ReplicationServiceClient = require("ReplicationServiceClient")

local PlayerHudController = {}
PlayerHudController.ServiceName = "PlayerHudController"

function PlayerHudController:Init(serviceBag)
	self._replicationService = serviceBag:GetService(ReplicationServiceClient)
end

return PlayerHudController

Use ServiceBag:GetService() for both services. Do not call methods directly on the module table.

Mental Model

On the server, create a state:

local state = replicationService:CreateState({
	Id = "RoundState",
	InitialState = {
		Phase = "Waiting",
		TimeRemaining = 0,
	},
	Audience = replicationService.Audiences.All,
})

Mutate it on the server:

state:Set("Phase", "Playing")
state:Set("TimeRemaining", 120)
state:Increment("TimeRemaining", -1)

Read it on the client:

replicationServiceClient:PromiseState("RoundState"):Then(function(roundState)
	maid:GiveTask(roundState:Observe("TimeRemaining"):Subscribe(function(timeRemaining)
		timerLabel.Text = tostring(timeRemaining)
	end))
end)

The client cannot call Set, Increment, Delete, or Batch. Client objects are read-only caches.

Common Use Cases

Player Data

Use this for profile fields the owning player is allowed to see. Persistence and profile reconciliation stay in your player data package.

Server:

local clientState = replicationService:CreateState({
	Id = `PlayerData:{player.UserId}`,
	InitialState = {
		Currency = {
			Coins = profile:Get("Currency.Coins"),
			Gems = profile:Get("Currency.Gems"),
		},
		Progression = {
			Level = profile:Get("Progression.Level"),
			Experience = profile:Get("Progression.Experience"),
			RequiredExperience = profile:Get("Progression.RequiredExperience"),
		},
	},
	Audience = player,
})

profile:Increment("Currency.Coins", 100)
clientState:Set("Currency.Coins", profile:Get("Currency.Coins"))

Client:

replicationServiceClient:PromiseState(`PlayerData:{localPlayer.UserId}`)
	:Then(function(playerData)
		maid:GiveTask(playerData:Observe("Currency.Coins"):Subscribe(function(coins)
			coinsLabel.Text = tostring(coins)
		end))

		maid:GiveTask(playerData:ObserveSelector(function(snapshot)
			return snapshot.Progression.Experience / snapshot.Progression.RequiredExperience
		end):Subscribe(function(progress)
			xpBar.Size = UDim2.fromScale(progress, 1)
		end))
	end)

Only put approved client-facing fields into the replicated state. Do not mirror the whole private profile unless the whole profile is safe for that player to see.

Round Or Match State

Use a global audience for state all players should see.

local roundState = replicationService:CreateState({
	Id = "RoundState",
	InitialState = {
		Phase = "Intermission",
		TimeRemaining = 30,
		MapName = nil,
	},
	Audience = replicationService.Audiences.All,
})

roundState:Batch(function(transaction)
	transaction:Set("Phase", "Playing")
	transaction:Set("TimeRemaining", 180)
	transaction:Set("MapName", "Factory")
end)

Client:

replicationServiceClient:PromiseState("RoundState"):Then(function(roundState)
	maid:GiveTask(roundState:Observe("Phase"):Subscribe(function(phase)
		phaseLabel.Text = phase
	end))

	maid:GiveTask(roundState:Observe("TimeRemaining"):Subscribe(function(seconds)
		timerLabel.Text = tostring(seconds)
	end))
end)

Objectives And Quests

Use one state per player when visibility is personal.

local objectiveState = replicationService:CreateState({
	Id = `Objectives:{player.UserId}`,
	InitialState = {
		Active = {},
		TrackedId = nil,
	},
	Audience = player,
})

objectiveState:Set({ "Active", questId }, {
	Title = "Restore Power",
	Current = 1,
	Required = 3,
})
objectiveState:Set("TrackedId", questId)

Client:

objectiveState:ObserveSelector(function(snapshot)
	local trackedId = snapshot.TrackedId
	return if trackedId then snapshot.Active[trackedId] else nil
end):Subscribe(function(objective)
	if objective then
		objectiveLabel.Text = `{objective.Title}: {objective.Current}/{objective.Required}`
	end
end)

Cooldowns And Timers

Replicate coarse state, not per-frame values unless you really need them.

local cooldownState = replicationService:CreateState({
	Id = `Cooldowns:{player.UserId}`,
	InitialState = {
		Abilities = {},
	},
	Audience = player,
})

cooldownState:Set({ "Abilities", "Dash" }, {
	ReadyAt = workspace:GetServerTimeNow() + 4,
	Duration = 4,
})

The client can render a local countdown from ReadyAt and Duration without the server sending a patch every frame.

Hotbar And Inventory UI

Replicate UI-ready state. Keep authoritative item ownership and equip rules in your inventory service.

local hotbarState = replicationService:CreateState({
	Id = `Hotbar:{player.UserId}`,
	InitialState = {
		Slots = {
			[1] = { ItemId = "WoodSword", Count = 1 },
			[2] = { ItemId = "HealthPotion", Count = 3 },
		},
		SelectedSlot = 1,
	},
	Audience = player,
})

hotbarState:Set({ "Slots", 2, "Count" }, 2)
hotbarState:Set("SelectedSlot", 2)

Status Effects

local statusState = replicationService:CreateState({
	Id = `StatusEffects:{player.UserId}`,
	InitialState = {
		Effects = {},
	},
	Audience = player,
})

statusState:Set({ "Effects", "Poison" }, {
	Stacks = 2,
	ExpiresAt = workspace:GetServerTimeNow() + 8,
})

statusState:Delete({ "Effects", "Poison" })

Interactables And World Objects

Use explicit audiences when only nearby players should see a state.

local generatorState = replicationService:CreateState({
	Id = "World.Generator.A",
	InitialState = {
		Powered = false,
		Health = 100,
		Prompt = "Repair",
	},
	Audience = replicationService.Audiences.None,
})

zone.PlayerEntered:Connect(function(player)
	generatorState:AddPlayer(player)
end)

zone.PlayerLeft:Connect(function(player)
	generatorState:RemovePlayer(player)
end)

generatorState:Set("Powered", true)

Removing a player from the audience destroys that client copy and stops future updates.

VFX And World Cues

ReplicationService can carry immutable Roblox value datatypes. This is useful for VFX cues or world state that needs positions, transforms, colors, and ranges.

local stormState = replicationService:CreateState({
	Id = "WorldEvent.Storm",
	InitialState = {
		Active = false,
		Center = Vector3.zero,
		Radius = 0,
		Color = Color3.fromRGB(120, 180, 255),
	},
	Audience = replicationService.Audiences.All,
})

stormState:Batch(function(transaction)
	transaction:Set("Active", true)
	transaction:Set("Center", Vector3.new(100, 12, -40))
	transaction:Set("Radius", 80)
	transaction:Set("Color", Color3.fromRGB(80, 140, 255))
end)

For one-shot effects, keep using an owning VFX service or remote. This package is better for replicated state that has a current value.

Team Or Group UI

ReplicationService does not know what a team, party, or squad is. Let your team/party service decide membership and feed ReplicationService players.

local teamState = replicationService:CreateState({
	Id = `Team:{teamId}`,
	InitialState = {
		Score = 0,
		Objective = "Capture",
	},
	Audience = {},
})

for _, player in teamService:GetPlayers(teamId) do
	teamState:AddPlayer(player)
end

teamService.PlayerAddedToTeam:Connect(function(player, changedTeamId)
	if changedTeamId == teamId then
		teamState:AddPlayer(player)
	end
end)

Server API

Create A State

local state = replicationService:CreateState({
	Id = "UniqueStateId",
	InitialState = {},
	Audience = replicationService.Audiences.None,
})

Id must be unique while the state exists. InitialState must pass validation. Audience can be:

  • replicationService.Audiences.All
  • replicationService.Audiences.None
  • one Player
  • an array of Player instances

Mutate State

state:Get("Path.To.Value")
state:GetSnapshot()

state:Set("Path.To.Value", 10)
state:Update("Path.To.Value", function(value)
	return value + 1
end)
state:Increment("Path.To.Value", 1)
state:Delete("Path.To.Value")

Use Delete(path) instead of Set(path, nil).

Batch Changes

state:Batch(function(transaction)
	transaction:Set("Phase", "Playing")
	transaction:Increment("Score.Red", 1)
	transaction:Delete("PendingVote")
end)

Batches publish one coherent revision. If the callback errors, no staged changes are committed.

Observe On The Server

maid:GiveTask(state:Observe("Score.Red"):Subscribe(function(score)
	scoreCache.Red = score
end))

Server observers are useful for package-to-package integration. Avoid direct print in production package code; route diagnostics through your logger.

Client API

Get Or Wait For State

local state = replicationServiceClient:GetState("RoundState")

replicationServiceClient:PromiseState("RoundState"):Then(function(roundState)
	-- ready to read
end)

GetState returns nil until the server has authorized and initialized that state. PromiseState is usually friendlier for UI startup.

Read Values

local phase = state:Get("Phase")
local snapshot = state:GetSnapshot()

Snapshots are cloned and frozen. Accidental client mutation fails locally, but security still comes from the server owning all authoritative state.

Observe Values

maid:GiveTask(state:Observe("Phase"):Subscribe(function(phase)
	phaseLabel.Text = phase
end))

maid:GiveTask(state:ObserveSnapshot():Subscribe(function(snapshot)
	render(snapshot)
end))

maid:GiveTask(state:ObserveSelector(function(snapshot)
	return snapshot.Currency.Coins
end):Subscribe(function(coins)
	coinsLabel.Text = tostring(coins)
end))

Observers emit the current value once the state is ready, emit again only when the observed value changes, and complete when the state is destroyed.

Paths

Paths can be dotted strings or arrays:

state:Set("Currency.Coins", 100)
state:Set({ "Currency", "Coins" }, 100)

Arrays are useful when a segment is dynamic:

state:Set({ "Inventory", itemId, "Count" }, 3)

Path segments are normalized internally.

Security

Clients cannot ask for arbitrary state and receive it. The server decides visibility by audience membership and sends only authorized states.

Do not put secrets into replicated state. If a client receives it, the client can inspect it. Use separate state objects when different players should see different data.

Do not use ReplicationService as an RPC command layer. Client requests should go to the service that owns the action:

-- Good shape:
-- Client -> ShopService:RequestPurchase(itemId)
-- ShopService validates
-- ShopService updates profile
-- ShopService updates replicated player state

Validation And Supported Values

ReplicationService rejects malformed state before replication. It validates:

  • state ids
  • paths
  • replicated value types
  • cyclic tables
  • non-finite numbers
  • excessive depth, node count, string length, and path length
  • duplicate state ids
  • mutations after destroy

Supported values:

  • nil, booleans, finite numbers, and strings
  • structured tables with string or numeric keys
  • Vector2, Vector3, CFrame, Color3
  • UDim, UDim2, Rect
  • NumberRange, NumberSequence, ColorSequence
  • BrickColor

Rejected values include Instance, functions, threads, buffers, cyclic tables, and mutable userdata.

Performance Notes

ReplicationService sends one initial snapshot per authorized player and then patches for ordinary mutations.

Prefer:

  • batching related changes
  • coarse timer data such as ReadyAt instead of per-frame updates
  • separate state objects for different visibility groups
  • selectors for UI values that should only re-render when their result changes

Avoid:

  • replicating huge tables as one state
  • sending high-frequency per-frame state unless you have profiled it
  • putting private server-only data in client-visible state

Lifecycle And Cleanup

Every state has Destroy(). Destroying a server state sends destroy messages to authorized clients and removes registry entries. Removing a player from an audience destroys that client copy.

Own client subscriptions with a Maid:

maid:GiveTask(state:Observe("Value"):Subscribe(function(value)
	-- update UI
end))

Reference

ReplicationService:

| Method | Description | | --- | --- | | CreateState(config) | Creates a server-owned state. | | GetState(stateId) | Returns a server state or nil. | | GetStateIds() | Returns sorted state ids. | | ObserveStateIds() | Observes server state id registry changes. |

ReplicatedState:

| Method | Description | | --- | --- | | Get(path?) | Gets a cloned value. | | GetSnapshot() | Gets a frozen snapshot. | | Set(path, value) | Sets a value and publishes a patch. | | Update(path, callback) | Computes and sets a value. | | Increment(path, amount?) | Adds to a numeric value. | | Delete(path) | Removes a value. | | Batch(callback) | Publishes grouped changes atomically. | | AddPlayer(player) | Adds an explicit audience member. | | RemovePlayer(player) | Removes an explicit audience member. | | SetAudience(audience) | Replaces audience membership. | | Observe(path) | Observes a path locally on the server. | | ObserveSnapshot() | Observes server snapshots. | | Destroy() | Destroys state and client copies. |

ReplicationServiceClient:

| Method | Description | | --- | --- | | GetState(stateId) | Returns an authorized local state or nil. | | PromiseState(stateId) | Resolves when an authorized state is ready. | | ObserveState(stateId) | Observes local state availability. | | GetStateIds() | Returns sorted authorized state ids. | | ObserveStateIds() | Observes authorized state id changes. |

ReadonlyReplicatedState:

| Method | Description | | --- | --- | | IsReady() | Returns whether initial data is available. | | PromiseReady() | Resolves with the state once ready. | | Get(path?) | Gets a cloned value. | | GetSnapshot() | Gets a frozen snapshot. | | Observe(path) | Observes a path. | | ObserveSnapshot() | Observes snapshots. | | ObserveSelector(selector) | Observes computed values with distinct filtering. | | Destroy() | Destroys the local state object. |