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

@sorrell/app-settings

v3.0.0

Published

Schema-validated, file-backed application settings for effect applications.

Readme

@sorrell/app-settings

Schema-validated, JSON-file-backed application settings for effect.

The package constructs a typed service from an Effect schema. By default, it selects the native per-user configuration directory for process.platform, and creates the directory tree if needed. Successful changes are written atomically before the in-memory value is committed, and a scoped file watcher publishes valid external changes to the application.

Usage

import { NodeFileSystem, NodePath } from "@effect/platform-node";
import { Effect, Layer, Schema, Stream } from "effect";
import { AppSettings } from "@sorrell/app-settings";

const SettingsSchema = Schema.Struct({
    launchAtStartup: Schema.Boolean,
    theme: Schema.String
});

const Settings = AppSettings.Make(
    SettingsSchema,
    {
        ApplicationName: "SorrellWm",
        Initial:
        {
            launchAtStartup: false,
            theme: "system"
        }
    }
);

const PlatformLive = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer);
const Live = Settings.Layer.pipe(Layer.provide(PlatformLive));

const Program = Effect.gen(function*()
{
    const Service = yield* Settings;

    const Theme = yield* Service.GetSetting("theme");
    yield* Effect.log(`Current theme: ${ Theme }`);

    yield* Service.SetSetting("theme", "dark");

    const LogSettingsChanged = (Value) => Effect.log("Settings changed", Value);

    /* `Changes` immediately emits the current value, then every update made *
     * through the service or loaded from an external file edit.            */
    yield* pipe(
        Service.Changes,
        Stream.runForEach(LogSettingsChanged),
        Effect.forkScoped
    );
});

Effect.runPromise(pipe(
    Effect.scoped(Program),
    Effect.provide(Live)
));

The layer requires Effect's FileSystem and Path services. This package was piloted and tested with @effect/platform-node in SorrellWm, but it is expected that other platforms may be used without issue.

Default path

With the default application name SorrellWm and filename settings.json, the path is:

| Platform | Path | | -------- | --------------------------------------------------------------------------------------- | | Windows | %APPDATA%\SorrellWm\settings.json | | macOS | ~/Library/Application Support/SorrellWm/settings.json | | Linux | $XDG_CONFIG_HOME/SorrellWm/settings.json or ~/.config/SorrellWm/settings.json |

This table describes GetDefaultFilePath's Node-style desktop OS conventions only; it does not apply on React Native/Expo (see below).

ApplicationName and FileName customize the final two components,

const Settings = AppSettings.Make(
    SettingsSchema,
    {
        ApplicationName: "MyApplication",
        FileName: "preferences.json"
    }
);

A completely custom path can be supplied with FilePath,

const Settings = AppSettings.Make(
    SettingsSchema,
    {
        ApplicationName: "MyApplication",
        FilePath: "./configuration/settings.json"
    }
);

Expo / React Native

The module has no top-level Node built-in imports (node:path, node:os, etc.), so it is safe to include in a Metro/Expo bundle. Two things are still the consuming app's own responsibility, since neither is shipped by this package:

  • Always pass Options.FilePath explicitly. GetDefaultFilePath only implements Node-style desktop OS conventions (%APPDATA%, ~/Library/Application Support, XDG) and has no principled way to derive an equivalent for React Native's Platform.OS ("ios"|"android"|"web"). A typical Expo app derives its path from expo-file-system instead:

    import * as FileSystem from "expo-file-system";
    import { AppSettings } from "@sorrell/app-settings";
    
    const Settings = AppSettings.Make(
        SettingsSchema,
        {
            ApplicationName: "MyExpoApp",
            FilePath: `${ FileSystem.documentDirectory }settings.json`,
            Initial: { /* ... */ }
        }
    );
  • Supply your own FileSystem/Path Effect layer. No @effect/platform-* package exists for React Native/Expo today (there's no separate @sorrell/app-settings-shipped Expo layer either -- this package stays a single, platform-agnostic module, the same way @sorrell/app-settings itself never depended on @effect/platform-node). An Expo consumer hand-rolls a layer against expo-file-system, providing exactly the members AppSettings actually calls:

    • FileSystem.FileSystem: exists, makeDirectory({ recursive }), readFileString, makeTempFile({ directory, prefix, suffix }), writeFileString, rename, remove({ force }), watch.
    • Path.Path: resolve, dirname, basename.

    expo-file-system has no directory-watch API equivalent to Node's fs.watch. When external edits to a sandboxed app's own settings file aren't a concern, a hand-rolled watch can reasonably return an empty/never-emitting stream -- reads on startup and writes via Set/SetSetting/Update still work fully either way.

External state synchronization

Settings are the durable description of desired state. Use a downstream synchronization layer when settings also control state outside the JSON file, such as registering the application to run when the user signs in:

const Settings = AppSettings.Make(
    SettingsSchema,
    {
        ApplicationName: "SorrellWm",
        Initial:
        {
            launchAtStartup: false,
            theme: "system"
        }
    }
);

declare const SynchronizeLaunchAtStartup: (Enabled: boolean) =>
    Effect.Effect<void, Error, StartupRegistration>;

const SynchronizationLive = AppSettings.SyncSetting(
    Settings,
    "launchAtStartup",
    SynchronizeLaunchAtStartup
);

const Live = SynchronizationLive.pipe(
    Layer.provideMerge(Settings.Layer),
    Layer.provide(PlatformLive),
    Layer.provide(StartupRegistrationLive)
);

SyncSetting immediately applies the current committed value, then applies later changes to that setting. Changes to unrelated settings are ignored using Object.is equality. The synchronizer's Effect requirements are retained by the returned layer, so it may depend on services that themselves use Settings without creating a construction cycle.

Sync(Settings, Reconcile) provides the same behavior for the complete settings value. Failures are logged, and do not terminate monitoring; the committed settings remain the desired state and later changes are still processed. A reconciler can apply its own retry policy with Effect.retry when external failures are transient.

Service API

| Member | Behavior | | --------------------- | ------------------------------------------------------------------------------- | | Get | Retrieves the complete current settings value. | | GetSetting(Key) | Retrieves one setting with its schema-derived type. | | Set(Value) | Validates, atomically persists, and replaces all settings. | | SetSetting(Key, Value) | Validates, atomically persists, and replaces one setting. | | Update(f) | Computes and persists a complete replacement value. | | Changes | Replays the current value and broadcasts subsequent local and external updates. |

File and failure behavior

  • Existing files are parsed as JSON and decoded with the supplied schema when the layer starts
  • If the file does not exist, Initial is persisted. When Initial is omitted, an empty object is decoded so defaults defined by the schema can provide the initial settings
  • Missing parent directories are created recursively before the file is loaded or written
  • Writes use a temporary file in the same directory followed by an atomic replacement. The service's in-memory value and Changes stream are updated only after that replacement succeeds
  • A failed write returns an AppSettings.FileError and leaves the last committed in-memory value unchanged
  • Invalid external JSON or schema-invalid external values are logged and ignored. The last valid value remains active, and monitoring continues
  • A failed watcher is logged and restarted after WatchRetryDelay (one second by default)

AppSettings.Make also accepts the following options,

  • ApplicationName
  • FileName
  • FilePath
  • JsonIndentNum
  • WatchDebounce
  • WatchRetryDelay

The returned tag retains the inferred settings type, its .Schema, its resolved .FilePath, and its scoped .Layer.