ignotum 0.0.2 → 0.0.4

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 (64) hide show
  1. package/README.md +23 -7
  2. package/dist/cli/bin.mjs +32313 -751
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/runtime/{api-Cpx57mk3.d.ts → api-D2bsAz4T.d.ts} +3 -2
  5. package/dist/runtime/{api-BXXIZc_Q.js → api-DtX8qPrq.js} +58 -27
  6. package/dist/runtime/api-DtX8qPrq.js.map +1 -0
  7. package/dist/runtime/client.d.ts +11 -4
  8. package/dist/runtime/client.js +343 -79
  9. package/dist/runtime/client.js.map +1 -1
  10. package/dist/runtime/{result-B2W-z2wG.js → descriptor-t6BOEGw9-BTHR-ZMv.js} +119 -4
  11. package/dist/runtime/descriptor-t6BOEGw9-BTHR-ZMv.js.map +1 -0
  12. package/dist/runtime/id-Btwac71X-B9OeBzvq.d.ts +9 -0
  13. package/dist/runtime/id-D570vudg.js +26 -0
  14. package/dist/runtime/id-D570vudg.js.map +1 -0
  15. package/dist/runtime/{index-BgWROoyk.d.ts → index-Dl_VQGmA.d.ts} +114 -31
  16. package/dist/runtime/internal/api.d.ts +1 -1
  17. package/dist/runtime/internal/api.js +1 -1
  18. package/dist/runtime/internal/host.d.ts +44 -0
  19. package/dist/runtime/internal/host.js +213 -0
  20. package/dist/runtime/internal/host.js.map +1 -0
  21. package/dist/runtime/internal/server.d.ts +1 -1
  22. package/dist/runtime/internal/server.js +1 -1
  23. package/dist/runtime/internal/types.d.ts +1 -1
  24. package/dist/runtime/internal/types.js +1 -1
  25. package/dist/runtime/{result-C1ZdsM6Y.d.ts → result-BgcHn25t.d.ts} +21 -6
  26. package/dist/runtime/schema-B-jMZEbs.js +227 -0
  27. package/dist/runtime/schema-B-jMZEbs.js.map +1 -0
  28. package/dist/runtime/server.d.ts +2 -2
  29. package/dist/runtime/server.js +2 -2
  30. package/package.json +15 -5
  31. package/src/cli/agent-files.ts +6 -2
  32. package/src/cli/app-configuration.ts +135 -0
  33. package/src/cli/bin.ts +24 -1
  34. package/src/cli/build/artifact.ts +95 -0
  35. package/src/cli/build/client.ts +103 -0
  36. package/src/cli/build/server.ts +526 -0
  37. package/src/cli/client-config.ts +141 -0
  38. package/src/cli/client-entry.ts +152 -0
  39. package/src/cli/client-plugin.ts +86 -16
  40. package/src/cli/codegen.ts +5 -5
  41. package/src/cli/command.ts +41 -9
  42. package/src/cli/control-client.ts +343 -0
  43. package/src/cli/deploy.ts +217 -0
  44. package/src/cli/dev.ts +43 -108
  45. package/src/cli/module-boundaries.ts +183 -0
  46. package/src/cli/{new-project.ts → new-app.ts} +86 -86
  47. package/src/cli/package-manager.ts +9 -9
  48. package/src/client/app.ts +15 -0
  49. package/src/client/errors.ts +1 -17
  50. package/src/client/index.ts +2 -0
  51. package/src/client/sync.ts +172 -120
  52. package/src/dev-runtime/database.ts +253 -96
  53. package/src/dev-runtime/dev-database.ts +14 -14
  54. package/src/dev-runtime/functions.ts +57 -88
  55. package/src/dev-runtime/id.ts +2 -10
  56. package/src/dev-runtime/migrations.ts +68 -0
  57. package/src/dev-runtime/sync.ts +266 -83
  58. package/src/internal/function-definition.ts +77 -0
  59. package/src/internal/host.ts +1 -0
  60. package/src/internal/http-paths.ts +3 -1
  61. package/dist/runtime/api-BXXIZc_Q.js.map +0 -1
  62. package/dist/runtime/result-B2W-z2wG.js.map +0 -1
  63. package/dist/runtime/schema-CNEVLF7D.js +0 -116
  64. package/dist/runtime/schema-CNEVLF7D.js.map +0 -1
