@mepuka/skygent 0.2.0 → 0.3.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.
Files changed (49) hide show
  1. package/README.md +269 -31
  2. package/index.ts +18 -3
  3. package/package.json +1 -1
  4. package/src/cli/app.ts +4 -2
  5. package/src/cli/config.ts +20 -3
  6. package/src/cli/doc/table-renderers.ts +29 -0
  7. package/src/cli/doc/thread.ts +2 -4
  8. package/src/cli/exit-codes.ts +2 -0
  9. package/src/cli/feed.ts +35 -55
  10. package/src/cli/filter-dsl.ts +146 -11
  11. package/src/cli/filter-errors.ts +9 -3
  12. package/src/cli/filter-help.ts +7 -0
  13. package/src/cli/filter-input.ts +3 -2
  14. package/src/cli/filter.ts +84 -4
  15. package/src/cli/graph.ts +193 -156
  16. package/src/cli/input.ts +45 -0
  17. package/src/cli/layers.ts +10 -0
  18. package/src/cli/logging.ts +8 -0
  19. package/src/cli/output-render.ts +14 -0
  20. package/src/cli/pagination.ts +18 -0
  21. package/src/cli/parse-errors.ts +18 -0
  22. package/src/cli/pipe.ts +157 -0
  23. package/src/cli/post.ts +43 -66
  24. package/src/cli/query.ts +349 -74
  25. package/src/cli/search.ts +92 -118
  26. package/src/cli/shared.ts +0 -19
  27. package/src/cli/store-errors.ts +24 -13
  28. package/src/cli/store-tree.ts +6 -4
  29. package/src/cli/store.ts +35 -2
  30. package/src/cli/stream-merge.ts +105 -0
  31. package/src/cli/sync-factory.ts +28 -3
  32. package/src/cli/sync.ts +16 -18
  33. package/src/cli/thread-options.ts +33 -0
  34. package/src/cli/time.ts +171 -0
  35. package/src/cli/view-thread.ts +12 -18
  36. package/src/cli/watch.ts +61 -19
  37. package/src/domain/errors.ts +6 -1
  38. package/src/domain/format.ts +21 -0
  39. package/src/domain/order.ts +24 -0
  40. package/src/graph/relationships.ts +129 -0
  41. package/src/services/jetstream-sync.ts +4 -4
  42. package/src/services/lineage-store.ts +15 -1
  43. package/src/services/store-commit.ts +60 -0
  44. package/src/services/store-manager.ts +69 -2
  45. package/src/services/store-renamer.ts +286 -0
  46. package/src/services/store-stats.ts +7 -5
  47. package/src/services/sync-engine.ts +136 -85
  48. package/src/services/sync-reporter.ts +3 -1
  49. package/src/services/sync-settings.ts +24 -0
