Skip to content

Latest commit

 

History

History
69 lines (49 loc) · 3.95 KB

File metadata and controls

69 lines (49 loc) · 3.95 KB

opencode database guide

Database

  • Schema: Drizzle schema lives in src/**/*.sql.ts.
  • Naming: tables and columns use snake*case; join columns are <entity>_id; indexes are <table>*<column>\_idx.
  • Migrations: generated by Drizzle Kit using drizzle.config.ts (schema: ./src/**/*.sql.ts, output: ./migration).
  • Command: bun run db generate --name <slug>.
  • Output: creates migration/<timestamp>_<slug>/migration.sql and snapshot.json.
  • Tests: migration tests should read the per-folder layout (no _journal.json).

opencode Effect rules

Use these rules when writing or migrating Effect code.

See specs/effect-migration.md for the compact pattern reference and examples.

Core

  • Use Effect.gen(function* () { ... }) for composition.
  • Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers.
  • Effect.fn / Effect.fnUntraced accept pipeable operators as extra arguments, so avoid unnecessary outer .pipe() wrappers.
  • Use Effect.callback for callback-based APIs.
  • Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date.

Schemas and errors

  • Use Schema.Class for multi-field data.
  • Use branded schemas (Schema.brand) for single-value types.
  • Use Schema.TaggedErrorClass for typed errors.
  • Use Schema.Defect instead of unknown for defect-like causes.
  • In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches.

Runtime vs InstanceState

  • Use makeRuntime (from src/effect/run-service.ts) for all services. It returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers.
  • Use InstanceState (from src/effect/instance-state.ts) for per-directory or per-project state that needs per-instance cleanup. It uses ScopedCache keyed by directory — each open project gets its own state, automatically cleaned up on disposal.
  • If two open directories should not share one copy of the service, it needs InstanceState.
  • Do the work directly in the InstanceState.make closure — ScopedCache handles run-once semantics. Don't add fibers, ensure() callbacks, or started flags on top.
  • Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.).
  • Use Effect.forkScoped inside the closure for background stream consumers — the fiber is interrupted when the instance is disposed.

Preferred Effect services

  • In effectified services, prefer yielding existing Effect services over dropping down to ad hoc platform APIs.
  • Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O.
  • Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers.
  • Prefer HttpClient.HttpClient instead of raw fetch.
  • Prefer Path.Path, Config, Clock, and DateTime when those concerns are already inside Effect code.
  • For background loops or scheduled tasks, use Effect.repeat or Effect.schedule with Effect.forkScoped in the layer definition.

Effect.cached for deduplication

Use Effect.cached when multiple concurrent callers should share a single in-flight computation rather than storing Fiber | undefined or Promise | undefined manually. See specs/effect-migration.md for the full pattern.

Instance.bind — ALS for native callbacks

Instance.bind(fn) captures the current Instance AsyncLocalStorage context and restores it synchronously when called.

Use it for native addon callbacks (@parcel/watcher, node-pty, native fs.watch, etc.) that need to call Bus.publish or anything that reads Instance.directory.

You do not need it for setTimeout, Promise.then, EventEmitter.on, or Effect fibers.

const cb = Instance.bind((err, evts) => {
  Bus.publish(MyEvent, { ... })
})
nativeAddon.subscribe(dir, cb)