@@ -1,6 +1,18 @@
1
1
  import { Context, DateTime, Effect, Layer, Option, Schema } from "effect";
2
2
  import { SqlClient, SqlError, SqlSchema } from "effect/unstable/sql";
3
- import { GeneratedId, type GeneratedId as GeneratedIdType } from "@ignotum/contracts/runtime/id";
3
+ import {
4
+ GeneratedId,
5
+ TableId,
6
+ type GeneratedId as GeneratedIdType,
7
+ } from "@ignotum/contracts/runtime/id";
8
+ import { IdGenerator } from "@ignotum/shared/id";
9
+ import {
10
+ AppStateRevision,
11
+ dependencyKey,
12
+ documentDependency,
13
+ tableDependency,
14
+ type DependencyKey,
15
+ } from "@ignotum/contracts/runtime/hosted";
4
16
  import {
5
17
  documentNotFound,
6
18
  resultFromEffect,
@@ -12,7 +24,8 @@ import type {
12
24
  RuntimeTableDefinition,
13
25
  } from "@ignotum/contracts/runtime/schema";
14
26
  import { SchemaDefinitionTypeId } from "@ignotum/contracts/schema";
15
- import { IdGenerator } from "@ignotum/runtime/id";
27
+
28
+ import { DevelopmentDatabase } from "./migrations.js";
16
29
 
17
30
  interface RuntimeObject {
18
31
  readonly [fieldName: string]: RuntimeDocumentValue | undefined;
@@ -58,32 +71,78 @@ export interface LocalMutationContext {
58
71
  }
59
72
 
60
73
  interface LocalDatabaseService {
74
+ readonly revision: Effect.Effect<AppStateRevision>;
61
75
  readonly queryTransaction: <Success, Failure>(
62
76
  schema: RuntimeSchemaDefinition,
63
77
  use: (context: Readonly<LocalQueryContext>) => Effect.Effect<Success, Failure>,
64
78
  ) => Effect.Effect<Success, Failure | SqlError.SqlError>;
79
+ readonly trackedQueryTransaction: <Success, Failure>(
80
+ schema: RuntimeSchemaDefinition,
81
+ use: (context: Readonly<LocalQueryContext>) => Effect.Effect<Success, Failure>,
82
+ ) => Effect.Effect<
83
+ {
84
+ readonly value: Success;
85
+ readonly dependencies: ReadonlyArray<DependencyKey>;
86
+ readonly observedRevision: AppStateRevision;
87
+ },
88
+ Failure | SqlError.SqlError
89
+ >;
65
90
  readonly mutationTransaction: <Success, Failure>(
66
91
  schema: RuntimeSchemaDefinition,
67
92
  use: (context: Readonly<LocalMutationContext>) => Effect.Effect<Success, Failure>,
68
93
  ) => Effect.Effect<Success, Failure | SqlError.SqlError>;
94
+ readonly trackedMutationTransaction: <Success, Failure>(
95
+ schema: RuntimeSchemaDefinition,
96
+ use: (context: Readonly<LocalMutationContext>) => Effect.Effect<Success, Failure>,
97
+ ) => Effect.Effect<
98
+ {
99
+ readonly value: Success;
100
+ readonly invalidations: ReadonlyArray<DependencyKey>;
101
+ readonly committedRevision: AppStateRevision;
102
+ },
103
+ Failure | SqlError.SqlError
104
+ >;
69
105
  }
70
106
 
71
107
  const StoredDocument = Schema.Struct({
72
108
  id: GeneratedId,
73
- tableName: Schema.String,
74
109
  createdAt: Schema.Int,
75
110
  updatedAt: Schema.Int,
76
111
  fields: Schema.String,
77
112
  });
78
113
  type StoredDocument = typeof StoredDocument.Type;
79
114
 
115
+ const StoredTable = Schema.Struct({ id: TableId, name: Schema.String });
116
+ type StoredTable = typeof StoredTable.Type;
117
+ interface ResolvedTable extends StoredTable {
118
+ readonly definition: RuntimeTableDefinition;
119
+ }
120
+
80
121
  const DocumentLookup = Schema.Struct({
81
122
  id: Schema.String,
82
- tableName: Schema.String,
123
+ tableId: TableId,
83
124
  });
84
- const TableLookup = Schema.Struct({ tableName: Schema.String });
125
+ const TableLookup = Schema.Struct({ tableId: TableId });
126
+ const TableNameLookup = Schema.Struct({ name: Schema.String });
127
+ const TableInsert = Schema.Struct({ id: TableId, name: Schema.String });
128
+ const RevisionRow = Schema.Struct({ revision: AppStateRevision });
85
129
  type DatabaseOperation = "collect" | "get" | "insert" | "patch" | "replace";
86
130
 
131
+ interface DependencyRecorder {
132
+ readonly record: (...dependencies: ReadonlyArray<DependencyKey>) => void;
133
+ readonly values: () => ReadonlyArray<DependencyKey>;
134
+ }
135
+
136
+ const makeDependencyRecorder = (): DependencyRecorder => {
137
+ const dependencies = new Map<string, DependencyKey>();
138
+ return {
139
+ record: (...added) => {
140
+ for (const dependency of added) dependencies.set(dependencyKey(dependency), dependency);
141
+ },
142
+ values: () => globalThis.Array.from(dependencies.values()),
143
+ };
144
+ };
145
+
87
146
  export class UnknownDatabaseTable extends Schema.TaggedError<UnknownDatabaseTable>()(
88
147
  "UnknownDatabaseTable",
89
148
  {
@@ -139,37 +198,37 @@ const fieldsCodec = (table: RuntimeTableDefinition) =>
139
198
  );
140
199
 
141
200
  const encodeFields = Effect.fn("LocalDatabase.encodeFields")(function* (
142
- schema: RuntimeSchemaDefinition,
201
+ table: RuntimeTableDefinition,
143
202
  tableName: string,
144
203
  value: RuntimeValue,
145
204
  operation: Extract<DatabaseOperation, "insert" | "patch" | "replace">,
146
205
  id: string | null,
147
206
  ) {
148
- const table = yield* tableFor(schema, tableName);
149
207
  return yield* Schema.encodeEffect(fieldsCodec(table), {
150
208
  onExcessProperty: "error",
151
209
  })(value).pipe(Effect.mapError((cause) => schemaMismatch(tableName, id, operation, cause)));
152
210
  });
153
211
 
154
212
  const decodeFields = Effect.fn("LocalDatabase.decodeFields")(function* (
155
- schema: RuntimeSchemaDefinition,
213
+ table: RuntimeTableDefinition,
214
+ tableName: string,
156
215
  row: StoredDocument,
157
216
  operation: Extract<DatabaseOperation, "collect" | "get" | "patch">,
158
217
  ) {
159
- const table = yield* tableFor(schema, row.tableName);
160
218
  return yield* Schema.decodeEffect(fieldsCodec(table), {
161
219
  onExcessProperty: "error",
162
220
  })(row.fields).pipe(
163
- Effect.mapError((cause) => schemaMismatch(row.tableName, row.id, operation, cause)),
221
+ Effect.mapError((cause) => schemaMismatch(tableName, row.id, operation, cause)),
164
222
  );
165
223
  });
166
224
 
167
225
  const toRuntimeDocument = Effect.fn("LocalDatabase.toRuntimeDocument")(function* (
168
- schema: RuntimeSchemaDefinition,
226
+ table: RuntimeTableDefinition,
227
+ tableName: string,
169
228
  row: StoredDocument,
170
229
  operation: Extract<DatabaseOperation, "collect" | "get">,
171
230
  ) {
172
- const fields = yield* decodeFields(schema, row, operation);
231
+ const fields = yield* decodeFields(table, tableName, row, operation);
173
232
  return {
174
233
  ...fields,
175
234
  id: row.id,
@@ -188,76 +247,125 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
188
247
  LocalDatabase,
189
248
  Effect.gen(function* () {
190
249
  const ids = yield* IdGenerator;
250
+ yield* DevelopmentDatabase;
191
251
  const sql = yield* SqlClient.SqlClient;
192
252
 
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
- `;
253
+ const findTable = SqlSchema.findOneOption({
254
+ Request: TableNameLookup,
255
+ Result: StoredTable,
256
+ execute: ({ name }) => sql`
257
+ SELECT id, name
258
+ FROM tables
259
+ WHERE name = ${name}
260
+ LIMIT 1
261
+ `,
262
+ });
263
+ const insertTable = SqlSchema.findOne({
264
+ Request: TableInsert,
265
+ Result: StoredTable,
266
+ execute: ({ id, name }) => sql`
267
+ INSERT INTO tables (id, name)
268
+ VALUES (${id}, ${name})
269
+ ON CONFLICT (name) DO UPDATE SET name = excluded.name
270
+ RETURNING id, name
271
+ `,
272
+ });
207
273
 
208
274
  const findStored = SqlSchema.findOneOption({
209
275
  Request: DocumentLookup,
210
276
  Result: StoredDocument,
211
- execute: ({ id, tableName }) => sql`
212
- SELECT id, tableName, createdAt, updatedAt, fields
277
+ execute: ({ id, tableId }) => sql`
278
+ SELECT id, createdAt, updatedAt, fields
213
279
  FROM documents
214
- WHERE id = ${id} AND tableName = ${tableName}
280
+ WHERE id = ${id} AND tableId = ${tableId}
215
281
  LIMIT 1
216
282
  `,
217
283
  });
218
284
  const collectStored = SqlSchema.findAll({
219
285
  Request: TableLookup,
220
286
  Result: StoredDocument,
221
- execute: ({ tableName }) => sql`
222
- SELECT id, tableName, createdAt, updatedAt, fields
287
+ execute: ({ tableId }) => sql`
288
+ SELECT id, createdAt, updatedAt, fields
223
289
  FROM documents
224
- WHERE tableName = ${tableName}
290
+ WHERE tableId = ${tableId}
225
291
  ORDER BY createdAt ASC, id ASC
226
292
  `,
227
293
  });
294
+ const currentRevision = SqlSchema.findOne({
295
+ Request: Schema.Void,
296
+ Result: RevisionRow,
297
+ execute: () => sql`SELECT revision FROM app_state WHERE singleton = 1`,
298
+ });
299
+ const advanceRevision = SqlSchema.findOne({
300
+ Request: Schema.Void,
301
+ Result: RevisionRow,
302
+ execute: () => sql`
303
+ UPDATE app_state
304
+ SET revision = revision + 1
305
+ WHERE singleton = 1
306
+ RETURNING revision
307
+ `,
308
+ });
309
+
310
+ const resolveTable = Effect.fn("LocalDatabase.resolveTable")(function* (
311
+ schema: RuntimeSchemaDefinition,
312
+ name: string,
313
+ ): Effect.fn.Return<
314
+ ResolvedTable,
315
+ UnknownDatabaseTable | SqlError.SqlError | Schema.SchemaError
316
+ > {
317
+ const definition = yield* tableFor(schema, name);
318
+ const existing = yield* findTable({ name });
319
+ if (Option.isSome(existing)) return { ...existing.value, definition };
320
+ const id = yield* ids.generate(TableId);
321
+ const stored = yield* insertTable({ id, name }).pipe(
322
+ Effect.catchTags({ NoSuchElementError: Effect.die }),
323
+ );
324
+ return { ...stored, definition };
325
+ });
228
326
 
229
327
  const find = Effect.fn("LocalDatabase.find")(function* (
230
328
  schema: RuntimeSchemaDefinition,
231
329
  tableName: string,
232
330
  id: string,
331
+ dependencies?: DependencyRecorder,
233
332
  ) {
234
- yield* tableFor(schema, tableName);
235
- const row = yield* findStored({ id, tableName });
333
+ const table = yield* resolveTable(schema, tableName);
334
+ const documentId = GeneratedId.make(id);
335
+ dependencies?.record(documentDependency(table.id, documentId));
336
+ const row = yield* findStored({ id, tableId: table.id });
236
337
  if (Option.isNone(row)) return undefined;
237
- return yield* toRuntimeDocument(schema, row.value, "get");
338
+ return yield* toRuntimeDocument(table.definition, tableName, row.value, "get");
238
339
  });
239
340
 
240
341
  const collect = Effect.fn("LocalDatabase.collect")(function* (
241
342
  schema: RuntimeSchemaDefinition,
242
343
  tableName: string,
344
+ dependencies?: DependencyRecorder,
243
345
  ) {
244
- yield* tableFor(schema, tableName);
245
- const rows = yield* collectStored({ tableName });
246
- return yield* Effect.forEach(rows, (row) => toRuntimeDocument(schema, row, "collect"));
346
+ const table = yield* resolveTable(schema, tableName);
347
+ dependencies?.record(tableDependency(table.id));
348
+ const rows = yield* collectStored({ tableId: table.id });
349
+ return yield* Effect.forEach(rows, (row) =>
350
+ toRuntimeDocument(table.definition, tableName, row, "collect"),
351
+ );
247
352
  });
248
353
 
249
354
  const insert = Effect.fn("LocalDatabase.insert")(function* (
250
355
  schema: RuntimeSchemaDefinition,
251
356
  tableName: string,
252
357
  value: RuntimeValue,
358
+ invalidations: DependencyRecorder,
253
359
  ) {
254
- const fields = yield* encodeFields(schema, tableName, value, "insert", null);
255
- const id = yield* ids.generate;
360
+ const table = yield* resolveTable(schema, tableName);
361
+ const fields = yield* encodeFields(table.definition, tableName, value, "insert", null);
362
+ const id = yield* ids.generate(GeneratedId);
256
363
  const now = DateTime.toEpochMillis(yield* DateTime.now);
257
364
  yield* sql`
258
- INSERT INTO documents (id, tableName, createdAt, updatedAt, fields)
259
- VALUES (${id}, ${tableName}, ${now}, ${now}, ${fields})
365
+ INSERT INTO documents (id, tableId, createdAt, updatedAt, fields)
366
+ VALUES (${id}, ${table.id}, ${now}, ${now}, ${fields})
260
367
  `;
368
+ invalidations.record(tableDependency(table.id), documentDependency(table.id, id));
261
369
  return id;
262
370
  });
263
371
 
@@ -265,9 +373,12 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
265
373
  schema: RuntimeSchemaDefinition,
266
374
  tableName: string,
267
375
  id: string,
376
+ invalidations: DependencyRecorder,
268
377
  ) {
269
- yield* tableFor(schema, tableName);
270
- yield* sql`DELETE FROM documents WHERE id = ${id} AND tableName = ${tableName}`;
378
+ const table = yield* resolveTable(schema, tableName);
379
+ const documentId = GeneratedId.make(id);
380
+ yield* sql`DELETE FROM documents WHERE id = ${id} AND tableId = ${table.id}`;
381
+ invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId));
271
382
  });
272
383
 
273
384
  const patch = Effect.fn("LocalDatabase.patch")(function* (
@@ -275,24 +386,28 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
275
386
  tableName: string,
276
387
  id: string,
277
388
  value: RuntimeValue,
389
+ invalidations: DependencyRecorder,
278
390
  ) {
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
- `;
391
+ const table = yield* resolveTable(schema, tableName);
392
+ const documentId = GeneratedId.make(id);
393
+ const stored = yield* findStored({ id, tableId: table.id });
394
+ if (Option.isSome(stored)) {
395
+ const current = yield* decodeFields(table.definition, tableName, stored.value, "patch");
396
+ const fields = yield* encodeFields(
397
+ table.definition,
398
+ tableName,
399
+ { ...current, ...value },
400
+ "patch",
401
+ id,
402
+ );
403
+ const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
404
+ yield* sql`
405
+ UPDATE documents
406
+ SET fields = ${fields}, updatedAt = ${updatedAt}
407
+ WHERE id = ${id} AND tableId = ${table.id}
408
+ `;
409
+ }
410
+ invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId));
296
411
  });
297
412
 
298
413
  const replace = Effect.fn("LocalDatabase.replace")(function* (
@@ -300,26 +415,33 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
300
415
  tableName: string,
301
416
  id: string,
302
417
  value: RuntimeValue,
418
+ invalidations: DependencyRecorder,
303
419
  ) {
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
- `;
420
+ const table = yield* resolveTable(schema, tableName);
421
+ const documentId = GeneratedId.make(id);
422
+ const stored = yield* findStored({ id, tableId: table.id });
423
+ if (Option.isSome(stored)) {
424
+ const fields = yield* encodeFields(table.definition, tableName, value, "replace", id);
425
+ const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
426
+ yield* sql`
427
+ UPDATE documents
428
+ SET fields = ${fields}, updatedAt = ${updatedAt}
429
+ WHERE id = ${id} AND tableId = ${table.id}
430
+ `;
431
+ }
432
+ invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId));
314
433
  });
315
434
 
316
- const makeReader = (schema: RuntimeSchemaDefinition): LocalDatabaseReader =>
435
+ const makeReader = (
436
+ schema: RuntimeSchemaDefinition,
437
+ dependencies?: DependencyRecorder,
438
+ ): LocalDatabaseReader =>
317
439
  Object.freeze({
318
440
  find: (tableName: string, id: string) =>
319
- resultFromEffect(find(schema, tableName, id).pipe(Effect.orDie)),
441
+ resultFromEffect(find(schema, tableName, id, dependencies).pipe(Effect.orDie)),
320
442
  get: (tableName: string, id: string) =>
321
443
  resultFromEffect(
322
- find(schema, tableName, id).pipe(
444
+ find(schema, tableName, id, dependencies).pipe(
323
445
  Effect.orDie,
324
446
  Effect.flatMap((document) =>
325
447
  document === undefined
@@ -330,44 +452,79 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
330
452
  ),
331
453
  query: (tableName: string) =>
332
454
  Object.freeze({
333
- collect: () => resultFromEffect(collect(schema, tableName).pipe(Effect.orDie)),
455
+ collect: () =>
456
+ resultFromEffect(collect(schema, tableName, dependencies).pipe(Effect.orDie)),
334
457
  }),
335
458
  });
336
459
 
337
- const makeWriter = (schema: RuntimeSchemaDefinition): LocalDatabaseWriter => {
460
+ const makeWriter = (
461
+ schema: RuntimeSchemaDefinition,
462
+ invalidations: DependencyRecorder,
463
+ ): LocalDatabaseWriter => {
338
464
  const reader = makeReader(schema);
339
465
  return Object.freeze({
340
466
  ...reader,
341
467
  delete: (tableName: string, id: string) =>
342
- resultFromEffect(deleteDocument(schema, tableName, id).pipe(Effect.orDie)),
468
+ resultFromEffect(
469
+ deleteDocument(schema, tableName, id, invalidations).pipe(Effect.orDie),
470
+ ),
343
471
  insert: (tableName: string, value: RuntimeValue) =>
344
- resultFromEffect(insert(schema, tableName, value).pipe(Effect.orDie)),
472
+ resultFromEffect(insert(schema, tableName, value, invalidations).pipe(Effect.orDie)),
345
473
  patch: (tableName: string, id: string, value: RuntimeValue) =>
346
- resultFromEffect(patch(schema, tableName, id, value).pipe(Effect.orDie)),
474
+ resultFromEffect(patch(schema, tableName, id, value, invalidations).pipe(Effect.orDie)),
347
475
  replace: (tableName: string, id: string, value: RuntimeValue) =>
348
- resultFromEffect(replace(schema, tableName, id, value).pipe(Effect.orDie)),
476
+ resultFromEffect(
477
+ replace(schema, tableName, id, value, invalidations).pipe(Effect.orDie),
478
+ ),
349
479
  });
350
480
  };
351
481
 
482
+ const trackedQueryTransaction: LocalDatabaseService["trackedQueryTransaction"] = (
483
+ schema,
484
+ use,
485
+ ) =>
486
+ sql.withTransaction(
487
+ Effect.gen(function* () {
488
+ const dependencies = makeDependencyRecorder();
489
+ const context = Object.freeze({
490
+ db: makeReader(schema, dependencies),
491
+ });
492
+ const value = yield* use(context);
493
+ const { revision: observedRevision } = yield* currentRevision(undefined).pipe(
494
+ Effect.orDie,
495
+ );
496
+ return { value, dependencies: dependencies.values(), observedRevision };
497
+ }),
498
+ );
499
+ const trackedMutationTransaction: LocalDatabaseService["trackedMutationTransaction"] = (
500
+ schema,
501
+ use,
502
+ ) =>
503
+ sql.withTransaction(
504
+ Effect.gen(function* () {
505
+ const invalidations = makeDependencyRecorder();
506
+ const context = Object.freeze({
507
+ db: makeWriter(schema, invalidations),
508
+ });
509
+ const value = yield* use(context);
510
+ const { revision: committedRevision } = yield* advanceRevision(undefined).pipe(
511
+ Effect.orDie,
512
+ );
513
+ return { value, invalidations: invalidations.values(), committedRevision };
514
+ }),
515
+ );
516
+
352
517
  return LocalDatabase.of({
518
+ revision: currentRevision(undefined).pipe(
519
+ Effect.orDie,
520
+ Effect.map(({ revision }) => revision),
521
+ ),
353
522
  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
- ),
523
+ trackedQueryTransaction(schema, use).pipe(Effect.map(({ value }) => value)),
524
+ trackedQueryTransaction,
362
525
  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
- ),
526
+ trackedMutationTransaction(schema, use).pipe(Effect.map(({ value }) => value)),
527
+ trackedMutationTransaction,
371
528
  });
372
529
  }),
373
530
  );
@@ -1,11 +1,13 @@
1
- import { Crypto, Effect, FileSystem, Option, Path, Predicate, Schema } from "effect";
1
+ import { DevDatabaseLockId } from "@ignotum/contracts/runtime/id";
2
+ import { IdGenerator } from "@ignotum/shared/id";
3
+ import { Effect, FileSystem, Option, Path, Predicate, Schema } from "effect";
2
4
 
3
5
  const databaseFileNames = ["state.db", "state.db-shm", "state.db-wal", "state.db-journal"] as const;
4
6
  const lockFileName = "lock.json";
5
7
 
6
8
  const DevDatabaseLockRecord = Schema.Struct({
7
9
  pid: Schema.Int,
8
- token: Schema.String,
10
+ token: DevDatabaseLockId,
9
11
  });
10
12
  const DevDatabaseLockRecordJson = Schema.fromJsonString(DevDatabaseLockRecord);
11
13
  type DevDatabaseLockRecord = typeof DevDatabaseLockRecord.Type;
@@ -34,9 +36,9 @@ export class DevDatabaseLockFailed extends Schema.TaggedError<DevDatabaseLockFai
34
36
  },
35
37
  ) {}
36
38
 
37
- export const devDatabasePath = Effect.fn("DevDatabase.path")(function* (projectDirectory: string) {
39
+ export const devDatabasePath = Effect.fn("DevDatabase.path")(function* (appDirectory: string) {
38
40
  const path = yield* Path.Path;
39
- return path.join(projectDirectory, ".ignotum", "dev", "state.db");
41
+ return path.join(appDirectory, ".ignotum", "dev", "state.db");
40
42
  });
41
43
 
42
44
  const processIsRunning = (pid: number): boolean => {
@@ -49,17 +51,17 @@ const processIsRunning = (pid: number): boolean => {
49
51
  };
50
52
 
51
53
  export const acquireDevDatabaseLock = Effect.fn("DevDatabase.acquireLock")(function* (
52
- projectDirectory: string,
54
+ appDirectory: string,
53
55
  ) {
54
- const crypto = yield* Crypto.Crypto;
56
+ const ids = yield* IdGenerator;
55
57
  const fileSystem = yield* FileSystem.FileSystem;
56
58
  const path = yield* Path.Path;
57
- const databasePath = yield* devDatabasePath(projectDirectory);
59
+ const databasePath = yield* devDatabasePath(appDirectory);
58
60
  const directory = path.dirname(databasePath);
59
61
  const lockPath = path.join(directory, lockFileName);
60
62
  const record: DevDatabaseLockRecord = {
61
63
  pid: process.pid,
62
- token: yield* crypto.randomUUIDv4,
64
+ token: yield* ids.generate(DevDatabaseLockId),
63
65
  };
64
66
  const encoded = yield* Schema.encodeEffect(DevDatabaseLockRecordJson)(record).pipe(
65
67
  Effect.mapError((cause) =>
@@ -162,16 +164,14 @@ export const acquireDevDatabaseLock = Effect.fn("DevDatabase.acquireLock")(funct
162
164
  yield* Effect.acquireRelease(acquire, release);
163
165
  });
164
166
 
165
- export const resetDevDatabase = Effect.fn("DevDatabase.reset")(function* (
166
- projectDirectory: string,
167
- ) {
167
+ export const resetDevDatabase = Effect.fn("DevDatabase.reset")(function* (appDirectory: string) {
168
168
  return yield* Effect.scoped(
169
169
  Effect.gen(function* () {
170
- yield* acquireDevDatabaseLock(projectDirectory);
170
+ yield* acquireDevDatabaseLock(appDirectory);
171
171
  const fileSystem = yield* FileSystem.FileSystem;
172
172
  const path = yield* Path.Path;
173
- const directory = path.join(projectDirectory, ".ignotum", "dev");
174
- const databasePath = yield* devDatabasePath(projectDirectory);
173
+ const directory = path.join(appDirectory, ".ignotum", "dev");
174
+ const databasePath = yield* devDatabasePath(appDirectory);
175
175
  let removed = 0;
176
176
 
177
177
  yield* Effect.forEach(