@@ -0,0 +1,129 @@
1
+ import { Graph, Option } from "effect";
2
+ import type { RelationshipView } from "../domain/bsky.js";
3
+
4
+ export type RelationshipNode = {
5
+ readonly did?: string;
6
+ readonly handle?: string;
7
+ readonly displayName?: string;
8
+ readonly inputs: ReadonlyArray<string>;
9
+ readonly notFound?: boolean;
10
+ };
11
+
12
+ export type RelationshipEdge = {
13
+ readonly following: boolean;
14
+ readonly followedBy: boolean;
15
+ readonly mutual: boolean;
16
+ readonly blocking: boolean;
17
+ readonly blockedBy: boolean;
18
+ readonly blockingByList: boolean;
19
+ readonly blockedByList: boolean;
20
+ readonly notFound: boolean;
21
+ };
22
+
23
+ export type RelationshipEntry = {
24
+ readonly actor: RelationshipNode;
25
+ readonly other: RelationshipNode;
26
+ readonly relationship: RelationshipEdge;
27
+ };
28
+
29
+ export type RelationshipGraphResult = {
30
+ readonly graph: Graph.DirectedGraph<RelationshipNode, RelationshipEdge>;
31
+ readonly actorIndex: Graph.NodeIndex;
32
+ readonly nodeIndexByKey: Map<string, Graph.NodeIndex>;
33
+ };
34
+
35
+ const edgeFromRelationship = (relationship: RelationshipView): RelationshipEdge => {
36
+ if (!("did" in relationship)) {
37
+ return {
38
+ following: false,
39
+ followedBy: false,
40
+ mutual: false,
41
+ blocking: false,
42
+ blockedBy: false,
43
+ blockingByList: false,
44
+ blockedByList: false,
45
+ notFound: true
46
+ };
47
+ }
48
+ const following = typeof relationship.following === "string";
49
+ const followedBy = typeof relationship.followedBy === "string";
50
+ const blocking = typeof relationship.blocking === "string";
51
+ const blockedBy = typeof relationship.blockedBy === "string";
52
+ const blockingByList = typeof relationship.blockingByList === "string";
53
+ const blockedByList = typeof relationship.blockedByList === "string";
54
+ return {
55
+ following,
56
+ followedBy,
57
+ mutual: following && followedBy,
58
+ blocking,
59
+ blockedBy,
60
+ blockingByList,
61
+ blockedByList,
62
+ notFound: false
63
+ };
64
+ };
65
+
66
+ const nodeFromKey = (key: string): RelationshipNode => ({
67
+ ...(key.startsWith("did:") ? { did: key } : {}),
68
+ inputs: [key],
69
+ notFound: true
70
+ });
71
+
72
+ export const buildRelationshipGraph = (
73
+ actorKey: string,
74
+ nodesByKey: Map<string, RelationshipNode>,
75
+ relationships: ReadonlyArray<RelationshipView>
76
+ ): RelationshipGraphResult => {
77
+ const base = Graph.directed<RelationshipNode, RelationshipEdge>();
78
+ const nodeIndexByKey = new Map<string, Graph.NodeIndex>();
79
+
80
+ const graph = Graph.mutate(base, (mutable) => {
81
+ const actorNode = nodesByKey.get(actorKey) ?? nodeFromKey(actorKey);
82
+ const actorIndex = Graph.addNode(mutable, actorNode);
83
+ nodeIndexByKey.set(actorKey, actorIndex);
84
+
85
+ const ensureNode = (key: string, node: RelationshipNode) => {
86
+ const existing = nodeIndexByKey.get(key);
87
+ if (existing !== undefined) {
88
+ return existing;
89
+ }
90
+ const index = Graph.addNode(mutable, node);
91
+ nodeIndexByKey.set(key, index);
92
+ return index;
93
+ };
94
+
95
+ for (const relationship of relationships) {
96
+ const key = "did" in relationship ? relationship.did : relationship.actor;
97
+ const node = nodesByKey.get(key) ?? nodeFromKey(key);
98
+ const otherIndex = ensureNode(key, node);
99
+ Graph.addEdge(mutable, actorIndex, otherIndex, edgeFromRelationship(relationship));
100
+ }
101
+ });
102
+
103
+ const actorIndex = nodeIndexByKey.get(actorKey);
104
+ if (actorIndex === undefined) {
105
+ throw new Error(`Missing actor node for ${actorKey}`);
106
+ }
107
+
108
+ return { graph, actorIndex, nodeIndexByKey };
109
+ };
110
+
111
+ export const relationshipEntries = (
112
+ graph: Graph.DirectedGraph<RelationshipNode, RelationshipEdge>
113
+ ): ReadonlyArray<RelationshipEntry> => {
114
+ const edges = Array.from(Graph.values(Graph.edges(graph)));
115
+ const entries: RelationshipEntry[] = [];
116
+ for (const edge of edges) {
117
+ const actor = Option.getOrUndefined(Graph.getNode(graph, edge.source));
118
+ const other = Option.getOrUndefined(Graph.getNode(graph, edge.target));
119
+ if (!actor || !other) {
120
+ continue;
121
+ }
122
+ entries.push({ actor, other, relationship: edge.data });
123
+ }
124
+ return entries;
125
+ };
126
+
127
+ export const relationshipMermaid = (
128
+ graph: Graph.DirectedGraph<RelationshipNode, RelationshipEdge>
129
+ ): string => Graph.toMermaid(graph);
@@ -583,10 +583,10 @@ export class JetstreamSyncEngine extends Context.Tag("@skygent/JetstreamSyncEngi
583
583
  Stream.interruptWhen(
584
584
  Effect.sleep(config.duration).pipe(
585
585
  Effect.zipRight(
586
- Effect.logWarning(
587
- "Jetstream sync exceeded duration; shutting down.",
588
- { durationMs: Duration.toMillis(config.duration) }
589
- )
586
+ reporter.warn("Jetstream sync exceeded duration; shutting down.", {
587
+ durationMs: Duration.toMillis(config.duration),
588
+ store: config.store.name
589
+ })
590
590
  ),
591
591
  Effect.zipRight(safeShutdown)
592
592
  )
@@ -63,6 +63,14 @@ export class LineageStore extends Context.Tag("@skygent/LineageStore")<
63
63
  * @returns Effect resolving to void, or StoreIoError on failure
64
64
  */
65
65
  readonly save: (lineage: StoreLineage) => Effect.Effect<void, StoreIoError>;
66
+
67
+ /**
68
+ * Removes lineage information for a store.
69
+ *
70
+ * @param storeName - The name of the store to remove lineage for
71
+ * @returns Effect resolving to void, or StoreIoError on failure
72
+ */
73
+ readonly remove: (storeName: StoreName) => Effect.Effect<void, StoreIoError>;
66
74
  }
67
75
  >() {
68
76
  static readonly layer = Layer.effect(
@@ -83,7 +91,13 @@ export class LineageStore extends Context.Tag("@skygent/LineageStore")<
83
91
  .pipe(Effect.mapError(toStoreIoError(lineage.storeName)))
84
92
  );
85
93
 
86
- return LineageStore.of({ get, save });
94
+ const remove = Effect.fn("LineageStore.remove")((storeName: StoreName) =>
95
+ lineages
96
+ .remove(lineageKey(storeName))
97
+ .pipe(Effect.mapError(toStoreIoError(storeName)))
98
+ );
99
+
100
+ return LineageStore.of({ get, save, remove });
87
101
  })
