ignotum 0.0.0 → 0.0.2

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.
Files changed (57) hide show
  1. package/README.md +161 -0
  2. package/dist/cli/bin.d.mts +1 -0
  3. package/dist/cli/bin.mjs +1696 -0
  4. package/dist/cli/bin.mjs.map +1 -0
  5. package/dist/runtime/api-BXXIZc_Q.js +119 -0
  6. package/dist/runtime/api-BXXIZc_Q.js.map +1 -0
  7. package/dist/runtime/api-Cpx57mk3.d.ts +47 -0
  8. package/dist/runtime/client/jsx-dev-runtime.d.ts +2 -0
  9. package/dist/runtime/client/jsx-dev-runtime.js +2 -0
  10. package/dist/runtime/client/jsx-runtime.d.ts +2 -0
  11. package/dist/runtime/client/jsx-runtime.js +2 -0
  12. package/dist/runtime/client.d.ts +29 -0
  13. package/dist/runtime/client.js +364 -0
  14. package/dist/runtime/client.js.map +1 -0
  15. package/dist/runtime/index-BgWROoyk.d.ts +249 -0
  16. package/dist/runtime/internal/api.d.ts +2 -0
  17. package/dist/runtime/internal/api.js +2 -0
  18. package/dist/runtime/internal/server.d.ts +6 -0
  19. package/dist/runtime/internal/server.js +7 -0
  20. package/dist/runtime/internal/server.js.map +1 -0
  21. package/dist/runtime/internal/types.d.ts +2 -0
  22. package/dist/runtime/internal/types.js +2 -0
  23. package/dist/runtime/result-B2W-z2wG.js +136 -0
  24. package/dist/runtime/result-B2W-z2wG.js.map +1 -0
  25. package/dist/runtime/result-C1ZdsM6Y.d.ts +106 -0
  26. package/dist/runtime/schema-CNEVLF7D.js +116 -0
  27. package/dist/runtime/schema-CNEVLF7D.js.map +1 -0
  28. package/dist/runtime/server.d.ts +9 -0
  29. package/dist/runtime/server.js +9 -0
  30. package/dist/runtime/server.js.map +1 -0
  31. package/package.json +81 -2
  32. package/src/cli/agent-files.ts +35 -0
  33. package/src/cli/bin.ts +5 -0
  34. package/src/cli/client-plugin.ts +66 -0
  35. package/src/cli/codegen.ts +203 -0
  36. package/src/cli/command.ts +155 -0
  37. package/src/cli/dev.ts +206 -0
  38. package/src/cli/new-project.ts +377 -0
  39. package/src/cli/package-manager.ts +55 -0
  40. package/src/client/errors.ts +83 -0
  41. package/src/client/hooks.ts +141 -0
  42. package/src/client/index.ts +90 -0
  43. package/src/client/jsx-dev-runtime.ts +2 -0
  44. package/src/client/jsx-runtime.ts +2 -0
  45. package/src/client/query.ts +17 -0
  46. package/src/client/sync.ts +487 -0
  47. package/src/dev-runtime/database.ts +374 -0
  48. package/src/dev-runtime/dev-database.ts +199 -0
  49. package/src/dev-runtime/functions.ts +293 -0
  50. package/src/dev-runtime/id.ts +11 -0
  51. package/src/dev-runtime/sync.ts +473 -0
  52. package/src/internal/api.ts +141 -0
  53. package/src/internal/http-paths.ts +5 -0
  54. package/src/internal/server.ts +3 -0
  55. package/src/internal/types.ts +2 -0
  56. package/src/raw.d.ts +4 -0
  57. package/src/server/index.ts +8 -0
