Skip to content

Releases: evoluhq/evolu

@evolu/web@3.1.2

Choose a tag to compare

@github-actions github-actions released this 06 Sep 23:38

Patch Changes

  • 9fc736f: Updated internal Evolu peer dependency requirements

    Aligned internal peer dependency minimums with the current workspace releases. Upgrade the Evolu packages together when updating an adapter or framework integration.

  • Updated dependencies [532feaa]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [3d84543]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [76554bd]

  • Updated dependencies [918d77b]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [91ff875]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [140c4cf]

  • Updated dependencies [3d84543]

  • Updated dependencies [3d84543]

  • Updated dependencies [f3b5829]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [9ee6f15]

    • @evolu/common@8.10.0

@evolu/vue@2.0.3

Choose a tag to compare

@github-actions github-actions released this 06 Sep 23:38

Patch Changes

  • 9fc736f: Updated internal Evolu peer dependency requirements

    Aligned internal peer dependency minimums with the current workspace releases. Upgrade the Evolu packages together when updating an adapter or framework integration.

  • Updated dependencies [532feaa]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [3d84543]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [76554bd]

  • Updated dependencies [918d77b]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [91ff875]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [140c4cf]

  • Updated dependencies [3d84543]

  • Updated dependencies [3d84543]

  • Updated dependencies [f3b5829]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [9ee6f15]

    • @evolu/common@8.10.0

@evolu/vitest@2.0.2

Choose a tag to compare

Patch Changes

  • 9fc736f: Updated internal Evolu peer dependency requirements

    Aligned internal peer dependency minimums with the current workspace releases. Upgrade the Evolu packages together when updating an adapter or framework integration.

  • Updated dependencies [532feaa]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [3d84543]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [76554bd]

  • Updated dependencies [918d77b]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [91ff875]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [140c4cf]

  • Updated dependencies [3d84543]

  • Updated dependencies [3d84543]

  • Updated dependencies [f3b5829]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [9ee6f15]

    • @evolu/common@8.10.0

@evolu/svelte@3.0.3

Choose a tag to compare

Patch Changes

  • 9fc736f: Updated internal Evolu peer dependency requirements

    Aligned internal peer dependency minimums with the current workspace releases. Upgrade the Evolu packages together when updating an adapter or framework integration.

  • Updated dependencies [532feaa]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [3d84543]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [76554bd]

  • Updated dependencies [918d77b]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [9fc736f]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [91ff875]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [140c4cf]

  • Updated dependencies [3d84543]

  • Updated dependencies [3d84543]

  • Updated dependencies [f3b5829]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [9ee6f15]

    • @evolu/common@8.10.0
    • @evolu/web@3.1.2

@evolu/relay@4.0.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 23:43

Major Changes

  • 6269b91: Published the configurable relay package

    The relay was a private workspace app that only shipped as a Docker image. It
    is now published to npm with the evolu-relay executable, so it can run with
    npx @evolu/relay. The evoluhq/relay Docker image installs the published
    package and is built as part of every release that publishes a new relay
    version.

    The relay reads its port from PORT and its per-owner storage quota
    from EVOLU_RELAY_MAX_OWNER_BYTES, as a size literal such as 10MiB.
    Bare byte counts such as 1048576 are not accepted; use 1MiB instead.
    PORT supports hosting platforms that supply the listening port. There is no
    EVOLU_RELAY_PORT override. The port still defaults to 4000. Leaving the quota
    unset imposes no per-owner storage limit; 0B permits no stored bytes.
    Invalid supplied values never fall back to defaults or disable the quota.

    Ports must be integers from 0 through 65535; zero requests an automatically
    assigned listening port. An empty, invalid, or unknown EVOLU_RELAY_*
    variable stops the relay with a formatted validation error, before creating
    the data directory or initializing its database. Startup reports the first
    validation message and preserves all collected errors in the Error's cause.
    The Docker health check uses the configured PORT; use a fixed, nonzero port
    when running the image.

    Environment names must exactly match the declared CONSTANT_CASE names. Incorrect
    casing within the EVOLU_RELAY_ namespace is reported as an unknown variable.
    Casing is not repaired, and double underscores do not introduce nested settings.