88
102
  );
89
103
  }
@@ -67,6 +67,13 @@ export class StoreCommitter extends Context.Tag("@skygent/StoreCommitter")<
67
67
  store: StoreRef,
68
68
  event: PostUpsert
69
69
  ) => Effect.Effect<EventLogEntry, StoreIoError>;
70
+ /**
71
+ * Append multiple upsert events in a single transaction.
72
+ */
73
+ readonly appendUpserts: (
74
+ store: StoreRef,
75
+ events: ReadonlyArray<PostUpsert>
76
+ ) => Effect.Effect<ReadonlyArray<EventLogEntry>, StoreIoError>;
70
77
 
71
78
  /**
72
79
  * Append an upsert event only if the post doesn't already exist.
@@ -84,6 +91,13 @@ export class StoreCommitter extends Context.Tag("@skygent/StoreCommitter")<
84
91
  store: StoreRef,
85
92
  event: PostUpsert
86
93
  ) => Effect.Effect<Option.Option<EventLogEntry>, StoreIoError>;
94
+ /**
95
+ * Append multiple upsert events if missing in a single transaction.
96
+ */
97
+ readonly appendUpsertsIfMissing: (
98
+ store: StoreRef,
99
+ events: ReadonlyArray<PostUpsert>
100
+ ) => Effect.Effect<ReadonlyArray<Option.Option<EventLogEntry>>, StoreIoError>;
87
101
 
