Skip to content
devsess

Using without Effect

You want the session scaffolding — sessions, sticky ports, managed subprocesses — but you don't write Effect, and you're not about to learn it to run a dev script. devsess/async is the same machinery as Getting Started, behind plain async/await instead of Effect.gen.

It still needs effect and @effect/platform-node sitting in node_modulesdevsess/async is built on the same Effect core underneath, it only keeps that out of your own code. Install is otherwise identical to the core install.

The same script, rewritten

With Effect:

// scripts/dev.ts
import { join } from 'node:path'
import { defineDevCli } from 'devsess'
import { Effect } from 'effect'
 
const main = defineDevCli({
	name: 'web',
	dir: join(import.meta.dirname, '..'),
	run: (ctx) =>
		Effect.gen(function* () {
			const session = yield* ctx.session
			yield* Effect.logInfo(`[dev] session: ${session.name}`)
 
			const port = yield* ctx.getStickyPort()
 
			yield* ctx.runManagedSubprocess('bunx', ['vite'], {
				env: { PORT: String(port) },
			})
		}),
})
 
main(process.argv)

With devsess/async:

// scripts/dev.ts
import { join } from 'node:path'
import { defineDevCli } from 'devsess/async'
 
const main = defineDevCli({
	name: 'web',
	dir: join(import.meta.dirname, '..'),
	run: async (ctx) => {
		const session = await ctx.session()
		console.log(`[dev] session: ${session.name}`)
 
		const port = await ctx.getStickyPort()
 
		await ctx.runManagedSubprocess('bunx', ['vite'], {
			env: { PORT: String(port) },
		})
	},
})
 
main(process.argv)

No effect import, no generator function — otherwise the same script.

Differences that actually bite

  • ctx.session is a function, not a value. The Effect API hands you an Effect you resolve with yield* ctx.session. devsess/async hands you a function: await ctx.session(). Call it as many times as you want — it's still resolved once per run internally, so repeat calls are free, not repeat work.
  • session.path(rel) is synchronous. No await, no yield* — it returns a string directly: const dir = session.path('cache'). It still doesn't create the directory, same as the Effect version.
  • createDevSessions(rootDir) replaces DevSessions and makeDevSessionsLayer. No service tag, no Layer — for anything you do outside defineDevCli. Its rootDir is the sessions directory itself, not your project root: pass <project>/.data/sessions, or you'll get session directories somewhere defineDevCli never looks.
  • cli and Schema are both re-exported — but only from devsess/async. import { cli, Schema } from 'devsess/async' covers flags (cli.Flag...) and SessionState schemas (Schema.Struct(...)) without ever importing effect. The core devsess package re-exports cli too, but not Schema — Effect users are already importing from effect for everything else, so it doesn't bother.

A script or a test reaches for createDevSessions directly:

// scripts/seed.ts
import { join } from 'node:path'
import { createDevSessions } from 'devsess/async'
 
const sessions = createDevSessions(
	join(import.meta.dirname, '..', '.data/sessions'),
)
const session = await sessions.getLatestOrCreate()

What you give up

devsess/pglite doesn't work with devsess/async — not "no wrapper exists yet," but structurally incompatible.

If your dev script needs a database per session, that piece has to be written against the Effect API. You can still write the rest of the script with devsess/async and drop into Effect.gen/Effect.runPromise for that one piece — but at that point you're back to an effect import, at least in that one file.

Everything else carries over exactly: dev sessions (including SessionState), sticky ports, managed subprocess lifetime, and running signals. Same underlying code, wrapped in Promises instead of Effects.