devsess 0.3.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +3 -3
- package/dist/async/index.d.ts +1 -1
- package/dist/async/index.d.ts.map +1 -1
- package/dist/async/index.js +9 -7
- package/dist/async/index.js.map +1 -1
- package/dist/async/platform.d.ts +2 -2
- package/dist/async/platform.d.ts.map +1 -1
- package/dist/async/run-dev-cli.d.ts +1 -1
- package/dist/async/run-dev-cli.d.ts.map +1 -1
- package/dist/chunk-KG24ZAKO.js +166 -0
- package/dist/chunk-KG24ZAKO.js.map +1 -0
- package/dist/{chunk-ULIVG5ID.js → chunk-MBPIOYW3.js} +166 -233
- package/dist/chunk-MBPIOYW3.js.map +1 -0
- package/dist/chunk-OODLZZCC.js +287 -0
- package/dist/chunk-OODLZZCC.js.map +1 -0
- package/dist/chunk-UOIMVHGX.js +29 -0
- package/dist/chunk-UOIMVHGX.js.map +1 -0
- package/dist/current-session.d.ts +1 -0
- package/dist/current-session.d.ts.map +1 -1
- package/dist/dev/daemon-client.d.ts +21 -0
- package/dist/dev/daemon-client.d.ts.map +1 -0
- package/dist/dev/daemon-readiness.d.ts.map +1 -1
- package/dist/dev/daemon-services.d.ts +3 -0
- package/dist/dev/daemon-services.d.ts.map +1 -0
- package/dist/dev/sticky-port.d.ts +4 -1
- package/dist/dev/sticky-port.d.ts.map +1 -1
- package/dist/dev/subprocess.d.ts +1 -1
- package/dist/dev/subprocess.d.ts.map +1 -1
- package/dist/dev-sessions.d.ts +2 -0
- package/dist/dev-sessions.d.ts.map +1 -1
- package/dist/index.js +10 -26
- package/dist/index.js.map +1 -1
- package/dist/lock.d.ts +3 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/pglite/index.d.ts +2 -2
- package/dist/pglite/index.d.ts.map +1 -1
- package/dist/pglite/index.js +20 -30
- package/dist/pglite/index.js.map +1 -1
- package/dist/services/container.d.ts +17 -0
- package/dist/services/container.d.ts.map +1 -0
- package/dist/services/core.d.ts +30 -0
- package/dist/services/core.d.ts.map +1 -0
- package/dist/services/docker-args.d.ts +31 -0
- package/dist/services/docker-args.d.ts.map +1 -0
- package/dist/services/index.d.ts +50 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +379 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/local.d.ts +11 -0
- package/dist/services/local.d.ts.map +1 -0
- package/dist/services/shared/callsite.d.ts +4 -0
- package/dist/services/shared/callsite.d.ts.map +1 -0
- package/dist/services/shared/entry.d.ts +2 -0
- package/dist/services/shared/entry.d.ts.map +1 -0
- package/dist/services/shared/entry.js +137 -0
- package/dist/services/shared/entry.js.map +1 -0
- package/dist/services/shared/host.d.ts +5 -0
- package/dist/services/shared/host.d.ts.map +1 -0
- package/dist/services/shared/index.d.ts +11 -0
- package/dist/services/shared/index.d.ts.map +1 -0
- package/dist/services/shared/protocol.d.ts +39 -0
- package/dist/services/shared/protocol.d.ts.map +1 -0
- package/dist/services/shared/root.d.ts +5 -0
- package/dist/services/shared/root.d.ts.map +1 -0
- package/dist/socket-line.d.ts +3 -0
- package/dist/socket-line.d.ts.map +1 -0
- package/package.json +94 -88
- package/dist/chunk-ULIVG5ID.js.map +0 -1
package/dist/pglite/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pglite/index.ts"],"sourcesContent":["import { PGlite } from '@electric-sql/pglite';\nimport { drizzle } from 'drizzle-orm/pglite';\nimport { migrate } from 'drizzle-orm/pglite/migrator';\nimport { Data, Effect } from 'effect';\nimport { FileSystem } from 'effect/FileSystem';\nimport { Path } from 'effect/Path';\nimport type { DevSession } from '../dev-sessions';\n\nexport class PgliteError extends Data.TaggedError('PgliteError')<{\n\tmessage: string;\n\tcause?: unknown;\n}> {}\n\nconst IN_MEMORY_DATA_DIR = 'memory://';\n\n/** Drizzle's own defaults, used whenever a caller doesn't override them. */\nconst DEFAULT_MIGRATIONS_TABLE = '__drizzle_migrations';\nconst DEFAULT_MIGRATIONS_SCHEMA = 'drizzle';\n\nexport type PgliteMigrations = {\n\tmigrationsFolder: string;\n\tmigrationsTable?: string;\n\tmigrationsSchema?: string;\n};\n\n/** Double-quotes an identifier part for safe interpolation into SQL. */\nconst quoteIdentifierPart = (part: string) => `\"${part.replaceAll('\"', '\"\"')}\"`;\n\nconst qualifiedMigrationsTable = (\n\tmigrations: Pick<PgliteMigrations, 'migrationsTable' | 'migrationsSchema'>,\n) =>\n\t`${quoteIdentifierPart(migrations.migrationsSchema ?? DEFAULT_MIGRATIONS_SCHEMA)}.${quoteIdentifierPart(migrations.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE)}`;\n\n/**\n * A persisted data dir that already exists holds a hydrated database, so it is\n * opened as-is rather than re-seeded from the dump. In-memory clients have\n * nothing to reuse.\n */\nconst shouldReuseExistingDatabase = (dataDir: string | undefined) =>\n\tEffect.gen(function* () {\n\t\tif (!dataDir || dataDir === IN_MEMORY_DATA_DIR) {\n\t\t\treturn false;\n\t\t}\n\t\tconst fs = yield* FileSystem;\n\t\treturn yield* fs.exists(dataDir);\n\t});\n\nexport const createPgliteFromDump = (opts: {\n\tdataDir?: string;\n\tdumpPath: string;\n}) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem;\n\n\t\tif (yield* shouldReuseExistingDatabase(opts.dataDir)) {\n\t\t\treturn new PGlite(opts.dataDir);\n\t\t}\n\n\t\tif (!(yield* fs.exists(opts.dumpPath))) {\n\t\t\treturn yield* new PgliteError({\n\t\t\t\tmessage: `Dump file not found at ${opts.dumpPath}`,\n\t\t\t});\n\t\t}\n\n\t\tconst buffer = yield* fs.readFile(opts.dumpPath);\n\t\tconst file = new Blob([new Uint8Array(buffer)]);\n\n\t\tconst client = new PGlite(opts.dataDir, {\n\t\t\tloadDataDir: file,\n\t\t});\n\n\t\treturn client;\n\t});\n\nconst toUint8Array = (input: File | Blob) =>\n\tEffect.gen(function* () {\n\t\tconst buf = yield* Effect.tryPromise({\n\t\t\ttry: () => input.arrayBuffer(),\n\t\t\tcatch: (error) =>\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Failed to convert dump to Uint8Array',\n\t\t\t\t\tcause: error,\n\t\t\t\t}),\n\t\t});\n\t\treturn new Uint8Array(buf);\n\t});\n\nexport const dumpPgliteToFile = (client: PGlite, dest: string) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem;\n\t\tconst path = yield* Path;\n\n\t\tconst destDir = path.dirname(dest);\n\t\tconst dirExists = yield* fs.exists(destDir);\n\n\t\tif (!dirExists) {\n\t\t\tyield* fs.makeDirectory(destDir, { recursive: true });\n\t\t}\n\n\t\tconst dump = yield* Effect.tryPromise({\n\t\t\ttry: () => client.dumpDataDir(),\n\t\t\tcatch: (error) =>\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Failed to dump PGlite database',\n\t\t\t\t\tcause: error,\n\t\t\t\t}),\n\t\t});\n\n\t\tconst data = yield* toUint8Array(dump);\n\t\tyield* fs.writeFile(dest, data);\n\t});\n\nexport const migratePglite = (client: PGlite, migrations: PgliteMigrations) =>\n\tEffect.tryPromise({\n\t\ttry: () =>\n\t\t\tmigrate(drizzle(client), {\n\t\t\t\tmigrationsFolder: migrations.migrationsFolder,\n\t\t\t\tmigrationsTable: migrations.migrationsTable,\n\t\t\t\tmigrationsSchema: migrations.migrationsSchema,\n\t\t\t}),\n\t\tcatch: (error) =>\n\t\t\tnew PgliteError({\n\t\t\t\tmessage: `Failed to migrate pglite from ${migrations.migrationsFolder}`,\n\t\t\t\tcause: error,\n\t\t\t}),\n\t});\n\nconst closePglite = (client: PGlite) =>\n\tEffect.tryPromise({\n\t\ttry: () => client.close(),\n\t\tcatch: (error) =>\n\t\t\tnew PgliteError({\n\t\t\t\tmessage: 'Failed to close PGlite client',\n\t\t\t\tcause: error,\n\t\t\t}),\n\t});\n\nexport const getDbMigrationCount = (\n\tclient: PGlite,\n\tmigrations?: Pick<PgliteMigrations, 'migrationsTable' | 'migrationsSchema'>,\n) =>\n\tEffect.gen(function* () {\n\t\tconst qualifiedTable = qualifiedMigrationsTable(migrations ?? {});\n\n\t\tyield* Effect.tryPromise({\n\t\t\ttry: () => client.waitReady,\n\t\t\tcatch: (error) =>\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Failed to wait for PGlite ready',\n\t\t\t\t\tcause: error,\n\t\t\t\t}),\n\t\t});\n\n\t\tconst existsResult = yield* Effect.tryPromise({\n\t\t\ttry: () =>\n\t\t\t\tclient.query<{ exists: boolean }>(\n\t\t\t\t\t'SELECT to_regclass($1) IS NOT NULL AS \"exists\"',\n\t\t\t\t\t[qualifiedTable],\n\t\t\t\t),\n\t\t\tcatch: (error) =>\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Failed to check drizzle migrations table',\n\t\t\t\t\tcause: error,\n\t\t\t\t}),\n\t\t});\n\n\t\tconst existsRow = existsResult.rows[0];\n\t\tif (!existsRow) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Missing row from drizzle migrations existence check',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tif (!existsRow.exists) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst countResult = yield* Effect.tryPromise({\n\t\t\ttry: () =>\n\t\t\t\tclient.query<{ c: number }>(\n\t\t\t\t\t`SELECT count(*)::int AS c FROM ${qualifiedTable}`,\n\t\t\t\t),\n\t\t\tcatch: (error) =>\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Failed to count drizzle migrations',\n\t\t\t\t\tcause: error,\n\t\t\t\t}),\n\t\t});\n\n\t\tconst countRow = countResult.rows[0];\n\t\tif (!countRow) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Missing row from drizzle migrations count query',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn countRow.c;\n\t});\n\nexport const getExpectedMigrationCount = (migrationsFolder: string) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem;\n\t\tconst path = yield* Path;\n\t\tconst journalPath = path.join(migrationsFolder, 'meta/_journal.json');\n\t\tconst journalExists = yield* fs.exists(journalPath);\n\t\tif (!journalExists) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: `Migration journal not found at ${journalPath}`,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tconst content = yield* fs.readFileString(journalPath);\n\t\tconst journal = yield* Effect.try({\n\t\t\ttry: () => JSON.parse(content) as { entries: unknown[] },\n\t\t\tcatch: (cause) =>\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: `Failed to parse migration journal at ${journalPath}`,\n\t\t\t\t\tcause,\n\t\t\t\t}),\n\t\t});\n\t\tif (!Array.isArray(journal.entries)) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: `Migration journal at ${journalPath} is missing an \"entries\" array`,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn journal.entries.length;\n\t});\n\nexport const buildPgliteDump = (\n\topts: PgliteMigrations & { dumpPath: string },\n) =>\n\tEffect.gen(function* () {\n\t\tconst client = new PGlite(IN_MEMORY_DATA_DIR);\n\t\tyield* migratePglite(client, opts);\n\t\tyield* dumpPgliteToFile(client, opts.dumpPath);\n\t\tyield* closePglite(client);\n\t});\n\nexport const ensurePgliteDump = (\n\topts: PgliteMigrations & { dumpPath: string },\n) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem;\n\t\tconst dumpExists = yield* fs.exists(opts.dumpPath);\n\t\tif (!dumpExists) {\n\t\t\tyield* buildPgliteDump(opts);\n\t\t}\n\t});\n\nexport const openLitePglite = (\n\topts: PgliteMigrations & { dataDir: string; dumpPath: string },\n) =>\n\tEffect.gen(function* () {\n\t\tyield* ensurePgliteDump(opts);\n\t\tconst client = yield* createPgliteFromDump({\n\t\t\tdataDir: opts.dataDir,\n\t\t\tdumpPath: opts.dumpPath,\n\t\t});\n\t\tconst expected = yield* getExpectedMigrationCount(opts.migrationsFolder);\n\t\tconst actual = yield* getDbMigrationCount(client, opts);\n\t\t/** Schema is present but nothing is journaled — it predates journaled migrations. */\n\t\tconst predatesMigrationJournal = expected > 0 && actual === 0;\n\t\tif (predatesMigrationJournal) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t`Stale local pglite at ${opts.dataDir} (schema present, no migration journal) — ` +\n\t\t\t\t\t\t'it was built by an older scheme. Remove it and the dump, then restart:\\n' +\n\t\t\t\t\t\t` rm -rf ${opts.dataDir} ${opts.dumpPath}`,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tyield* migratePglite(client, opts);\n\t\treturn client;\n\t});\n\n/**\n * Prepares a PGlite database scoped to a dev session: the data dir lives at\n * `<session>/pglite` and the migration dump at `<session>/pglite.dump`. Builds\n * the dump from `migrationsFolder` if missing, hydrates the client, and runs\n * pending migrations. Closes the client before returning paths for another\n * process (e.g. `DATABASE_LITE_PATH`). Use `openLitePglite` for an in-process client.\n */\nexport const prepareSessionPglite = (\n\tsession: DevSession,\n\topts: PgliteMigrations,\n) =>\n\tEffect.gen(function* () {\n\t\tconst dataDir = yield* session.path('pglite');\n\t\tconst dumpPath = yield* session.path('pglite.dump');\n\t\tconst client = yield* openLitePglite({\n\t\t\t...opts,\n\t\t\tdataDir,\n\t\t\tdumpPath,\n\t\t});\n\t\tyield* closePglite(client);\n\t\treturn { dataDir, dumpPath };\n\t});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/pglite/index.ts"],"sourcesContent":["import { PGlite } from '@electric-sql/pglite';\nimport { readMigrationFiles } from 'drizzle-orm/migrator';\nimport { drizzle } from 'drizzle-orm/pglite';\nimport { migrate } from 'drizzle-orm/pglite/migrator';\nimport { Data, Effect } from 'effect';\nimport { FileSystem } from 'effect/FileSystem';\nimport { Path } from 'effect/Path';\nimport type { DevSession } from '../dev-sessions';\n\nexport class PgliteError extends Data.TaggedError('PgliteError')<{\n\tmessage: string;\n\tcause?: unknown;\n}> {}\n\nconst IN_MEMORY_DATA_DIR = 'memory://';\n\n/** Drizzle's own defaults, used whenever a caller doesn't override them. */\nconst DEFAULT_MIGRATIONS_TABLE = '__drizzle_migrations';\nconst DEFAULT_MIGRATIONS_SCHEMA = 'drizzle';\n\nexport type PgliteMigrations = {\n\tmigrationsFolder: string;\n\tmigrationsTable?: string;\n\tmigrationsSchema?: string;\n};\n\n/** Double-quotes an identifier part for safe interpolation into SQL. */\nconst quoteIdentifierPart = (part: string) => `\"${part.replaceAll('\"', '\"\"')}\"`;\n\nconst qualifiedMigrationsTable = (\n\tmigrations: Pick<PgliteMigrations, 'migrationsTable' | 'migrationsSchema'>,\n) =>\n\t`${quoteIdentifierPart(migrations.migrationsSchema ?? DEFAULT_MIGRATIONS_SCHEMA)}.${quoteIdentifierPart(migrations.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE)}`;\n\n/**\n * A persisted data dir that already exists holds a hydrated database, so it is\n * opened as-is rather than re-seeded from the dump. In-memory clients have\n * nothing to reuse.\n */\nconst shouldReuseExistingDatabase = (dataDir: string | undefined) =>\n\tEffect.gen(function* () {\n\t\tif (!dataDir || dataDir === IN_MEMORY_DATA_DIR) {\n\t\t\treturn false;\n\t\t}\n\t\tconst fs = yield* FileSystem;\n\t\treturn yield* fs.exists(dataDir);\n\t});\n\nexport const createPgliteFromDump = (opts: {\n\tdataDir?: string;\n\tdumpPath: string;\n}) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem;\n\n\t\tif (yield* shouldReuseExistingDatabase(opts.dataDir)) {\n\t\t\treturn new PGlite(opts.dataDir);\n\t\t}\n\n\t\tif (!(yield* fs.exists(opts.dumpPath))) {\n\t\t\treturn yield* new PgliteError({\n\t\t\t\tmessage: `Dump file not found at ${opts.dumpPath}`,\n\t\t\t});\n\t\t}\n\n\t\tconst buffer = yield* fs.readFile(opts.dumpPath);\n\t\tconst file = new Blob([new Uint8Array(buffer)]);\n\n\t\tconst client = new PGlite(opts.dataDir, {\n\t\t\tloadDataDir: file,\n\t\t});\n\n\t\treturn client;\n\t});\n\nconst toUint8Array = (input: File | Blob) =>\n\tEffect.gen(function* () {\n\t\tconst buf = yield* Effect.tryPromise({\n\t\t\ttry: () => input.arrayBuffer(),\n\t\t\tcatch: (error) =>\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Failed to convert dump to Uint8Array',\n\t\t\t\t\tcause: error,\n\t\t\t\t}),\n\t\t});\n\t\treturn new Uint8Array(buf);\n\t});\n\nexport const dumpPgliteToFile = (client: PGlite, dest: string) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem;\n\t\tconst path = yield* Path;\n\n\t\tconst destDir = path.dirname(dest);\n\t\tconst dirExists = yield* fs.exists(destDir);\n\n\t\tif (!dirExists) {\n\t\t\tyield* fs.makeDirectory(destDir, { recursive: true });\n\t\t}\n\n\t\tconst dump = yield* Effect.tryPromise({\n\t\t\ttry: () => client.dumpDataDir(),\n\t\t\tcatch: (error) =>\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Failed to dump PGlite database',\n\t\t\t\t\tcause: error,\n\t\t\t\t}),\n\t\t});\n\n\t\tconst data = yield* toUint8Array(dump);\n\t\tconst tempName = yield* Effect.sync(\n\t\t\t() => `.${path.basename(dest)}.${crypto.randomUUID()}.tmp`,\n\t\t);\n\t\tconst tempPath = path.join(destDir, tempName);\n\t\t// A sibling rename keeps concurrent readers from seeing a partial dump.\n\t\tyield* Effect.gen(function* () {\n\t\t\tyield* fs.writeFile(tempPath, data);\n\t\t\tyield* fs.rename(tempPath, dest);\n\t\t}).pipe(\n\t\t\tEffect.onError(() =>\n\t\t\t\tfs.remove(tempPath, { force: true }).pipe(Effect.orDie),\n\t\t\t),\n\t\t);\n\t});\n\nexport const migratePglite = (client: PGlite, migrations: PgliteMigrations) =>\n\tEffect.tryPromise({\n\t\ttry: () =>\n\t\t\tmigrate(drizzle({ client }), {\n\t\t\t\tmigrationsFolder: migrations.migrationsFolder,\n\t\t\t\tmigrationsTable: migrations.migrationsTable,\n\t\t\t\tmigrationsSchema: migrations.migrationsSchema,\n\t\t\t}),\n\t\tcatch: (error) =>\n\t\t\tnew PgliteError({\n\t\t\t\tmessage: `Failed to migrate pglite from ${migrations.migrationsFolder}`,\n\t\t\t\tcause: error,\n\t\t\t}),\n\t});\n\nconst closePglite = (client: PGlite) =>\n\tEffect.tryPromise({\n\t\ttry: () => client.close(),\n\t\tcatch: (error) =>\n\t\t\tnew PgliteError({\n\t\t\t\tmessage: 'Failed to close PGlite client',\n\t\t\t\tcause: error,\n\t\t\t}),\n\t});\n\nexport const getDbMigrationCount = (\n\tclient: PGlite,\n\tmigrations?: Pick<PgliteMigrations, 'migrationsTable' | 'migrationsSchema'>,\n) =>\n\tEffect.gen(function* () {\n\t\tconst qualifiedTable = qualifiedMigrationsTable(migrations ?? {});\n\n\t\tyield* Effect.tryPromise({\n\t\t\ttry: () => client.waitReady,\n\t\t\tcatch: (error) =>\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Failed to wait for PGlite ready',\n\t\t\t\t\tcause: error,\n\t\t\t\t}),\n\t\t});\n\n\t\tconst existsResult = yield* Effect.tryPromise({\n\t\t\ttry: () =>\n\t\t\t\tclient.query<{ exists: boolean }>(\n\t\t\t\t\t'SELECT to_regclass($1) IS NOT NULL AS \"exists\"',\n\t\t\t\t\t[qualifiedTable],\n\t\t\t\t),\n\t\t\tcatch: (error) =>\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Failed to check drizzle migrations table',\n\t\t\t\t\tcause: error,\n\t\t\t\t}),\n\t\t});\n\n\t\tconst existsRow = existsResult.rows[0];\n\t\tif (!existsRow) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Missing row from drizzle migrations existence check',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tif (!existsRow.exists) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst countResult = yield* Effect.tryPromise({\n\t\t\ttry: () =>\n\t\t\t\tclient.query<{ c: number }>(\n\t\t\t\t\t`SELECT count(*)::int AS c FROM ${qualifiedTable}`,\n\t\t\t\t),\n\t\t\tcatch: (error) =>\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Failed to count drizzle migrations',\n\t\t\t\t\tcause: error,\n\t\t\t\t}),\n\t\t});\n\n\t\tconst countRow = countResult.rows[0];\n\t\tif (!countRow) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage: 'Missing row from drizzle migrations count query',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn countRow.c;\n\t});\n\nexport const getExpectedMigrationCount = (migrationsFolder: string) =>\n\tEffect.try({\n\t\ttry: () => readMigrationFiles({ migrationsFolder }).length,\n\t\tcatch: (cause) =>\n\t\t\tnew PgliteError({\n\t\t\t\tmessage: `Failed to read migrations from ${migrationsFolder}`,\n\t\t\t\tcause,\n\t\t\t}),\n\t});\n\nexport const buildPgliteDump = (\n\topts: PgliteMigrations & { dumpPath: string },\n) =>\n\tEffect.gen(function* () {\n\t\tconst client = new PGlite(IN_MEMORY_DATA_DIR);\n\t\tyield* migratePglite(client, opts);\n\t\tyield* dumpPgliteToFile(client, opts.dumpPath);\n\t\tyield* closePglite(client);\n\t});\n\nexport const ensurePgliteDump = (\n\topts: PgliteMigrations & { dumpPath: string },\n) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem;\n\t\tconst dumpExists = yield* fs.exists(opts.dumpPath);\n\t\tif (!dumpExists) {\n\t\t\tyield* buildPgliteDump(opts);\n\t\t}\n\t});\n\nexport const openLitePglite = (\n\topts: PgliteMigrations & { dataDir: string; dumpPath: string },\n) =>\n\tEffect.gen(function* () {\n\t\tyield* ensurePgliteDump(opts);\n\t\tconst client = yield* createPgliteFromDump({\n\t\t\tdataDir: opts.dataDir,\n\t\t\tdumpPath: opts.dumpPath,\n\t\t});\n\t\tconst expected = yield* getExpectedMigrationCount(opts.migrationsFolder);\n\t\tconst actual = yield* getDbMigrationCount(client, opts);\n\t\t/** Schema is present but nothing is journaled — it predates journaled migrations. */\n\t\tconst predatesMigrationJournal = expected > 0 && actual === 0;\n\t\tif (predatesMigrationJournal) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew PgliteError({\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t`Stale local pglite at ${opts.dataDir} (schema present, no migration journal) — ` +\n\t\t\t\t\t\t'it was built by an older scheme. Remove it and the dump, then restart:\\n' +\n\t\t\t\t\t\t` rm -rf ${opts.dataDir} ${opts.dumpPath}`,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tyield* migratePglite(client, opts);\n\t\treturn client;\n\t});\n\n/**\n * Prepares a PGlite database scoped to a dev session: the data dir lives at\n * `<session>/pglite` and the migration dump at `<session>/pglite.dump`. Builds\n * the dump from `migrationsFolder` if missing, hydrates the client, and runs\n * pending migrations. Closes the client before returning paths for another\n * process (e.g. `DATABASE_LITE_PATH`). Use `openLitePglite` for an in-process client.\n */\nexport const prepareSessionPglite = (\n\tsession: DevSession,\n\topts: PgliteMigrations,\n) =>\n\tEffect.gen(function* () {\n\t\tconst dataDir = yield* session.path('pglite');\n\t\tconst dumpPath = yield* session.path('pglite.dump');\n\t\tconst client = yield* openLitePglite({\n\t\t\t...opts,\n\t\t\tdataDir,\n\t\t\tdumpPath,\n\t\t});\n\t\tyield* closePglite(client);\n\t\treturn { dataDir, dumpPath };\n\t});\n"],"mappings":";AAAA,SAAS,cAAc;AACvB,SAAS,0BAA0B;AACnC,SAAS,eAAe;AACxB,SAAS,eAAe;AACxB,SAAS,MAAM,cAAc;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AAGd,IAAM,cAAN,cAA0B,KAAK,YAAY,aAAa,EAG5D;AAAC;AAEJ,IAAM,qBAAqB;AAG3B,IAAM,2BAA2B;AACjC,IAAM,4BAA4B;AASlC,IAAM,sBAAsB,CAAC,SAAiB,IAAI,KAAK,WAAW,KAAK,IAAI,CAAC;AAE5E,IAAM,2BAA2B,CAChC,eAEA,GAAG,oBAAoB,WAAW,oBAAoB,yBAAyB,CAAC,IAAI,oBAAoB,WAAW,mBAAmB,wBAAwB,CAAC;AAOhK,IAAM,8BAA8B,CAAC,YACpC,OAAO,IAAI,aAAa;AACvB,MAAI,CAAC,WAAW,YAAY,oBAAoB;AAC/C,WAAO;AAAA,EACR;AACA,QAAM,KAAK,OAAO;AAClB,SAAO,OAAO,GAAG,OAAO,OAAO;AAChC,CAAC;AAEK,IAAM,uBAAuB,CAAC,SAIpC,OAAO,IAAI,aAAa;AACvB,QAAM,KAAK,OAAO;AAElB,MAAI,OAAO,4BAA4B,KAAK,OAAO,GAAG;AACrD,WAAO,IAAI,OAAO,KAAK,OAAO;AAAA,EAC/B;AAEA,MAAI,EAAE,OAAO,GAAG,OAAO,KAAK,QAAQ,IAAI;AACvC,WAAO,OAAO,IAAI,YAAY;AAAA,MAC7B,SAAS,0BAA0B,KAAK,QAAQ;AAAA,IACjD,CAAC;AAAA,EACF;AAEA,QAAM,SAAS,OAAO,GAAG,SAAS,KAAK,QAAQ;AAC/C,QAAM,OAAO,IAAI,KAAK,CAAC,IAAI,WAAW,MAAM,CAAC,CAAC;AAE9C,QAAM,SAAS,IAAI,OAAO,KAAK,SAAS;AAAA,IACvC,aAAa;AAAA,EACd,CAAC;AAED,SAAO;AACR,CAAC;AAEF,IAAM,eAAe,CAAC,UACrB,OAAO,IAAI,aAAa;AACvB,QAAM,MAAM,OAAO,OAAO,WAAW;AAAA,IACpC,KAAK,MAAM,MAAM,YAAY;AAAA,IAC7B,OAAO,CAAC,UACP,IAAI,YAAY;AAAA,MACf,SAAS;AAAA,MACT,OAAO;AAAA,IACR,CAAC;AAAA,EACH,CAAC;AACD,SAAO,IAAI,WAAW,GAAG;AAC1B,CAAC;AAEK,IAAM,mBAAmB,CAAC,QAAgB,SAChD,OAAO,IAAI,aAAa;AACvB,QAAM,KAAK,OAAO;AAClB,QAAM,OAAO,OAAO;AAEpB,QAAM,UAAU,KAAK,QAAQ,IAAI;AACjC,QAAM,YAAY,OAAO,GAAG,OAAO,OAAO;AAE1C,MAAI,CAAC,WAAW;AACf,WAAO,GAAG,cAAc,SAAS,EAAE,WAAW,KAAK,CAAC;AAAA,EACrD;AAEA,QAAM,OAAO,OAAO,OAAO,WAAW;AAAA,IACrC,KAAK,MAAM,OAAO,YAAY;AAAA,IAC9B,OAAO,CAAC,UACP,IAAI,YAAY;AAAA,MACf,SAAS;AAAA,MACT,OAAO;AAAA,IACR,CAAC;AAAA,EACH,CAAC;AAED,QAAM,OAAO,OAAO,aAAa,IAAI;AACrC,QAAM,WAAW,OAAO,OAAO;AAAA,IAC9B,MAAM,IAAI,KAAK,SAAS,IAAI,CAAC,IAAI,OAAO,WAAW,CAAC;AAAA,EACrD;AACA,QAAM,WAAW,KAAK,KAAK,SAAS,QAAQ;AAE5C,SAAO,OAAO,IAAI,aAAa;AAC9B,WAAO,GAAG,UAAU,UAAU,IAAI;AAClC,WAAO,GAAG,OAAO,UAAU,IAAI;AAAA,EAChC,CAAC,EAAE;AAAA,IACF,OAAO;AAAA,MAAQ,MACd,GAAG,OAAO,UAAU,EAAE,OAAO,KAAK,CAAC,EAAE,KAAK,OAAO,KAAK;AAAA,IACvD;AAAA,EACD;AACD,CAAC;AAEK,IAAM,gBAAgB,CAAC,QAAgB,eAC7C,OAAO,WAAW;AAAA,EACjB,KAAK,MACJ,QAAQ,QAAQ,EAAE,OAAO,CAAC,GAAG;AAAA,IAC5B,kBAAkB,WAAW;AAAA,IAC7B,iBAAiB,WAAW;AAAA,IAC5B,kBAAkB,WAAW;AAAA,EAC9B,CAAC;AAAA,EACF,OAAO,CAAC,UACP,IAAI,YAAY;AAAA,IACf,SAAS,iCAAiC,WAAW,gBAAgB;AAAA,IACrE,OAAO;AAAA,EACR,CAAC;AACH,CAAC;AAEF,IAAM,cAAc,CAAC,WACpB,OAAO,WAAW;AAAA,EACjB,KAAK,MAAM,OAAO,MAAM;AAAA,EACxB,OAAO,CAAC,UACP,IAAI,YAAY;AAAA,IACf,SAAS;AAAA,IACT,OAAO;AAAA,EACR,CAAC;AACH,CAAC;AAEK,IAAM,sBAAsB,CAClC,QACA,eAEA,OAAO,IAAI,aAAa;AACvB,QAAM,iBAAiB,yBAAyB,cAAc,CAAC,CAAC;AAEhE,SAAO,OAAO,WAAW;AAAA,IACxB,KAAK,MAAM,OAAO;AAAA,IAClB,OAAO,CAAC,UACP,IAAI,YAAY;AAAA,MACf,SAAS;AAAA,MACT,OAAO;AAAA,IACR,CAAC;AAAA,EACH,CAAC;AAED,QAAM,eAAe,OAAO,OAAO,WAAW;AAAA,IAC7C,KAAK,MACJ,OAAO;AAAA,MACN;AAAA,MACA,CAAC,cAAc;AAAA,IAChB;AAAA,IACD,OAAO,CAAC,UACP,IAAI,YAAY;AAAA,MACf,SAAS;AAAA,MACT,OAAO;AAAA,IACR,CAAC;AAAA,EACH,CAAC;AAED,QAAM,YAAY,aAAa,KAAK,CAAC;AACrC,MAAI,CAAC,WAAW;AACf,WAAO,OAAO,OAAO;AAAA,MACpB,IAAI,YAAY;AAAA,QACf,SAAS;AAAA,MACV,CAAC;AAAA,IACF;AAAA,EACD;AACA,MAAI,CAAC,UAAU,QAAQ;AACtB,WAAO;AAAA,EACR;AAEA,QAAM,cAAc,OAAO,OAAO,WAAW;AAAA,IAC5C,KAAK,MACJ,OAAO;AAAA,MACN,kCAAkC,cAAc;AAAA,IACjD;AAAA,IACD,OAAO,CAAC,UACP,IAAI,YAAY;AAAA,MACf,SAAS;AAAA,MACT,OAAO;AAAA,IACR,CAAC;AAAA,EACH,CAAC;AAED,QAAM,WAAW,YAAY,KAAK,CAAC;AACnC,MAAI,CAAC,UAAU;AACd,WAAO,OAAO,OAAO;AAAA,MACpB,IAAI,YAAY;AAAA,QACf,SAAS;AAAA,MACV,CAAC;AAAA,IACF;AAAA,EACD;AACA,SAAO,SAAS;AACjB,CAAC;AAEK,IAAM,4BAA4B,CAAC,qBACzC,OAAO,IAAI;AAAA,EACV,KAAK,MAAM,mBAAmB,EAAE,iBAAiB,CAAC,EAAE;AAAA,EACpD,OAAO,CAAC,UACP,IAAI,YAAY;AAAA,IACf,SAAS,kCAAkC,gBAAgB;AAAA,IAC3D;AAAA,EACD,CAAC;AACH,CAAC;AAEK,IAAM,kBAAkB,CAC9B,SAEA,OAAO,IAAI,aAAa;AACvB,QAAM,SAAS,IAAI,OAAO,kBAAkB;AAC5C,SAAO,cAAc,QAAQ,IAAI;AACjC,SAAO,iBAAiB,QAAQ,KAAK,QAAQ;AAC7C,SAAO,YAAY,MAAM;AAC1B,CAAC;AAEK,IAAM,mBAAmB,CAC/B,SAEA,OAAO,IAAI,aAAa;AACvB,QAAM,KAAK,OAAO;AAClB,QAAM,aAAa,OAAO,GAAG,OAAO,KAAK,QAAQ;AACjD,MAAI,CAAC,YAAY;AAChB,WAAO,gBAAgB,IAAI;AAAA,EAC5B;AACD,CAAC;AAEK,IAAM,iBAAiB,CAC7B,SAEA,OAAO,IAAI,aAAa;AACvB,SAAO,iBAAiB,IAAI;AAC5B,QAAM,SAAS,OAAO,qBAAqB;AAAA,IAC1C,SAAS,KAAK;AAAA,IACd,UAAU,KAAK;AAAA,EAChB,CAAC;AACD,QAAM,WAAW,OAAO,0BAA0B,KAAK,gBAAgB;AACvE,QAAM,SAAS,OAAO,oBAAoB,QAAQ,IAAI;AAEtD,QAAM,2BAA2B,WAAW,KAAK,WAAW;AAC5D,MAAI,0BAA0B;AAC7B,WAAO,OAAO,OAAO;AAAA,MACpB,IAAI,YAAY;AAAA,QACf,SACC,yBAAyB,KAAK,OAAO;AAAA,WAEzB,KAAK,OAAO,IAAI,KAAK,QAAQ;AAAA,MAC3C,CAAC;AAAA,IACF;AAAA,EACD;AACA,SAAO,cAAc,QAAQ,IAAI;AACjC,SAAO;AACR,CAAC;AASK,IAAM,uBAAuB,CACnC,SACA,SAEA,OAAO,IAAI,aAAa;AACvB,QAAM,UAAU,OAAO,QAAQ,KAAK,QAAQ;AAC5C,QAAM,WAAW,OAAO,QAAQ,KAAK,aAAa;AAClD,QAAM,SAAS,OAAO,eAAe;AAAA,IACpC,GAAG;AAAA,IACH;AAAA,IACA;AAAA,EACD,CAAC;AACD,SAAO,YAAY,MAAM;AACzB,SAAO,EAAE,SAAS,SAAS;AAC5B,CAAC;","names":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { FileSystem } from 'effect/FileSystem';
|
|
3
|
+
import { Path } from 'effect/Path';
|
|
4
|
+
import { DevSessions } from '../dev-sessions';
|
|
5
|
+
import { type ServiceContext, ServiceError } from './core';
|
|
6
|
+
import { type Healthcheck } from './docker-args';
|
|
7
|
+
export type ContainerSpec<Port extends string> = {
|
|
8
|
+
image: string;
|
|
9
|
+
ports?: Record<Port, number>;
|
|
10
|
+
volumes?: Record<string, string>;
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
healthcheck?: Healthcheck;
|
|
13
|
+
};
|
|
14
|
+
export declare const startContainer: <Port extends string>(name: string, spec: ContainerSpec<Port>) => (ctx: ServiceContext<Port>) => Effect.Effect<{
|
|
15
|
+
container: string;
|
|
16
|
+
}, ServiceError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | DevSessions | FileSystem | Path | import("effect/Scope").Scope>;
|
|
17
|
+
//# sourceMappingURL=container.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../../src/services/container.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EACN,KAAK,cAAc,EACnB,YAAY,EAGZ,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAgB,KAAK,WAAW,EAAiB,MAAM,eAAe,CAAC;AAE9E,MAAM,MAAM,aAAa,CAAC,IAAI,SAAS,MAAM,IAAI;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,WAAW,CAAC,EAAE,WAAW,CAAC;CAC1B,CAAC;AA2FF,eAAO,MAAM,cAAc,GACzB,IAAI,SAAS,MAAM,QAAQ,MAAM,QAAQ,aAAa,CAAC,IAAI,CAAC,WACvD,cAAc,CAAC,IAAI,CAAC;;mJA2FiD,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import type { FileSystem } from 'effect/FileSystem';
|
|
3
|
+
import type { Path } from 'effect/Path';
|
|
4
|
+
import type { Scope } from 'effect/Scope';
|
|
5
|
+
import type { DevSession } from '../dev-sessions';
|
|
6
|
+
import type { RunningService, ServiceDefinition } from './index';
|
|
7
|
+
export declare const serviceBrand: unique symbol;
|
|
8
|
+
export declare const isServiceDefinition: (value: unknown) => value is ServiceDefinition<string, string, unknown, unknown, Scope | FileSystem | Path>;
|
|
9
|
+
export declare const withServiceError: (message: string) => <A, R>(self: Effect.Effect<A, unknown, R>) => Effect.Effect<A, ServiceError, R>;
|
|
10
|
+
export declare const runningService: <Port extends string, A>(result: {
|
|
11
|
+
value: A;
|
|
12
|
+
ports: Readonly<Record<Port, number>>;
|
|
13
|
+
}) => RunningService<Port, A>;
|
|
14
|
+
declare const ServiceError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
15
|
+
readonly _tag: "ServiceError";
|
|
16
|
+
} & Readonly<A>;
|
|
17
|
+
export declare class ServiceError extends ServiceError_base<{
|
|
18
|
+
message: string;
|
|
19
|
+
cause?: unknown;
|
|
20
|
+
}> {
|
|
21
|
+
}
|
|
22
|
+
export type ServiceContext<Port extends string> = {
|
|
23
|
+
readonly session: DevSession;
|
|
24
|
+
readonly ports: Readonly<Record<Port, number>>;
|
|
25
|
+
/** `<session>/services/<service>`, created before `start` runs and kept after it stops. */
|
|
26
|
+
readonly dataDir: string;
|
|
27
|
+
};
|
|
28
|
+
export declare const validName: (name: string) => boolean;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/services/core.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjE,eAAO,MAAM,YAAY,eAA0C,CAAC;AAEpE,eAAO,MAAM,mBAAmB,UACxB,OAAO,KACZ,KAAK,IAAI,iBAAiB,CAC5B,MAAM,EACN,MAAM,EACN,OAAO,EACP,OAAO,EACP,KAAK,GAAG,UAAU,GAAG,IAAI,CAKG,CAAC;AAE9B,eAAO,MAAM,gBAAgB,YAAa,MAAM,oFAK9C,CAAC;AAEH,eAAO,MAAM,cAAc,GAAI,IAAI,SAAS,MAAM,EAAE,CAAC,UAAU;IAC9D,KAAK,EAAE,CAAC,CAAC;IACT,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;CACtC,KAMM,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;;;;AAE/B,qBAAa,YAAa,SAAQ,kBAAiC;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;CAAG;AAEL,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,MAAM,IAAI;IACjD,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,eAAO,MAAM,SAAS,SAAU,MAAM,YACI,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Duration } from 'effect';
|
|
2
|
+
export type Healthcheck = {
|
|
3
|
+
test: readonly string[];
|
|
4
|
+
interval?: Duration.Input;
|
|
5
|
+
timeout?: Duration.Input;
|
|
6
|
+
retries?: number;
|
|
7
|
+
startPeriod?: Duration.Input;
|
|
8
|
+
};
|
|
9
|
+
export declare const buildRunArgs: (input: {
|
|
10
|
+
image: string;
|
|
11
|
+
container: string;
|
|
12
|
+
root: string;
|
|
13
|
+
session: string;
|
|
14
|
+
uid: number;
|
|
15
|
+
gid: number;
|
|
16
|
+
ports: ReadonlyArray<{
|
|
17
|
+
host: number;
|
|
18
|
+
container: number;
|
|
19
|
+
}>;
|
|
20
|
+
volumes: ReadonlyArray<{
|
|
21
|
+
source: string;
|
|
22
|
+
target: string;
|
|
23
|
+
}>;
|
|
24
|
+
env: Record<string, string>;
|
|
25
|
+
healthcheck: Healthcheck | undefined;
|
|
26
|
+
}) => string[];
|
|
27
|
+
export declare const selectOrphans: (containers: ReadonlyArray<{
|
|
28
|
+
id: string;
|
|
29
|
+
session: string;
|
|
30
|
+
}>, existingSessions: ReadonlySet<string>) => string[];
|
|
31
|
+
//# sourceMappingURL=docker-args.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docker-args.d.ts","sourceRoot":"","sources":["../../src/services/docker-args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElC,MAAM,MAAM,WAAW,GAAG;IACzB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;IAC1B,OAAO,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;CAC7B,CAAC;AAcF,eAAO,MAAM,YAAY,UAAW;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1D,OAAO,EAAE,aAAa,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,WAAW,EAAE,WAAW,GAAG,SAAS,CAAC;CACrC,KAAG,MAAM,EAoCT,CAAC;AAEF,eAAO,MAAM,aAAa,eACb,aAAa,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,oBACxC,WAAW,CAAC,MAAM,CAAC,aAIH,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from 'effect';
|
|
2
|
+
import type { FileSystem } from 'effect/FileSystem';
|
|
3
|
+
import type { Path } from 'effect/Path';
|
|
4
|
+
import type { Scope } from 'effect/Scope';
|
|
5
|
+
import { CurrentSession } from '../current-session';
|
|
6
|
+
import type { DevSession } from '../dev-sessions';
|
|
7
|
+
import { type ContainerSpec } from './container';
|
|
8
|
+
import { type ServiceContext, ServiceError, serviceBrand } from './core';
|
|
9
|
+
import { type SharedOptions } from './shared/index';
|
|
10
|
+
export type { Healthcheck } from './docker-args';
|
|
11
|
+
export { type ServiceContext, ServiceError };
|
|
12
|
+
export type RunningService<Port extends string, A> = (A extends object ? Omit<A, 'ports'> : unknown) & {
|
|
13
|
+
readonly ports: Readonly<Record<Port, number>>;
|
|
14
|
+
};
|
|
15
|
+
type ServiceIdentifier<Name extends string> = {
|
|
16
|
+
readonly devsessService: Name;
|
|
17
|
+
};
|
|
18
|
+
export type ServiceDefinition<Name extends string, Port extends string, A, E, R> = {
|
|
19
|
+
readonly [serviceBrand]: true;
|
|
20
|
+
readonly name: Name;
|
|
21
|
+
readonly ports: ReadonlyArray<Port>;
|
|
22
|
+
readonly shared?: SharedOptions<A>;
|
|
23
|
+
readonly definitionStack?: string;
|
|
24
|
+
/** Resolves once the service is ready; its scope closing stops it. */
|
|
25
|
+
readonly start: (ctx: ServiceContext<Port>) => Effect.Effect<A, E, R>;
|
|
26
|
+
/** Yield inside another service's `start` (or any effect) to depend on this one. */
|
|
27
|
+
readonly key: Context.Key<ServiceIdentifier<Name>, RunningService<Port, A>>;
|
|
28
|
+
/** Starts the service once per layer build and shares it with every consumer. */
|
|
29
|
+
readonly layer: Layer.Layer<ServiceIdentifier<Name>, E | ServiceError, Exclude<R, Scope> | CurrentSession | FileSystem | Path>;
|
|
30
|
+
};
|
|
31
|
+
declare const run: <Name extends string, Port extends string, A, E, R>(session: DevSession, def: Pick<ServiceDefinition<Name, Port, A, E, R>, 'name' | 'ports' | 'start' | 'shared' | 'definitionStack'>) => Effect.Effect<RunningService<Port, A>, E | ServiceError, R | FileSystem | Path | Scope>;
|
|
32
|
+
declare const make: <const Name extends string, A, E, R, const Port extends string = never>(def: {
|
|
33
|
+
name: Name;
|
|
34
|
+
ports?: ReadonlyArray<Port>;
|
|
35
|
+
shared?: SharedOptions<A>;
|
|
36
|
+
start: (ctx: ServiceContext<Port>) => Effect.Effect<A, E, R>;
|
|
37
|
+
}) => ServiceDefinition<Name, Port, A, E, R>;
|
|
38
|
+
export declare const Service: {
|
|
39
|
+
/** Defines a service whose `start` handler you write yourself. */
|
|
40
|
+
make: typeof make;
|
|
41
|
+
/** Defines a service backed by a Docker container, ready once its healthcheck passes. */
|
|
42
|
+
container: <const Name extends string, const Port extends string = never>(def: {
|
|
43
|
+
name: Name;
|
|
44
|
+
} & ContainerSpec<Port>) => ServiceDefinition<Name, Port, {
|
|
45
|
+
container: string;
|
|
46
|
+
}, ServiceError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("..").DevSessions | FileSystem | Path | Scope>;
|
|
47
|
+
/** Starts a service in `session`, outside a layer. */
|
|
48
|
+
run: typeof run;
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CurrentSession
|
|
3
|
+
} from "../chunk-UOIMVHGX.js";
|
|
4
|
+
import {
|
|
5
|
+
ServiceError,
|
|
6
|
+
acquireHost,
|
|
7
|
+
failWhenHostExits,
|
|
8
|
+
hostPaths,
|
|
9
|
+
launchHost,
|
|
10
|
+
readyMessage,
|
|
11
|
+
reportDaemonService,
|
|
12
|
+
runLocal,
|
|
13
|
+
runningService,
|
|
14
|
+
serviceBrand,
|
|
15
|
+
validName,
|
|
16
|
+
withServiceError
|
|
17
|
+
} from "../chunk-OODLZZCC.js";
|
|
18
|
+
import {
|
|
19
|
+
DevSessions
|
|
20
|
+
} from "../chunk-MBPIOYW3.js";
|
|
21
|
+
|
|
22
|
+
// src/services/index.ts
|
|
23
|
+
import { Context, Effect as Effect5, Layer } from "effect";
|
|
24
|
+
|
|
25
|
+
// src/services/container.ts
|
|
26
|
+
import { Effect, Stream } from "effect";
|
|
27
|
+
import { FileSystem } from "effect/FileSystem";
|
|
28
|
+
import { Path } from "effect/Path";
|
|
29
|
+
import { ChildProcess } from "effect/process";
|
|
30
|
+
|
|
31
|
+
// src/services/docker-args.ts
|
|
32
|
+
import { Duration } from "effect";
|
|
33
|
+
var dockerDuration = (input) => `${Duration.toMillis(input)}ms`;
|
|
34
|
+
var shellQuote = (part) => `'${part.replaceAll("'", "'\\''")}'`;
|
|
35
|
+
var healthCmd = (test) => {
|
|
36
|
+
if (test[0] === "CMD-SHELL") return test.slice(1).join(" ");
|
|
37
|
+
if (test[0] === "CMD") return test.slice(1).map(shellQuote).join(" ");
|
|
38
|
+
return test.join(" ");
|
|
39
|
+
};
|
|
40
|
+
var buildRunArgs = (input) => {
|
|
41
|
+
const args = [
|
|
42
|
+
"run",
|
|
43
|
+
"-d",
|
|
44
|
+
"--name",
|
|
45
|
+
input.container,
|
|
46
|
+
"--label",
|
|
47
|
+
`devsess.session=${input.session}`,
|
|
48
|
+
"--label",
|
|
49
|
+
`devsess.root=${input.root}`,
|
|
50
|
+
"--user",
|
|
51
|
+
`${input.uid}:${input.gid}`
|
|
52
|
+
];
|
|
53
|
+
for (const port of input.ports)
|
|
54
|
+
args.push("-p", `127.0.0.1:${port.host}:${port.container}`);
|
|
55
|
+
for (const volume of input.volumes)
|
|
56
|
+
args.push("-v", `${volume.source}:${volume.target}`);
|
|
57
|
+
for (const entry of Object.entries(input.env))
|
|
58
|
+
args.push("-e", `${entry[0]}=${entry[1]}`);
|
|
59
|
+
const healthcheck = input.healthcheck;
|
|
60
|
+
if (healthcheck) {
|
|
61
|
+
args.push("--health-cmd", healthCmd(healthcheck.test));
|
|
62
|
+
if (healthcheck.interval !== void 0)
|
|
63
|
+
args.push("--health-interval", dockerDuration(healthcheck.interval));
|
|
64
|
+
if (healthcheck.timeout !== void 0)
|
|
65
|
+
args.push("--health-timeout", dockerDuration(healthcheck.timeout));
|
|
66
|
+
if (healthcheck.retries !== void 0)
|
|
67
|
+
args.push("--health-retries", String(healthcheck.retries));
|
|
68
|
+
if (healthcheck.startPeriod !== void 0)
|
|
69
|
+
args.push(
|
|
70
|
+
"--health-start-period",
|
|
71
|
+
dockerDuration(healthcheck.startPeriod)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
args.push(input.image);
|
|
75
|
+
return args;
|
|
76
|
+
};
|
|
77
|
+
var selectOrphans = (containers, existingSessions) => containers.filter((container) => !existingSessions.has(container.session)).map((container) => container.id);
|
|
78
|
+
|
|
79
|
+
// src/services/container.ts
|
|
80
|
+
var docker = (args, options) => Effect.gen(function* () {
|
|
81
|
+
const command = ChildProcess.make("docker", args, {
|
|
82
|
+
stdout: options?.progress ? "inherit" : "pipe",
|
|
83
|
+
stderr: "inherit"
|
|
84
|
+
});
|
|
85
|
+
const result = yield* Effect.scoped(
|
|
86
|
+
Effect.gen(function* () {
|
|
87
|
+
const child = yield* command;
|
|
88
|
+
const output = options?.progress ? void 0 : (yield* Stream.mkString(Stream.decodeText(child.stdout))).trim();
|
|
89
|
+
const code = yield* child.exitCode;
|
|
90
|
+
return { output, code };
|
|
91
|
+
})
|
|
92
|
+
).pipe(
|
|
93
|
+
Effect.mapError(
|
|
94
|
+
(cause) => new ServiceError({
|
|
95
|
+
message: `Docker command failed: docker ${args.join(" ")}`,
|
|
96
|
+
cause
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
if (result.code !== 0 && !options?.allowFailure) {
|
|
101
|
+
return yield* new ServiceError({
|
|
102
|
+
message: `Docker command failed (exit ${result.code}): docker ${args.join(" ")}`
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
});
|
|
107
|
+
var listContainers = (root) => Effect.gen(function* () {
|
|
108
|
+
const listed = yield* docker([
|
|
109
|
+
"ps",
|
|
110
|
+
"-a",
|
|
111
|
+
"--filter",
|
|
112
|
+
`label=devsess.root=${root}`,
|
|
113
|
+
"--format",
|
|
114
|
+
'{{.ID}} {{.Label "devsess.session"}}'
|
|
115
|
+
]);
|
|
116
|
+
if (listed.output === void 0)
|
|
117
|
+
return yield* new ServiceError({
|
|
118
|
+
message: "Docker container listing returned no output"
|
|
119
|
+
});
|
|
120
|
+
const containers = [];
|
|
121
|
+
for (const line of listed.output.split("\n").filter(Boolean)) {
|
|
122
|
+
const fields = line.split(" ");
|
|
123
|
+
const id = fields[0];
|
|
124
|
+
const session = fields[1];
|
|
125
|
+
if (!id || !session)
|
|
126
|
+
return yield* new ServiceError({
|
|
127
|
+
message: `Invalid devsess container metadata: ${line}`
|
|
128
|
+
});
|
|
129
|
+
containers.push({ id, session });
|
|
130
|
+
}
|
|
131
|
+
return containers;
|
|
132
|
+
});
|
|
133
|
+
var waitHealthy = (name, container) => Effect.gen(function* () {
|
|
134
|
+
while (true) {
|
|
135
|
+
const state = yield* docker([
|
|
136
|
+
"inspect",
|
|
137
|
+
"--format",
|
|
138
|
+
"{{.State.Running}} {{.State.Health.Status}}",
|
|
139
|
+
container
|
|
140
|
+
]);
|
|
141
|
+
if (state.output === void 0)
|
|
142
|
+
return yield* new ServiceError({
|
|
143
|
+
message: `Docker returned no status for ${container}`
|
|
144
|
+
});
|
|
145
|
+
if (state.output === "true healthy") return;
|
|
146
|
+
if (state.output.startsWith("false "))
|
|
147
|
+
return yield* new ServiceError({
|
|
148
|
+
message: `Service ${name} exited before healthy; services run as your user; this image may require root`
|
|
149
|
+
});
|
|
150
|
+
if (state.output === "true unhealthy")
|
|
151
|
+
return yield* new ServiceError({
|
|
152
|
+
message: `Service ${name} is unhealthy`
|
|
153
|
+
});
|
|
154
|
+
yield* Effect.sleep("500 millis");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
var startContainer = (name, spec) => (ctx) => Effect.gen(function* () {
|
|
158
|
+
const fs = yield* FileSystem;
|
|
159
|
+
const path = yield* Path;
|
|
160
|
+
const sessions = yield* DevSessions;
|
|
161
|
+
const known = new Set(
|
|
162
|
+
(yield* sessions.getSessions).map((existing) => existing.name)
|
|
163
|
+
);
|
|
164
|
+
const containers = yield* listContainers(sessions.dir);
|
|
165
|
+
for (const id of selectOrphans(containers, known))
|
|
166
|
+
yield* docker(["rm", "-f", id]);
|
|
167
|
+
const container = `devsess-${ctx.session.name}-${name}`;
|
|
168
|
+
const portBindings = Object.entries(spec.ports ?? {}).map(
|
|
169
|
+
(entry) => ({
|
|
170
|
+
host: ctx.ports[entry[0]],
|
|
171
|
+
container: entry[1]
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
const volumes = [];
|
|
175
|
+
for (const entry of Object.entries(spec.volumes ?? {})) {
|
|
176
|
+
if (!validName(entry[0]))
|
|
177
|
+
return yield* new ServiceError({
|
|
178
|
+
message: `Invalid volume name: ${entry[0]}`
|
|
179
|
+
});
|
|
180
|
+
volumes.push({
|
|
181
|
+
source: path.join(ctx.dataDir, entry[0]),
|
|
182
|
+
target: entry[1]
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const start = Effect.gen(function* () {
|
|
186
|
+
const found = yield* docker(
|
|
187
|
+
[
|
|
188
|
+
"container",
|
|
189
|
+
"inspect",
|
|
190
|
+
"--format",
|
|
191
|
+
'{{.State.Running}} {{index .Config.Labels "devsess.root"}} {{index .Config.Labels "devsess.session"}}',
|
|
192
|
+
container
|
|
193
|
+
],
|
|
194
|
+
{ allowFailure: true }
|
|
195
|
+
);
|
|
196
|
+
const identity = found.output?.split(" ");
|
|
197
|
+
if (found.code === 0 && (identity?.[1] !== sessions.dir || identity[2] !== ctx.session.name))
|
|
198
|
+
return yield* new ServiceError({
|
|
199
|
+
message: `Container name ${container} belongs to another session or project`
|
|
200
|
+
});
|
|
201
|
+
if (found.code === 0 && identity?.[0] === "true") return;
|
|
202
|
+
if (found.code === 0) yield* docker(["rm", "-f", container]);
|
|
203
|
+
if (!process.getuid || !process.getgid)
|
|
204
|
+
return yield* new ServiceError({
|
|
205
|
+
message: "Docker services require a POSIX user and group ID"
|
|
206
|
+
});
|
|
207
|
+
for (const volume of volumes)
|
|
208
|
+
yield* fs.makeDirectory(volume.source, { recursive: true });
|
|
209
|
+
const image = yield* docker(
|
|
210
|
+
["image", "inspect", "--format", "{{.Id}}", spec.image],
|
|
211
|
+
{ allowFailure: true }
|
|
212
|
+
);
|
|
213
|
+
if (image.code !== 0) {
|
|
214
|
+
yield* Effect.logInfo(`[dev] pulling ${spec.image}`);
|
|
215
|
+
yield* docker(["pull", spec.image], { progress: true });
|
|
216
|
+
}
|
|
217
|
+
yield* docker(
|
|
218
|
+
buildRunArgs({
|
|
219
|
+
image: spec.image,
|
|
220
|
+
container,
|
|
221
|
+
root: sessions.dir,
|
|
222
|
+
session: ctx.session.name,
|
|
223
|
+
uid: process.getuid(),
|
|
224
|
+
gid: process.getgid(),
|
|
225
|
+
ports: portBindings,
|
|
226
|
+
volumes,
|
|
227
|
+
env: spec.env ?? {},
|
|
228
|
+
healthcheck: spec.healthcheck
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
yield* Effect.acquireRelease(
|
|
233
|
+
start,
|
|
234
|
+
() => docker(["rm", "-f", container]).pipe(
|
|
235
|
+
Effect.catch(
|
|
236
|
+
(error) => Effect.logError(`[dev] failed to remove ${container}: ${error}`)
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
);
|
|
240
|
+
if (spec.healthcheck) yield* waitHealthy(name, container);
|
|
241
|
+
return { container };
|
|
242
|
+
}).pipe(withServiceError(`Failed to start container for service ${name}`));
|
|
243
|
+
|
|
244
|
+
// src/services/shared/index.ts
|
|
245
|
+
import { Effect as Effect4, Schema } from "effect";
|
|
246
|
+
|
|
247
|
+
// src/services/shared/callsite.ts
|
|
248
|
+
import { resolve } from "path";
|
|
249
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
250
|
+
import { Effect as Effect2 } from "effect";
|
|
251
|
+
var packageDir = fileURLToPath(import.meta.url).match(
|
|
252
|
+
/^(.*)\/(?:src|dist)\//
|
|
253
|
+
)?.[1];
|
|
254
|
+
var definitionModule = (stack) => Effect2.gen(function* () {
|
|
255
|
+
if (packageDir === void 0)
|
|
256
|
+
return yield* new ServiceError({
|
|
257
|
+
message: "Unable to identify devsess internal stack frames"
|
|
258
|
+
});
|
|
259
|
+
if (stack !== void 0) {
|
|
260
|
+
for (const frame of stack.split("\n").slice(1)) {
|
|
261
|
+
const match = frame.match(
|
|
262
|
+
/(?:\(|\s|@)((?:file:\/\/|\/)[^\n]*?):\d+:\d+\)?$/
|
|
263
|
+
);
|
|
264
|
+
const location = match?.[1];
|
|
265
|
+
if (location === void 0) continue;
|
|
266
|
+
const file = yield* Effect2.try({
|
|
267
|
+
try: () => location.startsWith("file://") ? fileURLToPath(location) : resolve(location),
|
|
268
|
+
catch: (cause) => new ServiceError({
|
|
269
|
+
message: "Invalid service callsite path",
|
|
270
|
+
cause
|
|
271
|
+
})
|
|
272
|
+
});
|
|
273
|
+
if (file.startsWith(`${packageDir}/src/`) || file.startsWith(`${packageDir}/dist/`))
|
|
274
|
+
continue;
|
|
275
|
+
return yield* Effect2.try({
|
|
276
|
+
try: () => pathToFileURL(file).href,
|
|
277
|
+
catch: (cause) => new ServiceError({ message: "Invalid service module URL", cause })
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return yield* new ServiceError({
|
|
282
|
+
message: "Unable to locate the file that calls Service.make for a shared service"
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// src/services/shared/root.ts
|
|
287
|
+
import { Effect as Effect3 } from "effect";
|
|
288
|
+
import { FileSystem as FileSystem2 } from "effect/FileSystem";
|
|
289
|
+
import { Path as Path2 } from "effect/Path";
|
|
290
|
+
var findSharedRoot = (projectDir) => Effect3.gen(function* () {
|
|
291
|
+
const fs = yield* FileSystem2;
|
|
292
|
+
const path = yield* Path2;
|
|
293
|
+
const start = path.resolve(projectDir);
|
|
294
|
+
const walk = (dir, candidate) => Effect3.gen(function* () {
|
|
295
|
+
const topmost = yield* fs.exists(path.join(dir, "package.json"));
|
|
296
|
+
const next = topmost ? dir : candidate;
|
|
297
|
+
if (yield* fs.exists(path.join(dir, ".git"))) return next;
|
|
298
|
+
const parent = path.dirname(dir);
|
|
299
|
+
if (parent === dir) return start;
|
|
300
|
+
return yield* walk(parent, next);
|
|
301
|
+
});
|
|
302
|
+
return yield* walk(start, start);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// src/services/shared/index.ts
|
|
306
|
+
var locateHost = (session, name, stack) => Effect4.gen(function* () {
|
|
307
|
+
const module = yield* definitionModule(stack);
|
|
308
|
+
const root = yield* findSharedRoot(session.rootDir);
|
|
309
|
+
const sharedSession = yield* DevSessions.use(
|
|
310
|
+
(store) => store.getOrCreate("shared-services")
|
|
311
|
+
).pipe(Effect4.provide(DevSessions.layerAt(root)));
|
|
312
|
+
const paths = hostPaths(yield* sharedSession.path(""), name);
|
|
313
|
+
return { paths, args: { module, name, root, session: sharedSession.name } };
|
|
314
|
+
});
|
|
315
|
+
var runShared = (session, name, options, stack) => Effect4.gen(function* () {
|
|
316
|
+
if (!validName(name) || !validName(session.name))
|
|
317
|
+
return yield* new ServiceError({
|
|
318
|
+
message: "Invalid shared service or session name"
|
|
319
|
+
});
|
|
320
|
+
const host = yield* locateHost(session, name, stack);
|
|
321
|
+
const lease = yield* Effect4.acquireRelease(
|
|
322
|
+
acquireHost(host.paths, () => launchHost(host.paths, host.args)),
|
|
323
|
+
(value) => Effect4.sync(() => value.socket.destroy())
|
|
324
|
+
);
|
|
325
|
+
yield* Effect4.forkScoped(
|
|
326
|
+
failWhenHostExits(lease, name).pipe(
|
|
327
|
+
Effect4.catch((error) => Effect4.logError(error))
|
|
328
|
+
)
|
|
329
|
+
);
|
|
330
|
+
const ready = yield* Schema.decodeUnknownEffect(
|
|
331
|
+
readyMessage(options.output)
|
|
332
|
+
)(lease.line).pipe(withServiceError(`Invalid shared output from ${name}`));
|
|
333
|
+
yield* reportDaemonService(name, ready.ports);
|
|
334
|
+
return {
|
|
335
|
+
value: ready.output,
|
|
336
|
+
ports: ready.ports
|
|
337
|
+
};
|
|
338
|
+
}).pipe(withServiceError(`Failed to run shared service ${name}`));
|
|
339
|
+
|
|
340
|
+
// src/services/index.ts
|
|
341
|
+
var run = (session, def) => (def.shared !== void 0 ? runShared(session, def.name, def.shared, def.definitionStack) : runLocal(session, def)).pipe(Effect5.map(runningService));
|
|
342
|
+
var make = (def) => {
|
|
343
|
+
const base = {
|
|
344
|
+
[serviceBrand]: true,
|
|
345
|
+
name: def.name,
|
|
346
|
+
ports: def.ports ?? [],
|
|
347
|
+
start: def.start,
|
|
348
|
+
shared: def.shared,
|
|
349
|
+
definitionStack: def.shared === void 0 ? void 0 : new Error().stack
|
|
350
|
+
};
|
|
351
|
+
const key = Context.Service(
|
|
352
|
+
`devsess/services/${def.name}`
|
|
353
|
+
);
|
|
354
|
+
const layer = Layer.effect(
|
|
355
|
+
key,
|
|
356
|
+
Effect5.gen(function* () {
|
|
357
|
+
const session = yield* CurrentSession;
|
|
358
|
+
return yield* run(session, base);
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
return { ...base, key, layer };
|
|
362
|
+
};
|
|
363
|
+
var Service = {
|
|
364
|
+
/** Defines a service whose `start` handler you write yourself. */
|
|
365
|
+
make,
|
|
366
|
+
/** Defines a service backed by a Docker container, ready once its healthcheck passes. */
|
|
367
|
+
container: (def) => make({
|
|
368
|
+
name: def.name,
|
|
369
|
+
ports: Object.keys(def.ports ?? {}),
|
|
370
|
+
start: startContainer(def.name, def)
|
|
371
|
+
}),
|
|
372
|
+
/** Starts a service in `session`, outside a layer. */
|
|
373
|
+
run
|
|
374
|
+
};
|
|
375
|
+
export {
|
|
376
|
+
Service,
|
|
377
|
+
ServiceError
|
|
378
|
+
};
|
|
379
|
+
//# sourceMappingURL=index.js.map
|