88
102
  /**
89
103
  * Append a delete event to the store, removing the post.
@@ -125,6 +139,26 @@ export class StoreCommitter extends Context.Tag("@skygent/StoreCommitter")<
125
139
  .pipe(Effect.mapError(toStoreIoError(store.root)))
126
140
  );
127
141
 
142
+ const appendUpserts = Effect.fn("StoreCommitter.appendUpserts")(
143
+ (store: StoreRef, events: ReadonlyArray<PostUpsert>) => {
144
+ if (events.length === 0) {
145
+ return Effect.succeed([] as ReadonlyArray<EventLogEntry>);
146
+ }
147
+ return storeDb
148
+ .withClient(store, (client) =>
149
+ client.withTransaction(
150
+ Effect.forEach(events, (event) =>
151
+ Effect.gen(function* () {
152
+ yield* upsertPost(client, event.post);
153
+ return yield* writer.appendWithClient(client, event);
154
+ })
155
+ )
156
+ )
157
+ )
158
+ .pipe(Effect.mapError(toStoreIoError(store.root)));
159
+ }
160
+ );
161
+
128
162
  const appendUpsertIfMissing = Effect.fn(
129
163
  "StoreCommitter.appendUpsertIfMissing"
130
164
  )((store: StoreRef, event: PostUpsert) =>
@@ -144,6 +178,30 @@ export class StoreCommitter extends Context.Tag("@skygent/StoreCommitter")<
144
178
  .pipe(Effect.mapError(toStoreIoError(store.root)))
145
179
  );
146
180
 
181
+ const appendUpsertsIfMissing = Effect.fn(
182
+ "StoreCommitter.appendUpsertsIfMissing"
183
+ )((store: StoreRef, events: ReadonlyArray<PostUpsert>) => {
184
+ if (events.length === 0) {
185
+ return Effect.succeed([] as ReadonlyArray<Option.Option<EventLogEntry>>);
186
+ }
187
+ return storeDb
188
+ .withClient(store, (client) =>
189
+ client.withTransaction(
190
+ Effect.forEach(events, (event) =>
191
+ Effect.gen(function* () {
192
+ const inserted = yield* insertPostIfMissing(client, event.post);
193
+ if (!inserted) {
194
+ return Option.none<EventLogEntry>();
195
+ }
196
+ const entry = yield* writer.appendWithClient(client, event);
197
+ return Option.some(entry);
198
+ })
199
+ )
200
+ )
201
+ )
202
+ .pipe(Effect.mapError(toStoreIoError(store.root)));
203
+ });
204
+
147
205
  const appendDelete = Effect.fn("StoreCommitter.appendDelete")(
148
206
  (store: StoreRef, event: PostDelete) =>
149
207
  storeDb
@@ -160,7 +218,9 @@ export class StoreCommitter extends Context.Tag("@skygent/StoreCommitter")<
160
218
 
161
219
  return StoreCommitter.of({
162
220
  appendUpsert,
221
+ appendUpserts,
163
222
  appendUpsertIfMissing,
223
+ appendUpsertsIfMissing,
164
224
  appendDelete
165
225
  });
166
226
  })
@@ -63,7 +63,7 @@ import * as MigratorFileSystem from "@effect/sql/Migrator/FileSystem";
63
63
  import * as SqlClient from "@effect/sql/SqlClient";
64
64
  import * as SqlSchema from "@effect/sql/SqlSchema";
65
65
  import { SqliteClient } from "@effect/sql-sqlite-bun";
66
- import { StoreIoError } from "../domain/errors.js";
66
+ import { StoreAlreadyExists, StoreIoError, StoreNotFound } from "../domain/errors.js";
67
67
  import { StoreConfig, StoreMetadata, StoreRef } from "../domain/store.js";
68
68
  import { StoreName, StorePath } from "../domain/primitives.js";
69
69
  import { AppConfigService } from "./app-config.js";
@@ -188,6 +188,23 @@ export class StoreManager extends Context.Tag("@skygent/StoreManager")<
188
188
  readonly deleteStore: (
189
189
  name: StoreName
190
190
  ) => Effect.Effect<void, StoreIoError>;
191
+
192
+ /**
193
+ * Renames a store in the catalog.
194
+ *
195
+ * This updates the store name and root path while preserving creation time.
196
+ *
197
+ * @param from - Existing store name
198
+ * @param to - New store name
199
+ * @returns Effect resolving to the updated StoreRef
200
+ * @throws {StoreNotFound} If the source store does not exist
201
+ * @throws {StoreAlreadyExists} If the target store already exists
202
+ * @throws {StoreIoError} When database operations fail
203
+ */
204
+ readonly renameStore: (
205
+ from: StoreName,
206
+ to: StoreName
207
+ ) => Effect.Effect<StoreRef, StoreNotFound | StoreAlreadyExists | StoreIoError>;
191
208
  }