@@ -0,0 +1,374 @@
1
+ import { Context, DateTime, Effect, Layer, Option, Schema } from "effect";
2
+ import { SqlClient, SqlError, SqlSchema } from "effect/unstable/sql";
3
+ import { GeneratedId, type GeneratedId as GeneratedIdType } from "@ignotum/contracts/runtime/id";
4
+ import {
5
+ documentNotFound,
6
+ resultFromEffect,
7
+ type DocumentNotFoundValue,
8
+ type Result,
9
+ } from "@ignotum/contracts/runtime/result";
10
+ import type {
11
+ RuntimeSchemaDefinition,
12
+ RuntimeTableDefinition,
13
+ } from "@ignotum/contracts/runtime/schema";
14
+ import { SchemaDefinitionTypeId } from "@ignotum/contracts/schema";
15
+ import { IdGenerator } from "@ignotum/runtime/id";
16
+
17
+ interface RuntimeObject {
18
+ readonly [fieldName: string]: RuntimeDocumentValue | undefined;
19
+ }
20
+ type RuntimeDocumentValue =
21
+ | null
22
+ | boolean
23
+ | number
24
+ | string
25
+ | Date
26
+ | ReadonlyArray<RuntimeDocumentValue>
27
+ | RuntimeObject;
28
+ type RuntimeValue = RuntimeObject;
29
+
30
+ export interface RuntimeDocument {
31
+ readonly [fieldName: string]: RuntimeDocumentValue | undefined;
32
+ readonly id: GeneratedIdType;
33
+ readonly createdAt: Date;
34
+ readonly updatedAt: Date;
35
+ }
36
+
37
+ export interface LocalDatabaseReader {
38
+ readonly find: (tableName: string, id: string) => Result<RuntimeDocument | undefined, never>;
39
+ readonly get: (tableName: string, id: string) => Result<RuntimeDocument, DocumentNotFoundValue>;
40
+ readonly query: (tableName: string) => {
41
+ readonly collect: () => Result<ReadonlyArray<RuntimeDocument>, never>;
42
+ };
43
+ }
44
+
45
+ export interface LocalDatabaseWriter extends LocalDatabaseReader {
46
+ readonly delete: (tableName: string, id: string) => Result<void, never>;
47
+ readonly insert: (tableName: string, value: RuntimeValue) => Result<GeneratedIdType, never>;
48
+ readonly patch: (tableName: string, id: string, value: RuntimeValue) => Result<void, never>;
49
+ readonly replace: (tableName: string, id: string, value: RuntimeValue) => Result<void, never>;
50
+ }
51
+
52
+ export interface LocalQueryContext {
53
+ readonly db: LocalDatabaseReader;
54
+ }
55
+
56
+ export interface LocalMutationContext {
57
+ readonly db: LocalDatabaseWriter;
58
+ }
59
+
60
+ interface LocalDatabaseService {
61
+ readonly queryTransaction: <Success, Failure>(
62
+ schema: RuntimeSchemaDefinition,
63
+ use: (context: Readonly<LocalQueryContext>) => Effect.Effect<Success, Failure>,
64
+ ) => Effect.Effect<Success, Failure | SqlError.SqlError>;
65
+ readonly mutationTransaction: <Success, Failure>(
66
+ schema: RuntimeSchemaDefinition,
67
+ use: (context: Readonly<LocalMutationContext>) => Effect.Effect<Success, Failure>,
68
+ ) => Effect.Effect<Success, Failure | SqlError.SqlError>;
69
+ }
70
+
71
+ const StoredDocument = Schema.Struct({
72
+ id: GeneratedId,
73
+ tableName: Schema.String,
74
+ createdAt: Schema.Int,
75
+ updatedAt: Schema.Int,
76
+ fields: Schema.String,
77
+ });
78
+ type StoredDocument = typeof StoredDocument.Type;
79
+
80
+ const DocumentLookup = Schema.Struct({
81
+ id: Schema.String,
82
+ tableName: Schema.String,
83
+ });
84
+ const TableLookup = Schema.Struct({ tableName: Schema.String });
85
+ type DatabaseOperation = "collect" | "get" | "insert" | "patch" | "replace";
86
+
87
+ export class UnknownDatabaseTable extends Schema.TaggedError<UnknownDatabaseTable>()(
88
+ "UnknownDatabaseTable",
89
+ {
90
+ message: Schema.String,
91
+ tableName: Schema.String,
92
+ },
93
+ ) {}
94
+
95
+ export class DocumentSchemaMismatch extends Schema.TaggedError<DocumentSchemaMismatch>()(
96
+ "DocumentSchemaMismatch",
97
+ {
98
+ cause: Schema.Defect(),
99
+ id: Schema.NullOr(Schema.String),
100
+ message: Schema.String,
101
+ operation: Schema.Literals(["collect", "get", "insert", "patch", "replace"]),
102
+ tableName: Schema.String,
103
+ },
104
+ ) {}
105
+
106
+ const schemaMismatch = (
107
+ tableName: string,
108
+ id: string | null,
109
+ operation: DatabaseOperation,
110
+ cause: unknown,
111
+ ) =>
112
+ DocumentSchemaMismatch.make({
113
+ cause,
114
+ id,
115
+ message: `Document fields for ${tableName} did not match its schema during ${operation}.`,
116
+ operation,
117
+ tableName,
118
+ });
119
+
120
+ const tableFor = Effect.fn("LocalDatabase.tableFor")(function* (
121
+ schema: RuntimeSchemaDefinition,
122
+ tableName: string,
123
+ ) {
124
+ const table = schema[SchemaDefinitionTypeId][tableName];
125
+ if (table === undefined) {
126
+ return yield* UnknownDatabaseTable.make({
127
+ message: `Unknown database table ${tableName}.`,
128
+ tableName,
129
+ });
130
+ }
131
+ return table;
132
+ });
133
+
134
+ const fieldsCodec = (table: RuntimeTableDefinition) =>
135
+ // SAFETY: defineSchema creates every table schema with Schema.Struct, so its
136
+ // decoded value is a string-keyed object and the authoring API needs no services.
137
+ Schema.fromJsonString(
138
+ Schema.make<Schema.Codec<RuntimeValue, Schema.Json>>(Schema.toCodecJson(table.schema).ast),
139
+ );
140
+
141
+ const encodeFields = Effect.fn("LocalDatabase.encodeFields")(function* (
142
+ schema: RuntimeSchemaDefinition,
143
+ tableName: string,
144
+ value: RuntimeValue,
145
+ operation: Extract<DatabaseOperation, "insert" | "patch" | "replace">,
146
+ id: string | null,
147
+ ) {
148
+ const table = yield* tableFor(schema, tableName);
149
+ return yield* Schema.encodeEffect(fieldsCodec(table), {
150
+ onExcessProperty: "error",
151
+ })(value).pipe(Effect.mapError((cause) => schemaMismatch(tableName, id, operation, cause)));
152
+ });
153
+
154
+ const decodeFields = Effect.fn("LocalDatabase.decodeFields")(function* (
155
+ schema: RuntimeSchemaDefinition,
156
+ row: StoredDocument,
157
+ operation: Extract<DatabaseOperation, "collect" | "get" | "patch">,
158
+ ) {
159
+ const table = yield* tableFor(schema, row.tableName);
160
+ return yield* Schema.decodeEffect(fieldsCodec(table), {
161
+ onExcessProperty: "error",
162
+ })(row.fields).pipe(
163
+ Effect.mapError((cause) => schemaMismatch(row.tableName, row.id, operation, cause)),
164
+ );
165
+ });
166
+
167
+ const toRuntimeDocument = Effect.fn("LocalDatabase.toRuntimeDocument")(function* (
168
+ schema: RuntimeSchemaDefinition,
169
+ row: StoredDocument,
170
+ operation: Extract<DatabaseOperation, "collect" | "get">,
171
+ ) {
172
+ const fields = yield* decodeFields(schema, row, operation);
173
+ return {
174
+ ...fields,
175
+ id: row.id,
176
+ createdAt: DateTime.toDate(DateTime.makeUnsafe(row.createdAt)),
177
+ updatedAt: DateTime.toDate(DateTime.makeUnsafe(row.updatedAt)),
178
+ } satisfies RuntimeDocument;
179
+ });
180
+
181
+ const nextUpdatedAt = (now: DateTime.DateTime, previous: number): number =>
182
+ Math.max(DateTime.toEpochMillis(now), previous + 1);
183
+
184
+ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseService>()(
185
+ "ignotum/dev-runtime/database/LocalDatabase",
186
+ ) {
187
+ static readonly layer = Layer.effect(
188
+ LocalDatabase,
189
+ Effect.gen(function* () {
190
+ const ids = yield* IdGenerator;
191
+ const sql = yield* SqlClient.SqlClient;
192
+
193
+ yield* sql`
194
+ CREATE TABLE IF NOT EXISTS documents (
195
+ id TEXT PRIMARY KEY NOT NULL
196
+ CHECK (length(id) = 24 AND id NOT GLOB '*[^0-9a-z]*'),
197
+ tableName TEXT NOT NULL,
198
+ createdAt INTEGER NOT NULL,
199
+ updatedAt INTEGER NOT NULL,
200
+ fields TEXT NOT NULL CHECK (json_valid(fields))
201
+ ) STRICT
202
+ `;
203
+ yield* sql`
204
+ CREATE INDEX IF NOT EXISTS documents_table_name_idx
205
+ ON documents (tableName)
206
+ `;
207
+
208
+ const findStored = SqlSchema.findOneOption({
209
+ Request: DocumentLookup,
210
+ Result: StoredDocument,
211
+ execute: ({ id, tableName }) => sql`
212
+ SELECT id, tableName, createdAt, updatedAt, fields
213
+ FROM documents
214
+ WHERE id = ${id} AND tableName = ${tableName}
215
+ LIMIT 1
216
+ `,
217
+ });
218
+ const collectStored = SqlSchema.findAll({
219
+ Request: TableLookup,
220
+ Result: StoredDocument,
221
+ execute: ({ tableName }) => sql`
222
+ SELECT id, tableName, createdAt, updatedAt, fields
223
+ FROM documents
224
+ WHERE tableName = ${tableName}
225
+ ORDER BY createdAt ASC, id ASC
226
+ `,
227
+ });
228
+
229
+ const find = Effect.fn("LocalDatabase.find")(function* (
230
+ schema: RuntimeSchemaDefinition,
231
+ tableName: string,
232
+ id: string,
233
+ ) {
234
+ yield* tableFor(schema, tableName);
235
+ const row = yield* findStored({ id, tableName });
236
+ if (Option.isNone(row)) return undefined;
237
+ return yield* toRuntimeDocument(schema, row.value, "get");
238
+ });
239
+
240
+ const collect = Effect.fn("LocalDatabase.collect")(function* (
241
+ schema: RuntimeSchemaDefinition,
242
+ tableName: string,
243
+ ) {
244
+ yield* tableFor(schema, tableName);
245
+ const rows = yield* collectStored({ tableName });
246
+ return yield* Effect.forEach(rows, (row) => toRuntimeDocument(schema, row, "collect"));
247
+ });
248
+
249
+ const insert = Effect.fn("LocalDatabase.insert")(function* (
250
+ schema: RuntimeSchemaDefinition,
251
+ tableName: string,
252
+ value: RuntimeValue,
253
+ ) {
254
+ const fields = yield* encodeFields(schema, tableName, value, "insert", null);
255
+ const id = yield* ids.generate;
256
+ const now = DateTime.toEpochMillis(yield* DateTime.now);
257
+ yield* sql`
258
+ INSERT INTO documents (id, tableName, createdAt, updatedAt, fields)
259
+ VALUES (${id}, ${tableName}, ${now}, ${now}, ${fields})
260
+ `;
261
+ return id;
262
+ });
263
+
264
+ const deleteDocument = Effect.fn("LocalDatabase.delete")(function* (
265
+ schema: RuntimeSchemaDefinition,
266
+ tableName: string,
267
+ id: string,
268
+ ) {
269
+ yield* tableFor(schema, tableName);
270
+ yield* sql`DELETE FROM documents WHERE id = ${id} AND tableName = ${tableName}`;
271
+ });
272
+
273
+ const patch = Effect.fn("LocalDatabase.patch")(function* (
274
+ schema: RuntimeSchemaDefinition,
275
+ tableName: string,
276
+ id: string,
277
+ value: RuntimeValue,
278
+ ) {
279
+ yield* tableFor(schema, tableName);
280
+ const stored = yield* findStored({ id, tableName });
281
+ if (Option.isNone(stored)) return;
282
+ const current = yield* decodeFields(schema, stored.value, "patch");
283
+ const fields = yield* encodeFields(
284
+ schema,
285
+ tableName,
286
+ { ...current, ...value },
287
+ "patch",
288
+ id,
289
+ );
290
+ const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
291
+ yield* sql`
292
+ UPDATE documents
293
+ SET fields = ${fields}, updatedAt = ${updatedAt}
294
+ WHERE id = ${id} AND tableName = ${tableName}
295
+ `;
296
+ });
297
+
298
+ const replace = Effect.fn("LocalDatabase.replace")(function* (
299
+ schema: RuntimeSchemaDefinition,
300
+ tableName: string,
301
+ id: string,
302
+ value: RuntimeValue,
303
+ ) {
304
+ yield* tableFor(schema, tableName);
305
+ const stored = yield* findStored({ id, tableName });
306
+ if (Option.isNone(stored)) return;
307
+ const fields = yield* encodeFields(schema, tableName, value, "replace", id);
308
+ const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
309
+ yield* sql`
310
+ UPDATE documents
311
+ SET fields = ${fields}, updatedAt = ${updatedAt}
312
+ WHERE id = ${id} AND tableName = ${tableName}
313
+ `;
314
+ });
315
+
316
+ const makeReader = (schema: RuntimeSchemaDefinition): LocalDatabaseReader =>
317
+ Object.freeze({
318
+ find: (tableName: string, id: string) =>
319
+ resultFromEffect(find(schema, tableName, id).pipe(Effect.orDie)),
320
+ get: (tableName: string, id: string) =>
321
+ resultFromEffect(
322
+ find(schema, tableName, id).pipe(
323
+ Effect.orDie,
324
+ Effect.flatMap((document) =>
325
+ document === undefined
326
+ ? Effect.fail(documentNotFound(tableName, id))
327
+ : Effect.succeed(document),
328
+ ),
329
+ ),
330
+ ),
331
+ query: (tableName: string) =>
332
+ Object.freeze({
333
+ collect: () => resultFromEffect(collect(schema, tableName).pipe(Effect.orDie)),
334
+ }),
335
+ });
336
+
337
+ const makeWriter = (schema: RuntimeSchemaDefinition): LocalDatabaseWriter => {
338
+ const reader = makeReader(schema);
339
+ return Object.freeze({
340
+ ...reader,
341
+ delete: (tableName: string, id: string) =>
342
+ resultFromEffect(deleteDocument(schema, tableName, id).pipe(Effect.orDie)),
343
+ insert: (tableName: string, value: RuntimeValue) =>
344
+ resultFromEffect(insert(schema, tableName, value).pipe(Effect.orDie)),
345
+ patch: (tableName: string, id: string, value: RuntimeValue) =>
346
+ resultFromEffect(patch(schema, tableName, id, value).pipe(Effect.orDie)),
347
+ replace: (tableName: string, id: string, value: RuntimeValue) =>
348
+ resultFromEffect(replace(schema, tableName, id, value).pipe(Effect.orDie)),
349
+ });
350
+ };
351
+
352
+ return LocalDatabase.of({
353
+ queryTransaction: (schema, use) =>
354
+ sql.withTransaction(
355
+ Effect.gen(function* () {
356
+ const context = Object.freeze({
357
+ db: makeReader(schema),
358
+ });
359
+ return yield* use(context);
360
+ }),
361
+ ),
362
+ mutationTransaction: (schema, use) =>
363
+ sql.withTransaction(
364
+ Effect.gen(function* () {
365
+ const context = Object.freeze({
366
+ db: makeWriter(schema),
367
+ });
368
+ return yield* use(context);
369
+ }),
370
+ ),
371
+ });
372
+ }),
373
+ );
374
+ }
@@ -0,0 +1,199 @@
1
+ import { Crypto, Effect, FileSystem, Option, Path, Predicate, Schema } from "effect";
2
+
3
+ const databaseFileNames = ["state.db", "state.db-shm", "state.db-wal", "state.db-journal"] as const;
4
+ const lockFileName = "lock.json";
5
+
6
+ const DevDatabaseLockRecord = Schema.Struct({
7
+ pid: Schema.Int,
8
+ token: Schema.String,
9
+ });
10
+ const DevDatabaseLockRecordJson = Schema.fromJsonString(DevDatabaseLockRecord);
11
+ type DevDatabaseLockRecord = typeof DevDatabaseLockRecord.Type;
12
+
13
+ export class DevDatabaseResetFailed extends Schema.TaggedError<DevDatabaseResetFailed>()(
14
+ "DevDatabaseResetFailed",
15
+ {
16
+ cause: Schema.Defect(),
17
+ message: Schema.String,
18
+ path: Schema.String,
19
+ },
20
+ ) {}
21
+
22
+ export class DevDatabaseInUse extends Schema.TaggedError<DevDatabaseInUse>()("DevDatabaseInUse", {
23
+ message: Schema.String,
24
+ path: Schema.String,
25
+ pid: Schema.Int,
26
+ }) {}
27
+
28
+ export class DevDatabaseLockFailed extends Schema.TaggedError<DevDatabaseLockFailed>()(
29
+ "DevDatabaseLockFailed",
30
+ {
31
+ cause: Schema.Defect(),
32
+ message: Schema.String,
33
+ path: Schema.String,
34
+ },
35
+ ) {}
36
+
37
+ export const devDatabasePath = Effect.fn("DevDatabase.path")(function* (projectDirectory: string) {
38
+ const path = yield* Path.Path;
39
+ return path.join(projectDirectory, ".ignotum", "dev", "state.db");
40
+ });
41
+
42
+ const processIsRunning = (pid: number): boolean => {
43
+ try {
44
+ process.kill(pid, 0);
45
+ return true;
46
+ } catch (cause) {
47
+ return !(Predicate.hasProperty(cause, "code") && cause.code === "ESRCH");
48
+ }
49
+ };
50
+
51
+ export const acquireDevDatabaseLock = Effect.fn("DevDatabase.acquireLock")(function* (
52
+ projectDirectory: string,
53
+ ) {
54
+ const crypto = yield* Crypto.Crypto;
55
+ const fileSystem = yield* FileSystem.FileSystem;
56
+ const path = yield* Path.Path;
57
+ const databasePath = yield* devDatabasePath(projectDirectory);
58
+ const directory = path.dirname(databasePath);
59
+ const lockPath = path.join(directory, lockFileName);
60
+ const record: DevDatabaseLockRecord = {
61
+ pid: process.pid,
62
+ token: yield* crypto.randomUUIDv4,
63
+ };
64
+ const encoded = yield* Schema.encodeEffect(DevDatabaseLockRecordJson)(record).pipe(
65
+ Effect.mapError((cause) =>
66
+ DevDatabaseLockFailed.make({
67
+ cause,
68
+ message: `Could not encode the development database lock at ${lockPath}.`,
69
+ path: lockPath,
70
+ }),
71
+ ),
72
+ );
73
+
74
+ yield* fileSystem.makeDirectory(directory, { recursive: true }).pipe(
75
+ Effect.mapError((cause) =>
76
+ DevDatabaseLockFailed.make({
77
+ cause,
78
+ message: `Could not create the development state directory at ${directory}.`,
79
+ path: lockPath,
80
+ }),
81
+ ),
82
+ );
83
+
84
+ const createLock = () =>
85
+ fileSystem.writeFileString(lockPath, encoded, { flag: "wx" }).pipe(
86
+ Effect.matchEffect({
87
+ onFailure: (cause) =>
88
+ cause.reason._tag === "AlreadyExists"
89
+ ? Effect.succeed(false)
90
+ : DevDatabaseLockFailed.make({
91
+ cause,
92
+ message: `Could not create the development database lock at ${lockPath}.`,
93
+ path: lockPath,
94
+ }),
95
+ onSuccess: () => Effect.succeed(true),
96
+ }),
97
+ );
98
+
99
+ const readLock = Effect.fn("DevDatabase.readLock")(function* () {
100
+ const content = yield* fileSystem.readFileString(lockPath).pipe(
101
+ Effect.mapError((cause) =>
102
+ DevDatabaseLockFailed.make({
103
+ cause,
104
+ message: `Could not read the development database lock at ${lockPath}.`,
105
+ path: lockPath,
106
+ }),
107
+ ),
108
+ );
109
+ return yield* Schema.decodeEffect(DevDatabaseLockRecordJson)(content).pipe(
110
+ Effect.mapError((cause) =>
111
+ DevDatabaseLockFailed.make({
112
+ cause,
113
+ message: `The development database lock at ${lockPath} is invalid. Remove it and try again.`,
114
+ path: lockPath,
115
+ }),
116
+ ),
117
+ );
118
+ });
119
+
120
+ const failInUse = (current: DevDatabaseLockRecord) =>
121
+ DevDatabaseInUse.make({
122
+ message: `Another Ignotum development server is using ${databasePath} with process ${current.pid}.`,
123
+ path: lockPath,
124
+ pid: current.pid,
125
+ });
126
+
127
+ const acquire = Effect.gen(function* () {
128
+ if (yield* createLock()) return record;
129
+
130
+ const current = yield* readLock();
131
+ if (processIsRunning(current.pid)) return yield* failInUse(current);
132
+
133
+ yield* fileSystem.remove(lockPath).pipe(
134
+ Effect.mapError((cause) =>
135
+ DevDatabaseLockFailed.make({
136
+ cause,
137
+ message: `Could not remove the stale development database lock at ${lockPath}.`,
138
+ path: lockPath,
139
+ }),
140
+ ),
141
+ );
142
+
143
+ if (yield* createLock()) return record;
144
+ return yield* failInUse(yield* readLock());
145
+ });
146
+
147
+ const release = (owned: DevDatabaseLockRecord) =>
148
+ Effect.gen(function* () {
149
+ if (!(yield* fileSystem.exists(lockPath))) return;
150
+ const content = yield* fileSystem.readFileString(lockPath);
151
+ const current = Schema.decodeOption(DevDatabaseLockRecordJson)(content);
152
+ if (Option.isNone(current) || current.value.token !== owned.token) return;
153
+ yield* fileSystem.remove(lockPath);
154
+ }).pipe(
155
+ Effect.catchCause((cause) =>
156
+ Effect.logWarning(`Could not release the development database lock at ${lockPath}.`).pipe(
157
+ Effect.annotateLogs({ cause }),
158
+ ),
159
+ ),
160
+ );
161
+
162
+ yield* Effect.acquireRelease(acquire, release);
163
+ });
164
+
165
+ export const resetDevDatabase = Effect.fn("DevDatabase.reset")(function* (
166
+ projectDirectory: string,
167
+ ) {
168
+ return yield* Effect.scoped(
169
+ Effect.gen(function* () {
170
+ yield* acquireDevDatabaseLock(projectDirectory);
171
+ const fileSystem = yield* FileSystem.FileSystem;
172
+ const path = yield* Path.Path;
173
+ const directory = path.join(projectDirectory, ".ignotum", "dev");
174
+ const databasePath = yield* devDatabasePath(projectDirectory);
175
+ let removed = 0;
176
+
177
+ yield* Effect.forEach(
178
+ databaseFileNames,
179
+ Effect.fn("DevDatabase.removeFile")(function* (fileName) {
180
+ const filePath = path.join(directory, fileName);
181
+ if (!(yield* fileSystem.exists(filePath))) return;
182
+ yield* fileSystem.remove(filePath);
183
+ removed += 1;
184
+ }),
185
+ { discard: true },
186
+ ).pipe(
187
+ Effect.mapError((cause) =>
188
+ DevDatabaseResetFailed.make({
189
+ cause,
190
+ message: `Could not reset ${databasePath}. Stop the Ignotum dev server and try again.`,
191
+ path: databasePath,
192
+ }),
193
+ ),
194
+ );
195
+
196
+ return { databasePath, removed } as const;
197
+ }),
198
+ );
199
+ });