Patch Changes

@evolu/react@11.0.3

Choose a tag to compare

Patch Changes

  • 9fc736f: Updated internal Evolu peer dependency requirements

    Aligned internal peer dependency minimums with the current workspace releases. Upgrade the Evolu packages together when updating an adapter or framework integration.

  • Updated dependencies [532feaa]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [3d84543]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [76554bd]

  • Updated dependencies [918d77b]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [91ff875]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [140c4cf]

  • Updated dependencies [3d84543]

  • Updated dependencies [3d84543]

  • Updated dependencies [f3b5829]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [9ee6f15]

    • @evolu/common@8.10.0

@evolu/react-web@3.0.3

Choose a tag to compare

Patch Changes

  • 9fc736f: Updated internal Evolu peer dependency requirements

    Aligned internal peer dependency minimums with the current workspace releases. Upgrade the Evolu packages together when updating an adapter or framework integration.

  • Updated dependencies [532feaa]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [3d84543]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [76554bd]

  • Updated dependencies [918d77b]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [9fc736f]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [91ff875]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [140c4cf]

  • Updated dependencies [3d84543]

  • Updated dependencies [3d84543]

  • Updated dependencies [f3b5829]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [9ee6f15]

    • @evolu/common@8.10.0
    • @evolu/web@3.1.2

@evolu/react-native@16.0.2

Choose a tag to compare

Patch Changes

  • 9fc736f: Updated internal Evolu peer dependency requirements

    Aligned internal peer dependency minimums with the current workspace releases. Upgrade the Evolu packages together when updating an adapter or framework integration.

  • Updated dependencies [532feaa]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [3d84543]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [76554bd]

  • Updated dependencies [918d77b]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [9fc736f]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [91ff875]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [140c4cf]

  • Updated dependencies [3d84543]

  • Updated dependencies [3d84543]

  • Updated dependencies [f3b5829]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [9ee6f15]

    • @evolu/common@8.10.0
    • @evolu/react@11.0.3

@evolu/nodejs@4.0.0

Choose a tag to compare

Major Changes

  • 2183ca3: Required validated Port values in relay configuration

    NodeJsRelayConfig.port remains optional, but a supplied value must now be a
    validated Port instead of a plain number. This ensures ports are integers
    from 0 through 65535 before starting the server. Zero requests an automatically
    assigned listening port.

    Use Port.orThrow for startup constants or handle the Result from
    Port.fromUnknown or PortFromString.fromUnknown for external input.

    import { assertType, Port, type Task } from "@evolu/common";
    import type { Relay } from "@evolu/common/local-first";
    import { createRelay, type RelayDeps } from "@evolu/nodejs";
    
    // @ts-expect-error Relay configuration requires a validated Port, not a number.
    const _old = createRelay({ port: 4000, isOwnerWithinQuota: () => true });
    
    const main = createRelay({
      port: Port.orThrow(4000),
      isOwnerWithinQuota: () => true,
    });
    assertType<typeof main, Task<Relay, never, RelayDeps>>();
    
    const defaultPort = createRelay({ isOwnerWithinQuota: () => true });
    assertType<typeof defaultPort, Task<Relay, never, RelayDeps>>();