192
209
  >() {
193
210
  /**
@@ -267,6 +284,21 @@ export class StoreManager extends Context.Tag("@skygent/StoreManager")<
267
284
  client`DELETE FROM stores WHERE name = ${name}`
268
285
  });
269
286
 
287
+ const renameStoreSql = SqlSchema.void({
288
+ Request: Schema.Struct({
289
+ oldName: StoreName,
290
+ newName: StoreName,
291
+ root: StorePath,
292
+ updatedAt: Schema.String
293
+ }),
294
+ execute: ({ oldName, newName, root, updatedAt }) =>
295
+ client`UPDATE stores
296
+ SET name = ${newName},
297
+ root = ${root},
298
+ updated_at = ${updatedAt}
299
+ WHERE name = ${oldName}`
300
+ });
301
+
270
302
  const createStore = Effect.fn("StoreManager.createStore")(
271
303
  (name: StoreName, config: StoreConfig) =>
272
304
  decodeStorePath(storeRootKey(name)).pipe(
@@ -337,6 +369,34 @@ export class StoreManager extends Context.Tag("@skygent/StoreManager")<
337
369
  );
338
370
  });
339
371
 
372
+ const renameStore = Effect.fn("StoreManager.renameStore")(
373
+ (from: StoreName, to: StoreName) =>
374
+ Effect.gen(function* () {
375
+ const newRoot = yield* decodeStorePath(storeRootKey(to));
376
+ const existing = yield* findStore(from).pipe(
377
+ Effect.mapError(toStoreIoError(manifestPath))
378
+ );
379
+ if (existing.length === 0) {
380
+ return yield* StoreNotFound.make({ name: from });
381
+ }
382
+ const conflict = yield* findStore(to).pipe(
383
+ Effect.mapError(toStoreIoError(manifestPath))
384
+ );
385
+ if (conflict.length > 0) {
386
+ return yield* StoreAlreadyExists.make({ name: to });
387
+ }
388
+ const nowMillis = yield* Clock.currentTimeMillis;
389
+ const now = new Date(nowMillis).toISOString();
390
+ yield* renameStoreSql({
391
+ oldName: from,
392
+ newName: to,
393
+ root: newRoot,
394
+ updatedAt: now
395
+ }).pipe(Effect.mapError(toStoreIoError(manifestPath)));
396
+ return StoreRef.make({ name: to, root: newRoot });
397
+ })
398
+ );
399
+
340
400
  const listStores = Effect.fn("StoreManager.listStores")(() =>
341
401
  Effect.gen(function* () {
342
402
  const rows = yield* listStoresSql(undefined);
@@ -352,7 +412,14 @@ export class StoreManager extends Context.Tag("@skygent/StoreManager")<
352
412
  }).pipe(Effect.mapError(toStoreIoError(manifestPath)))
353
413
  );
354
414
 
355
- return StoreManager.of({ createStore, getStore, listStores, getConfig, deleteStore });
415
+ return StoreManager.of({
416
+ createStore,
417
+ getStore,
418
+ listStores,
419
+ getConfig,
420
+ deleteStore,
421
+ renameStore
422
+ });
356
423
  })
357
424
  ).pipe(Layer.provide(Reactivity.layer));
358
425
  }
@@ -0,0 +1,286 @@
1
+ import { FileSystem, Path } from "@effect/platform";
2
+ import { Chunk, Context, Effect, Layer, Option, Ref, Schema } from "effect";
3
+ import { StoreAlreadyExists, StoreIoError, StoreNotFound } from "../domain/errors.js";
4
+ import { StoreLineage, StoreSource } from "../domain/derivation.js";
5
+ import { StoreName, StorePath } from "../domain/primitives.js";
6
+ import { StoreRef } from "../domain/store.js";
7
+ import { AppConfigService } from "./app-config.js";
8
+ import { LineageStore } from "./lineage-store.js";
9
+ import { StoreDb } from "./store-db.js";
10
+ import { StoreManager } from "./store-manager.js";
11
+
12
+ type StoreRenameResult = {
13
+ readonly from: StoreName;
14
+ readonly to: StoreName;
15
+ readonly moved: boolean;
16
+ readonly lineagesUpdated: number;
17
+ readonly checkpointsUpdated: number;
18
+ };
19
+
20
+ type RenameState = {
21
+ readonly completed: boolean;
22
+ readonly dirRenamed: boolean;
23
+ readonly catalogUpdated: boolean;
24
+ readonly checkpointsUpdated: boolean;
25
+ readonly lineagesUpdated: boolean;
26
+ };
27
+
28
+ const storeRootKey = (name: StoreName) =>
29
+ Schema.decodeUnknownSync(StorePath)(`stores/${name}`);
30
+
31
+ const toStoreIoError = (path: StorePath) => (cause: unknown) =>
32
+ StoreIoError.make({ path, cause });
33
+
34
+ const toStoreRef = (metadata: { readonly name: StoreName; readonly root: StorePath }) =>
35
+ StoreRef.make({ name: metadata.name, root: metadata.root });
36
+
37
+ const renameDirectory = (
38
+ fs: FileSystem.FileSystem,
39
+ path: Path.Path,
40
+ fromPath: string,
41
+ toPath: string
42
+ ) =>
43
+ Effect.gen(function* () {
44
+ if (fromPath === toPath) {
45
+ return;
46
+ }
47
+ const normalizedFrom = fromPath.toLowerCase();
48
+ const normalizedTo = toPath.toLowerCase();
49
+ if (normalizedFrom === normalizedTo) {
50
+ const tempName = `.rename-${Date.now()}`;
51
+ const tempPath = path.join(path.dirname(toPath), tempName);
52
+ yield* fs.rename(fromPath, tempPath);
53
+ yield* fs.rename(tempPath, toPath);
54
+ return;
55
+ }
56
+ yield* fs.rename(fromPath, toPath);
57
+ });
58
+
59
+ const renameLineage = (
60
+ lineage: StoreLineage,
61
+ from: StoreName,
62
+ to: StoreName
63
+ ) => {
64
+ const nextStoreName = lineage.storeName === from ? to : lineage.storeName;
65
+ let changed = nextStoreName !== lineage.storeName;
66
+ const nextSources = lineage.sources.map((source) => {
67
+ if (source.storeName !== from) {
68
+ return source;
69
+ }
70
+ changed = true;
71
+ return StoreSource.make({ ...source, storeName: to });
72
+ });
73
+ if (!changed) {
74
+ return Option.none<StoreLineage>();
75
+ }
76
+ return Option.some(
77
+ StoreLineage.make({
78
+ ...lineage,
79
+ storeName: nextStoreName,
80
+ sources: nextSources
81
+ })
82
+ );
83
+ };
84
+
85
+ export class StoreRenamer extends Context.Tag("@skygent/StoreRenamer")<
86
+ StoreRenamer,
87
+ {
88
+ readonly rename: (
89
+ from: StoreName,
90
+ to: StoreName
91
+ ) => Effect.Effect<StoreRenameResult, StoreNotFound | StoreAlreadyExists | StoreIoError>;
92
+ }
93
+ >() {
94
+ static readonly layer = Layer.effect(
95
+ StoreRenamer,
96
+ Effect.gen(function* () {
97
+ const manager = yield* StoreManager;
98
+ const storeDb = yield* StoreDb;
99
+ const lineageStore = yield* LineageStore;
100
+ const appConfig = yield* AppConfigService;
101
+ const fs = yield* FileSystem.FileSystem;
102
+ const path = yield* Path.Path;
103
+
104
+ const updateLineages = (
105
+ from: StoreName,
106
+ to: StoreName,
107
+ storeNames: ReadonlyArray<StoreName>
108
+ ) =>
109
+ Effect.forEach(
110
+ storeNames,
111
+ (storeName) =>
112
+ lineageStore.get(storeName).pipe(
113
+ Effect.flatMap(
114
+ Option.match({
115
+ onNone: () => Effect.succeed(0),
116
+ onSome: (lineage) =>
117
+ Option.match(renameLineage(lineage, from, to), {
118
+ onNone: () => Effect.succeed(0),
119
+ onSome: (updated) =>
120
+ Effect.gen(function* () {
121
+ yield* lineageStore.save(updated);
122
+ if (lineage.storeName === from) {
123
+ yield* lineageStore.remove(from);
124
+ }
125
+ return 1;
126
+ })
127
+ })
128
+ })
129
+ )
130
+ ),
131
+ { discard: false }
132
+ ).pipe(Effect.map((updates) => updates.reduce((sum, value) => sum + value, 0)));
133
+
134
+ const updateDerivationCheckpoints = (
135
+ from: StoreName,
136
+ to: StoreName,
137
+ stores: ReadonlyArray<StoreRef>
138
+ ) =>
139
+ Effect.forEach(
140
+ stores,
141
+ (store) =>
142
+ storeDb.withClient(store, (client) =>
143
+ client`UPDATE derivation_checkpoints
144
+ SET view_name = CASE WHEN view_name = ${from} THEN ${to} ELSE view_name END,
145
+ source_store = CASE WHEN source_store = ${from} THEN ${to} ELSE source_store END,
146
+ target_store = CASE WHEN target_store = ${from} THEN ${to} ELSE target_store END
147
+ WHERE view_name = ${from}
148
+ OR source_store = ${from}
149
+ OR target_store = ${from}`.pipe(Effect.as(1))
150
+ ).pipe(Effect.mapError(toStoreIoError(store.root))),
151
+ { discard: false }
152
+ ).pipe(Effect.map((updates) => updates.reduce((sum, value) => sum + value, 0)));
153
+
154
+ const rename = Effect.fn("StoreRenamer.rename")(
155
+ (from: StoreName, to: StoreName) =>
156
+ Effect.gen(function* () {
157
+ const storeOption = yield* manager.getStore(from);
158
+ if (Option.isNone(storeOption)) {
159
+ return yield* StoreNotFound.make({ name: from });
160
+ }
161
+ const targetOption = yield* manager.getStore(to);
162
+ if (Option.isSome(targetOption)) {
163
+ return yield* StoreAlreadyExists.make({ name: to });
164
+ }
165
+ const store = storeOption.value;
166
+ const newRoot = storeRootKey(to);
167
+ const fromPath = path.join(appConfig.storeRoot, store.root);
168
+ const toPath = path.join(appConfig.storeRoot, newRoot);
169
+ const fromExists = yield* fs
170
+ .exists(fromPath)
171
+ .pipe(Effect.mapError(toStoreIoError(store.root)));
172
+ const toExists = yield* fs
173
+ .exists(toPath)
174
+ .pipe(Effect.mapError(toStoreIoError(newRoot)));
175
+ if (toExists) {
176
+ return yield* StoreAlreadyExists.make({ name: to });
177
+ }
178
+
179
+ const storesBefore = yield* manager.listStores();
180
+ const storeNamesBefore = Chunk.toReadonlyArray(storesBefore).map(
181
+ (entry) => entry.name
182
+ );
183
+
184
+ const state = yield* Ref.make<RenameState>({
185
+ completed: false,
186
+ dirRenamed: false,
187
+ catalogUpdated: false,
188
+ checkpointsUpdated: false,
189
+ lineagesUpdated: false
190
+ });
191
+
192
+ const rollback = (status: RenameState) =>
193
+ status.completed
194
+ ? Effect.void
195
+ : Effect.gen(function* () {
196
+ if (status.lineagesUpdated) {
197
+ const stores = yield* manager.listStores();
198
+ const storeNames = Chunk.toReadonlyArray(stores).map(
199
+ (entry) => entry.name
200
+ );
201
+ yield* updateLineages(to, from, storeNames).pipe(
202
+ Effect.catchAll(() => Effect.void)
203
+ );
204
+ }
205
+ if (status.checkpointsUpdated) {
206
+ const stores = yield* manager.listStores();
207
+ const storeRefs = Chunk.toReadonlyArray(stores).map(toStoreRef);
208
+ yield* updateDerivationCheckpoints(to, from, storeRefs).pipe(
209
+ Effect.catchAll(() => Effect.void)
210
+ );
211
+ }
212
+ if (status.dirRenamed) {
213
+ yield* storeDb.removeClient(to);
214
+ yield* renameDirectory(fs, path, toPath, fromPath).pipe(
215
+ Effect.catchAll(() => Effect.void)
216
+ );
217
+ }
218
+ if (status.catalogUpdated) {
219
+ yield* manager.renameStore(to, from).pipe(
220
+ Effect.catchAll(() => Effect.void)
221
+ );
222
+ }
223
+ });
224
+
225
+ const program = Effect.gen(function* () {
226
+ yield* storeDb.removeClient(from);
227
+
228
+ if (fromExists) {
229
+ yield* renameDirectory(fs, path, fromPath, toPath).pipe(
230
+ Effect.mapError(toStoreIoError(store.root))
231
+ );
232
+ yield* Ref.update(state, (current) => ({ ...current, dirRenamed: true }));
233
+ }
234
+
235
+ yield* manager.renameStore(from, to);
236
+ yield* Ref.update(state, (current) => ({
237
+ ...current,
238
+ catalogUpdated: true
239
+ }));
240
+
241
+ const storesAfter = yield* manager.listStores();
242
+ const storeRefsAfter = Chunk.toReadonlyArray(storesAfter).map(toStoreRef);
243
+
244
+ const checkpointsUpdated = yield* updateDerivationCheckpoints(
245
+ from,
246
+ to,
247
+ storeRefsAfter
248
+ );
249
+ yield* Ref.update(state, (current) => ({
250
+ ...current,
251
+ checkpointsUpdated: true
252
+ }));
253
+
254
+ const lineagesUpdated = yield* updateLineages(from, to, storeNamesBefore);
255
+ yield* Ref.update(state, (current) => ({
256
+ ...current,
257
+ lineagesUpdated: true
258
+ }));
259
+
260
+ yield* Ref.update(state, (current) => ({ ...current, completed: true }));
261
+
262
+ return {
263
+ from,
264
+ to,
265
+ moved: fromExists,
266
+ lineagesUpdated,
267
+ checkpointsUpdated
268
+ } satisfies StoreRenameResult;
269
+ });
270
+
271
+ return yield* program.pipe(
272
+ Effect.ensuring(
273
+ Ref.get(state).pipe(
274
+ Effect.flatMap(rollback),
275
+ Effect.catchAll(() => Effect.void),
276
+ Effect.uninterruptible
277
+ )
278
+ )
279
+ );
280
+ })
281
+ );
282
+
283
+ return StoreRenamer.of({ rename });
284
+ })
285
+ );
286
+ }