API reference@evolu/commonResource › SharedResource

Defined in: packages/common/src/Resource.ts:197

Shared Resource.

Lazily creates the underlying resource on the first acquire call, shares it across callers via Leases, and disposes it when the last lease is released.

Example

import {
  all,
  assertEqual,
  assertFalse,
  createRun,
  createSharedResource,
  ok,
  type Task,
} from "@evolu/common";

interface Connection extends Disposable {
  readonly send: (message: string) => void;
}

let createdCount = 0;
const createConnection: Task<Connection> = () => {
  createdCount += 1;
  return ok({
    send: (_message) => {},
    [Symbol.dispose]: () => {},
  });
};

await using run = createRun();
await using sharedConnection = await run.ok(
  createSharedResource(createConnection, { idleDisposeAfter: "5s" }),
);

// Creating the owner is lazy; no connection is open yet.
assertEqual(createdCount, 0);

// `use` owns and releases a lease around each operation.
const send = (message: string): Task<void> =>
  sharedConnection.use((connection) => () => {
    connection.send(message);
    return ok();
  });

{
  // An explicit lease can span several operations. Concurrent `use` calls
  // share the connection kept alive by this lease.
  using batchLease = await run.ok(sharedConnection.acquire);
  batchLease.resource.send("first");
  batchLease.resource.send("second");
  await run.ok(all([send("hello"), send("world")], { concurrency: 2 }));
}

// Reacquiring during the idle delay reuses the same generation.
using reusedLease = await run.ok(sharedConnection.acquire);
assertFalse(reusedLease.created);
assertEqual(createdCount, 1);

FAQ

Why release a lease with using?

using guarantees a Lease is released on every exit path: normal completion, a thrown error, and abort — abort surfaces as an exception in Task code, so stack unwinding runs disposers. Binding a lease to an ordinary const instead is a deliberate ownership transfer; the new owner must guarantee release.

What happens when a lease leaks?

Nothing in JavaScript enforces using or release; a lease that is never released compiles silently (see MDN resource management pitfalls; a lint rule may eventually close this gap: https://github.com/typescript-eslint/typescript-eslint/issues/8255). Encoding resource lifetime in the type system would not be bulletproof either: types can force a lifetime to exist, but a lifetime scoped too widely leaks just as silently. Evolu therefore stays JS-native — using in the language — and bounds the damage structurally. A leaked lease keeps the resource alive but never past its owner: disposing a SharedResource drains all outstanding leases and disposes the resource. A leaked SharedResource is bounded by its Run, whose disposal also drains its leases and disposes its current resource. Leaks are also observable — snapshot exposes a lease count that never returns to zero — and detected: in development builds, a lease that is garbage-collected without release logs a warning with its acquire stack via the LeakDetector dependency.

Extends

Methods

[asyncDispose]()

asyncDispose: PromiseLike<void>;

Defined in: node_modules/@typescript/old/lib/lib.esnext.disposable.d.ts:38

Inherited from

AsyncDisposable.[asyncDispose]

Properties

acquire

readonly acquire: Task<Lease<T>>;

Defined in: packages/common/src/Resource.ts:220

Acquires a Lease on the shared resource.

The first call lazily creates the resource. Later calls reuse the same resource until the last lease is released.

Once started, acquire runs to completion even when the caller aborts its Fiber, and the returned lease still counts as held. If resource creation or disposal is in progress, acquire waits for it to complete. Always await the result and release the lease; a caller that stops waiting (for example with the daemon helper) abandons a lease that is never released, retaining the resource until the owning SharedResource is disposed.

Owner disposal is different from caller abort: if disposal starts before a lease is transferred, acquire aborts with runDisposedAbortReason from the SharedResource's internal Run. A resource returned by create after shutdown starts remains owned by this SharedResource and is disposed without a lease escaping. Transfer occurs when the internal acquisition completes, before the caller necessarily resumes. If owner disposal starts in that gap, the caller can receive a lease already drained by disposal. Its release returns false, and its resource must not be used.


acquireCurrent

readonly acquireCurrent: Task<
  | Lease<T>
| undefined>;

Defined in: packages/common/src/Resource.ts:230

Acquires a Lease on the current resource without creating one.

Waits for preceding creation or disposal to finish. Returns undefined if no current resource remains. Owner disposal while waiting aborts the acquisition before a lease is transferred. As with acquire, owner disposal after transfer can drain the lease before the caller resumes.


snapshot

readonly snapshot: () => SharedResourceSnapshot;

Defined in: packages/common/src/Resource.ts:250

Returns the current shared-resource state for monitoring/debugging.

use

readonly use: <R, E, D>(callback: (resource: DistributiveOmit<T>, created: boolean) => Task<R, E, D>) => Task<R, E, D>;

Defined in: packages/common/src/Resource.ts:242

Acquires a Lease, runs a Task with the shared resource, and releases the lease after the Task settles.

Creates the resource when absent. The callback receives whether this use created the resource generation. While the owner remains running, the lease prevents ordinary idle disposal. Disposing this SharedResource is forceful: it drains the lease and may dispose the resource before or while the callback Task runs.