Minor Changes

  • 918d77b: Added file system operations for Tasks

    Fs provides readFile, writeFile, readDirectory, createDirectory, copy,
    copyFile, rename, remove, getMetadata, exists, and createTempDirectory.
    Each operation returns a Task.

    Tasks can sequence file operations without synchronous I/O. Node.js's synchronous
    methods are intentionally omitted to avoid accidentally blocking the event loop.

    Inject createNodeFs() through runMain or createRun. Tasks declare FsDep
    and access the file system through run.deps.fs.

    On Node.js, readFile and writeFile pass the Run's abort signal to the native
    operation. If it rejects after cancellation, the Task propagates the Run's abort
    reason. Cancellation can leave a write partially completed. Successful operations
    return their values even if an abort was requested. Other operations run to
    completion and return their results once started.

    File system errors include a reason such as NotFound or IsDirectory, a
    diagnostic message, and the operation's path. URL paths are represented by href.
    Copy and rename errors also include the destination. exists returns false
    for NotFound and preserves other errors, including permission failures.

    getMetadata returns data with a type field such as "File" or "Directory".
    readDirectory lists relative entry names and supports recursive listing.
    On Node.js, copy delegates to recursive node:fs/promises.cp. By default it
    merges directories and replaces files. Its options are Node's force,
    errorOnExist, and preserveTimestamps: force: false skips existing files,
    and adding errorOnExist: true rejects existing files and directories.
    Symbolic links retain Node's behavior and may still be replaced with both of
    those options set. Tree copying provides no exclusive-creation or atomicity
    guarantee, and a failure can leave a partial copy.

    copyFile copies a single file and fails with AlreadyExists if the destination
    exists, unless overwrite: true is set. Its default exclusive creation also
    protects against competing copies. File contents are not published atomically.
    rename uses the platform's rename semantics and can replace an existing file.

    createTempDirectory() uses the system temporary directory by default. Its
    directory option selects an existing parent directory, and prefix sets a
    prefix for the generated directory name. The returned directory supports cleanup
    with await using. Once creation starts, it returns its result even if the Run
    aborts, so the caller can dispose the directory.
    On Node.js, the parent is resolved through the file system before creation, so
    symbolic links followed by .. retain their file system meaning. The returned
    path is absolute, so cleanup still removes the created directory if the process
    changes its working directory before disposal. Resolution errors identify the
    supplied parent; creation errors identify the resolved parent and name prefix.

    import {
      assertEqual,
      assertErr,
      ok,
      type FsDep,
      type FsError,
      type Task,
    } from "@evolu/common";
    import { createNodeFs, runMain } from "@evolu/nodejs";
    import { join } from "node:path";
    
    const main: Task<void, FsError, FsDep> = async (run) => {
      const { fs } = run.deps;
      const temp = await run(fs.createTempDirectory({ prefix: "evolu-fs-" }));
      if (!temp.ok) return temp;
    
      await using directory = temp.value;
      const path = join(directory.path, "message.txt");
    
      const result = await run(fs.writeFile(path, "hello"));
      if (!result.ok) return result;
    
      const text = await run(fs.readFile(path, "utf8"));
      if (!text.ok) return text;
      assertEqual(text.value, "hello");
    
      const metadata = await run(fs.getMetadata(path));
      if (!metadata.ok) return metadata;
      assertEqual(metadata.value.type, "File");
    
      const copyPath = join(directory.path, "copy.txt");
      const copied = await run(fs.copy(path, copyPath));
      if (!copied.ok) return copied;
      const copyConflict = await run(
        fs.copy(path, copyPath, { force: false, errorOnExist: true }),
      );
      assertErr(copyConflict);
      assertEqual(copyConflict.error.reason, "AlreadyExists");
    
      const conflict = await run(fs.copyFile(path, copyPath));
      assertErr(conflict);
      assertEqual(conflict.error.reason, "AlreadyExists");
    
      return ok();
    };
    
    await runMain({ fs: createNodeFs() }, { mode: "command" })(main);

    Use testCreateFs(overrides) to supply file system behavior in application tests.
    Unconfigured operations throw a defect naming the method when their Task runs.

    import {
      assertErr,
      testCreateFs,
      testCreateRun,
      err,
      type FsError,
    } from "@evolu/common";
    
    const error: FsError = {
      type: "FsError",
      reason: "PermissionDenied",
      path: "protected.txt",
      syscall: "open",
      message: "Permission denied",
    };
    
    await using run = testCreateRun({
      fs: testCreateFs({ readFile: () => () => err(error) }),
    });
    
    assertErr(await run(run.deps.fs.readFile("protected.txt", "utf8")), error);
  • 8ac49c8: Allowed main Tasks to return errors

    runMain accepts fallible Tasks. A returned error is fatal: it is preserved in
    Error.cause, reported through reportDefect, and sets process.exitCode to 1.
    Cleanup finishes before runMain resolves.

    import {
      assertEqual,
      IntFromString,
      ok,
      type Task,
      type TypeError,
    } from "@evolu/common";
    import { runMain } from "@evolu/nodejs";
    
    const main: Task<void, TypeError> = () => {
      const port = IntFromString.fromUnknown("4000");
      if (!port.ok) return port;
    
      assertEqual(port.value, 4000);
      return ok();
    };
    
    await runMain(main, { mode: "command" });

Patch Changes

  • 9fc736f: Updated internal Evolu peer dependency requirements

    Aligned internal peer dependency minimums with the current workspace releases. Upgrade the Evolu packages together when updating an adapter or framework integration.

  • 918d77b: Fixed type resolution in documentation examples

    testJSDocExamples now applies package aliases when type-checking imports
    within aliased modules. Examples can use branded types across common and
    platform packages without mixing source and built declarations.

  • Updated dependencies [532feaa]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [3d84543]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [76554bd]

  • Updated dependencies [918d77b]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [91ff875]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [6bd0a36]

  • Updated dependencies [140c4cf]

  • Updated dependencies [3d84543]

  • Updated dependencies [3d84543]

  • Updated dependencies [f3b5829]

  • Updated dependencies [ad85bdb]

  • Updated dependencies [f4d9ad7]

  • Updated dependencies [9ee6f15]

    • @evolu/common@8.10.0

@evolu/common@8.10.0

Choose a tag to compare

Minor Changes

  • 532feaa: Added ByteLength with human-readable literals

    ByteLength is the canonical non-negative safe integer number of bytes, as
    Millis is for time. It rejects JavaScript's negative zero so zero has one
    canonical representation. ByteSizeLiteral validates sizes such as
    "1023MiB" or "1.5GiB" at compile time and runtime using the binary units
    B, KiB, MiB, GiB, and TiB. Each unit stays below 1024, so
    1024KiB is written as "1MiB", and a half is the only decimal because it
    is the only one that is exact in every binary unit. APIs can accept ByteSize and normalize it with
    byteSizeToByteLength, and ByteLengthFromString parses either a number of
    bytes or a literal from text such as an environment variable.

    import {
      assertEqual,
      assertErr,
      assertOk,
      ByteLength,
      ByteLengthFromString,
      byteSizeToByteLength,
      type ByteSize,
    } from "@evolu/common";
    
    const quota: ByteSize = "1MiB";
    assertEqual(byteSizeToByteLength(quota), 1048576);
    assertEqual(byteSizeToByteLength(ByteLength.orThrow(1000)), 1000);
    assertErr(ByteLength.fromUnknown(-0));
    assertOk(ByteLengthFromString.fromUnknown("10MiB"), 10485760);
  • 6bd0a36: Added strict environment configuration codecs

    Use env({ ... }) to decode CONSTANT_CASE environment variables into typed
    camelCase settings. Declare fields directly for unprefixed variables such as
    PORT, and put related fields in one-level CONSTANT_CASE namespace groups
    such as APP. The decoded output stays flat. Duplicate output fields and
    external names fail during construction.

    TypeScript rejects fields declared as namespace groups and groups declared as
    fields. Full identifier spelling and name lengths are checked during construction.

    Field codecs validate values and encode them back to canonical strings. Field
    Types can use withDefault for explicit defaults. Resolve source precedence
    before replacing absence, or use the preserve strategy when later composition
    needs supplied-input evidence. Environment access and source merging remain
    application code.

    Unprefixed fields select exact names. Namespace groups select all names with
    their prefix and an underscore separator, ignoring casing during selection.
    Incorrectly cased or unknown selected names fail validation. Unrelated variables
    are ignored; misspelled unprefixed names or namespace prefixes may therefore
    still look absent. Namespaces must omit the trailing underscore.

    Optional fields may be absent; explicit undefined values are rejected.
    Empty strings remain present and must satisfy the field Type.
    Malformed and unknown selected names fail instead of silently using defaults.
    Errors retain the original environment names. Non-object inputs fail with
    standard object errors. Names follow EnvName: a
    CONSTANT_CASE identifier of at most 255 characters including the prefix.

    Matching own string properties are read from any non-null object, including
    arrays and process.env. Prototypes and internal contents, such as Map
    entries, are ignored. Missing required settings still fail validation.

    import {
      assertEqual,
      assertErr,
      assertOk,
      ByteLengthFromString,
      env,
      optional,
      PortFromString,
      typeErrorToIssues,
    } from "@evolu/common";
    
    const AppEnv = env({
      port: optional(PortFromString),
      APP: {
        maxOwnerBytes: optional(ByteLengthFromString),
      },
    });
    const config = AppEnv.fromUnknown({
      PORT: "04000",
      APP_MAX_OWNER_BYTES: "1MiB",
      HOME: "/home/evolu",
    });
    assertOk(config, { port: 4000, maxOwnerBytes: 1048576 });
    assertEqual(AppEnv.to(config.value), {
      PORT: "4000",
      APP_MAX_OWNER_BYTES: "1048576",
    });
    
    const invalid = AppEnv.fromUnknown({ APP_POTR: "4000" });
    assertErr(invalid);
    assertEqual(typeErrorToIssues(AppEnv, invalid.error)[0]?.path, ["APP_POTR"]);
    assertErr(AppEnv.fromUnknown({ PORT: "" }));
    assertErr(AppEnv.fromUnknown({ PORT: undefined }));
    assertOk(AppEnv.fromUnknown([]), {});
  • f4d9ad7: Added custom error wrappers for validation Types

    createTypeWithError creates a root Type from an existing validator, an error
    mapper, and a formatter. It forwards error-collection options automatically.
    The source must use identity encoding; the wrapper preserves its valid values
    and exposes its Output as Input.

    import {
      assertEqual,
      assertErr,
      createTypeWithError,
      Number,
      String,
      union,
      type TypeError,
      type UnionError,
    } from "@evolu/common";
    
    interface ValueError extends TypeError<"Value"> {
      readonly cause: UnionError;
    }
    
    const Value = createTypeWithError(
      "Value",
      union(String, Number),
      (cause): ValueError => ({ type: "Value", cause }),
      () => "Enter text or a number.",
    );
    
    const result = Value.fromUnknown(false, { errors: "all" });
    assertErr(result);
    assertEqual(result.error.cause.errors.length, 2);
    assertEqual(Value.formatError(result.error), "Enter text or a number.");
  • 3d84543: Added a prefixed string codec

    Use prefixed(prefix)(Type) to remove an exact, case-sensitive prefix when
    decoding and restore it when encoding. The wrapped Type validates the suffix
    and preserves its decoded output, including brands. Encoding uses its
    canonical string representation.

    import {
      assertEqual,
      assertErr,
      assertOk,
      assertType,
      ConstantCaseIdentifier,
      prefixed,
      PortFromString,
      type Port,
    } from "@evolu/common";
    
    const EnvName = prefixed("APP_")(ConstantCaseIdentifier);
    const name = EnvName.fromUnknown("APP_PORT");
    assertOk(name, "PORT");
    assertType<typeof name.value, ConstantCaseIdentifier>();
    assertEqual(EnvName.to(name.value), "APP_PORT");
    assertErr(EnvName.fromUnknown("OTHER_PORT"));
    assertErr(EnvName.fromUnknown("APP_port"));
    
    const PortSetting = prefixed("port:")(PortFromString);
    const port = PortSetting.fromUnknown("port:04000");
    assertOk(port, 4000);
    assertType<typeof port.value, Port>();
    assertEqual(PortSetting.to(port.value), "port:4000");

    The prefix must be a concrete string literal. The wrapped Type must accept a
    string Input and encode to strings. An empty prefix leaves its representation
    unchanged; an empty suffix is validated by the wrapped Type.

  • 6bd0a36: Added reversible object key codecs

    objectKeys(keyType)(objectType) gives a strict object's fields external
    names using the key Type's canonical encoding. Field Types, optionality, and
    semantic Output are preserved. Decoding accepts exact canonical names and
    reports errors at those names. Invalid schema keys and conflicting encodings
    fail during construction.

    Typed property errors include missing required properties and unexpected input
    keys.

    CamelCaseIdentifierFromConstantCaseIdentifier converts between the two
    identifier conventions without losing word boundaries. Compose it with
    prefixed to adapt namespaced keys.

    import {
      assertEqual,
      assertErr,
      assertOk,
      CamelCaseIdentifierFromConstantCaseIdentifier,
      object,
      objectKeys,
      PortFromString,
      prefixed,
    } from "@evolu/common";
    
    const Key = prefixed("APP_")(CamelCaseIdentifierFromConstantCaseIdentifier);
    const Settings = objectKeys(Key)(object({ http2Port: PortFromString }));
    const result = Settings.fromUnknown({ APP_HTTP2_PORT: "04000" });
    assertOk(result, { http2Port: 4000 });
    assertEqual(Settings.to(result.value), { APP_HTTP2_PORT: "4000" });
    assertErr(Settings.fromUnknown({ APP_HTTP_2_PORT: "4000" }));
  • 76554bd: Added explicit defaults

    Use withDefault(type, value) to fill missing optional properties or accepted
    null and undefined values. Invalid supplied values still fail validation.

    Pass { strategy: "preserve" } as the third argument to track whether the
    default was used and restore the original absence when encoding. An explicitly
    supplied value remains distinguishable even when it equals the default.

    Configured defaults are reused by reference, including when Types are localized
    with localizeTypes. partial retains localized field errors while disabling
    defaults for missing properties.

    Prefer defaults in the view over defaults in your database schema. Use
    nullable columns and apply ?? when reading or displaying values unless the
    default needs to be stored. Replacing null can enlarge rows unnecessarily and
    erase the distinction between "not specified" and an explicit user decision.

    import {
      assertEqual,
      assertOk,
      Boolean,
      nullOr,
      object,
      optional,
      withDefault,
    } from "@evolu/common";
    
    const Enabled = withDefault(nullOr(Boolean), true);
    
    assertOk(Enabled.fromUnknown(null), true);
    assertEqual(Enabled.to(true), true);
    
    const Settings = object({
      enabled: withDefault(optional(Boolean), true, { strategy: "preserve" }),
    });
    
    const missing = Settings.fromUnknown({});
    assertOk(missing, {
      enabled: { value: true, defaultUsed: true, original: "missing" },
    });
    assertEqual(Settings.to(missing.value), {});
    
    assertOk(Settings.fromUnknown({ enabled: true }), {
      enabled: { value: true, defaultUsed: false },
    });
  • 918d77b: Added file system operations for Tasks

    Fs provides readFile, writeFile, readDirectory, createDirectory, copy,
    copyFile, rename, remove, getMetadata, exists, and createTempDirectory.
    Each operation returns a Task.

    Tasks can sequence file operations without synchronous I/O. Node.js's synchronous
    methods are intentionally omitted to avoid accidentally blocking the event loop.

    Inject createNodeFs() through runMain or createRun. Tasks declare FsDep
    and access the file system through run.deps.fs.

    On Node.js, readFile and writeFile pass the Run's abort signal to the native
    operation. If it rejects after cancellation,...

Read more