@mepuka/skygent 0.2.0 → 0.3.1
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/README.md +269 -31
- package/index.ts +18 -3
- package/package.json +1 -1
- package/src/cli/app.ts +4 -2
- package/src/cli/compact-output.ts +52 -0
- package/src/cli/config.ts +46 -4
- package/src/cli/doc/table-renderers.ts +29 -0
- package/src/cli/doc/thread.ts +2 -4
- package/src/cli/exit-codes.ts +2 -0
- package/src/cli/feed.ts +78 -61
- package/src/cli/filter-dsl.ts +146 -11
- package/src/cli/filter-errors.ts +13 -11
- package/src/cli/filter-help.ts +7 -0
- package/src/cli/filter-input.ts +3 -2
- package/src/cli/filter.ts +83 -5
- package/src/cli/graph.ts +297 -169
- package/src/cli/input.ts +45 -0
- package/src/cli/interval.ts +4 -33
- package/src/cli/jetstream.ts +2 -0
- package/src/cli/layers.ts +10 -0
- package/src/cli/logging.ts +8 -0
- package/src/cli/option-schemas.ts +22 -0
- package/src/cli/output-format.ts +11 -0
- package/src/cli/output-render.ts +14 -0
- package/src/cli/pagination.ts +17 -0
- package/src/cli/parse-errors.ts +30 -0
- package/src/cli/parse.ts +1 -47
- package/src/cli/pipe-input.ts +18 -0
- package/src/cli/pipe.ts +154 -0
- package/src/cli/post.ts +88 -66
- package/src/cli/query-fields.ts +13 -3
- package/src/cli/query.ts +354 -100
- package/src/cli/search.ts +93 -136
- package/src/cli/shared-options.ts +11 -63
- package/src/cli/shared.ts +1 -20
- package/src/cli/store-errors.ts +28 -21
- package/src/cli/store-tree.ts +6 -4
- package/src/cli/store.ts +41 -2
- package/src/cli/stream-merge.ts +105 -0
- package/src/cli/sync-factory.ts +24 -7
- package/src/cli/sync.ts +46 -67
- package/src/cli/thread-options.ts +25 -0
- package/src/cli/time.ts +171 -0
- package/src/cli/view-thread.ts +29 -32
- package/src/cli/watch.ts +55 -26
- package/src/domain/errors.ts +6 -1
- package/src/domain/format.ts +21 -0
- package/src/domain/order.ts +24 -0
- package/src/domain/primitives.ts +20 -3
- package/src/graph/relationships.ts +129 -0
- package/src/services/bsky-client.ts +11 -5
- package/src/services/jetstream-sync.ts +4 -4
- package/src/services/lineage-store.ts +15 -1
- package/src/services/shared.ts +48 -1
- package/src/services/store-cleaner.ts +5 -2
- package/src/services/store-commit.ts +60 -0
- package/src/services/store-manager.ts +69 -2
- package/src/services/store-renamer.ts +288 -0
- package/src/services/store-stats.ts +7 -5
- package/src/services/sync-engine.ts +149 -89
- package/src/services/sync-reporter.ts +3 -1
- package/src/services/sync-settings.ts +24 -0
|
@@ -0,0 +1,288 @@
|
|
|
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 movedOnDisk: boolean;
|
|
17
|
+
readonly lineagesUpdated: number;
|
|
18
|
+
readonly checkpointsUpdated: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type RenameState = {
|
|
22
|
+
readonly completed: boolean;
|
|
23
|
+
readonly dirRenamed: boolean;
|
|
24
|
+
readonly catalogUpdated: boolean;
|
|
25
|
+
readonly checkpointsUpdated: boolean;
|
|
26
|
+
readonly lineagesUpdated: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const storeRootKey = (name: StoreName) =>
|
|
30
|
+
Schema.decodeUnknownSync(StorePath)(`stores/${name}`);
|
|
31
|
+
|
|
32
|
+
const toStoreIoError = (path: StorePath) => (cause: unknown) =>
|
|
33
|
+
StoreIoError.make({ path, cause });
|
|
34
|
+
|
|
35
|
+
const toStoreRef = (metadata: { readonly name: StoreName; readonly root: StorePath }) =>
|
|
36
|
+
StoreRef.make({ name: metadata.name, root: metadata.root });
|
|
37
|
+
|
|
38
|
+
const renameDirectory = (
|
|
39
|
+
fs: FileSystem.FileSystem,
|
|
40
|
+
path: Path.Path,
|
|
41
|
+
fromPath: string,
|
|
42
|
+
toPath: string
|
|
43
|
+
) =>
|
|
44
|
+
Effect.gen(function* () {
|
|
45
|
+
if (fromPath === toPath) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const normalizedFrom = fromPath.toLowerCase();
|
|
49
|
+
const normalizedTo = toPath.toLowerCase();
|
|
50
|
+
if (normalizedFrom === normalizedTo) {
|
|
51
|
+
const tempName = `.rename-${Date.now()}`;
|
|
52
|
+
const tempPath = path.join(path.dirname(toPath), tempName);
|
|
53
|
+
yield* fs.rename(fromPath, tempPath);
|
|
54
|
+
yield* fs.rename(tempPath, toPath);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
yield* fs.rename(fromPath, toPath);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const renameLineage = (
|
|
61
|
+
lineage: StoreLineage,
|
|
62
|
+
from: StoreName,
|
|
63
|
+
to: StoreName
|
|
64
|
+
) => {
|
|
65
|
+
const nextStoreName = lineage.storeName === from ? to : lineage.storeName;
|
|
66
|
+
let changed = nextStoreName !== lineage.storeName;
|
|
67
|
+
const nextSources = lineage.sources.map((source) => {
|
|
68
|
+
if (source.storeName !== from) {
|
|
69
|
+
return source;
|
|
70
|
+
}
|
|
71
|
+
changed = true;
|
|
72
|
+
return StoreSource.make({ ...source, storeName: to });
|
|
73
|
+
});
|
|
74
|
+
if (!changed) {
|
|
75
|
+
return Option.none<StoreLineage>();
|
|
76
|
+
}
|
|
77
|
+
return Option.some(
|
|
78
|
+
StoreLineage.make({
|
|
79
|
+
...lineage,
|
|
80
|
+
storeName: nextStoreName,
|
|
81
|
+
sources: nextSources
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export class StoreRenamer extends Context.Tag("@skygent/StoreRenamer")<
|
|
87
|
+
StoreRenamer,
|
|
88
|
+
{
|
|
89
|
+
readonly rename: (
|
|
90
|
+
from: StoreName,
|
|
91
|
+
to: StoreName
|
|
92
|
+
) => Effect.Effect<StoreRenameResult, StoreNotFound | StoreAlreadyExists | StoreIoError>;
|
|
93
|
+
}
|
|
94
|
+
>() {
|
|
95
|
+
static readonly layer = Layer.effect(
|
|
96
|
+
StoreRenamer,
|
|
97
|
+
Effect.gen(function* () {
|
|
98
|
+
const manager = yield* StoreManager;
|
|
99
|
+
const storeDb = yield* StoreDb;
|
|
100
|
+
const lineageStore = yield* LineageStore;
|
|
101
|
+
const appConfig = yield* AppConfigService;
|
|
102
|
+
const fs = yield* FileSystem.FileSystem;
|
|
103
|
+
const path = yield* Path.Path;
|
|
104
|
+
|
|
105
|
+
const updateLineages = (
|
|
106
|
+
from: StoreName,
|
|
107
|
+
to: StoreName,
|
|
108
|
+
storeNames: ReadonlyArray<StoreName>
|
|
109
|
+
) =>
|
|
110
|
+
Effect.forEach(
|
|
111
|
+
storeNames,
|
|
112
|
+
(storeName) =>
|
|
113
|
+
lineageStore.get(storeName).pipe(
|
|
114
|
+
Effect.flatMap(
|
|
115
|
+
Option.match({
|
|
116
|
+
onNone: () => Effect.succeed(0),
|
|
117
|
+
onSome: (lineage) =>
|
|
118
|
+
Option.match(renameLineage(lineage, from, to), {
|
|
119
|
+
onNone: () => Effect.succeed(0),
|
|
120
|
+
onSome: (updated) =>
|
|
121
|
+
Effect.gen(function* () {
|
|
122
|
+
yield* lineageStore.save(updated);
|
|
123
|
+
if (lineage.storeName === from) {
|
|
124
|
+
yield* lineageStore.remove(from);
|
|
125
|
+
}
|
|
126
|
+
return 1;
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
),
|
|
132
|
+
{ discard: false }
|
|
133
|
+
).pipe(Effect.map((updates) => updates.reduce((sum, value) => sum + value, 0)));
|
|
134
|
+
|
|
135
|
+
const updateDerivationCheckpoints = (
|
|
136
|
+
from: StoreName,
|
|
137
|
+
to: StoreName,
|
|
138
|
+
stores: ReadonlyArray<StoreRef>
|
|
139
|
+
) =>
|
|
140
|
+
Effect.forEach(
|
|
141
|
+
stores,
|
|
142
|
+
(store) =>
|
|
143
|
+
storeDb.withClient(store, (client) =>
|
|
144
|
+
client`UPDATE derivation_checkpoints
|
|
145
|
+
SET view_name = CASE WHEN view_name = ${from} THEN ${to} ELSE view_name END,
|
|
146
|
+
source_store = CASE WHEN source_store = ${from} THEN ${to} ELSE source_store END,
|
|
147
|
+
target_store = CASE WHEN target_store = ${from} THEN ${to} ELSE target_store END
|
|
148
|
+
WHERE view_name = ${from}
|
|
149
|
+
OR source_store = ${from}
|
|
150
|
+
OR target_store = ${from}`.pipe(Effect.as(1))
|
|
151
|
+
).pipe(Effect.mapError(toStoreIoError(store.root))),
|
|
152
|
+
{ discard: false }
|
|
153
|
+
).pipe(Effect.map((updates) => updates.reduce((sum, value) => sum + value, 0)));
|
|
154
|
+
|
|
155
|
+
const rename = Effect.fn("StoreRenamer.rename")(
|
|
156
|
+
(from: StoreName, to: StoreName) =>
|
|
157
|
+
Effect.gen(function* () {
|
|
158
|
+
const storeOption = yield* manager.getStore(from);
|
|
159
|
+
if (Option.isNone(storeOption)) {
|
|
160
|
+
return yield* StoreNotFound.make({ name: from });
|
|
161
|
+
}
|
|
162
|
+
const targetOption = yield* manager.getStore(to);
|
|
163
|
+
if (Option.isSome(targetOption)) {
|
|
164
|
+
return yield* StoreAlreadyExists.make({ name: to });
|
|
165
|
+
}
|
|
166
|
+
const store = storeOption.value;
|
|
167
|
+
const newRoot = storeRootKey(to);
|
|
168
|
+
const fromPath = path.join(appConfig.storeRoot, store.root);
|
|
169
|
+
const toPath = path.join(appConfig.storeRoot, newRoot);
|
|
170
|
+
const fromExists = yield* fs
|
|
171
|
+
.exists(fromPath)
|
|
172
|
+
.pipe(Effect.mapError(toStoreIoError(store.root)));
|
|
173
|
+
const toExists = yield* fs
|
|
174
|
+
.exists(toPath)
|
|
175
|
+
.pipe(Effect.mapError(toStoreIoError(newRoot)));
|
|
176
|
+
if (toExists) {
|
|
177
|
+
return yield* StoreAlreadyExists.make({ name: to });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const storesBefore = yield* manager.listStores();
|
|
181
|
+
const storeNamesBefore = Chunk.toReadonlyArray(storesBefore).map(
|
|
182
|
+
(entry) => entry.name
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const state = yield* Ref.make<RenameState>({
|
|
186
|
+
completed: false,
|
|
187
|
+
dirRenamed: false,
|
|
188
|
+
catalogUpdated: false,
|
|
189
|
+
checkpointsUpdated: false,
|
|
190
|
+
lineagesUpdated: false
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const rollback = (status: RenameState) =>
|
|
194
|
+
status.completed
|
|
195
|
+
? Effect.void
|
|
196
|
+
: Effect.gen(function* () {
|
|
197
|
+
if (status.lineagesUpdated) {
|
|
198
|
+
const stores = yield* manager.listStores();
|
|
199
|
+
const storeNames = Chunk.toReadonlyArray(stores).map(
|
|
200
|
+
(entry) => entry.name
|
|
201
|
+
);
|
|
202
|
+
yield* updateLineages(to, from, storeNames).pipe(
|
|
203
|
+
Effect.catchAll(() => Effect.void)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (status.checkpointsUpdated) {
|
|
207
|
+
const stores = yield* manager.listStores();
|
|
208
|
+
const storeRefs = Chunk.toReadonlyArray(stores).map(toStoreRef);
|
|
209
|
+
yield* updateDerivationCheckpoints(to, from, storeRefs).pipe(
|
|
210
|
+
Effect.catchAll(() => Effect.void)
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (status.dirRenamed) {
|
|
214
|
+
yield* storeDb.removeClient(to);
|
|
215
|
+
yield* renameDirectory(fs, path, toPath, fromPath).pipe(
|
|
216
|
+
Effect.catchAll(() => Effect.void)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (status.catalogUpdated) {
|
|
220
|
+
yield* manager.renameStore(to, from).pipe(
|
|
221
|
+
Effect.catchAll(() => Effect.void)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const program = Effect.gen(function* () {
|
|
227
|
+
yield* storeDb.removeClient(from);
|
|
228
|
+
|
|
229
|
+
if (fromExists) {
|
|
230
|
+
yield* renameDirectory(fs, path, fromPath, toPath).pipe(
|
|
231
|
+
Effect.mapError(toStoreIoError(store.root))
|
|
232
|
+
);
|
|
233
|
+
yield* Ref.update(state, (current) => ({ ...current, dirRenamed: true }));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
yield* manager.renameStore(from, to);
|
|
237
|
+
yield* Ref.update(state, (current) => ({
|
|
238
|
+
...current,
|
|
239
|
+
catalogUpdated: true
|
|
240
|
+
}));
|
|
241
|
+
|
|
242
|
+
const storesAfter = yield* manager.listStores();
|
|
243
|
+
const storeRefsAfter = Chunk.toReadonlyArray(storesAfter).map(toStoreRef);
|
|
244
|
+
|
|
245
|
+
const checkpointsUpdated = yield* updateDerivationCheckpoints(
|
|
246
|
+
from,
|
|
247
|
+
to,
|
|
248
|
+
storeRefsAfter
|
|
249
|
+
);
|
|
250
|
+
yield* Ref.update(state, (current) => ({
|
|
251
|
+
...current,
|
|
252
|
+
checkpointsUpdated: true
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
const lineagesUpdated = yield* updateLineages(from, to, storeNamesBefore);
|
|
256
|
+
yield* Ref.update(state, (current) => ({
|
|
257
|
+
...current,
|
|
258
|
+
lineagesUpdated: true
|
|
259
|
+
}));
|
|
260
|
+
|
|
261
|
+
yield* Ref.update(state, (current) => ({ ...current, completed: true }));
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
from,
|
|
265
|
+
to,
|
|
266
|
+
moved: true,
|
|
267
|
+
movedOnDisk: fromExists,
|
|
268
|
+
lineagesUpdated,
|
|
269
|
+
checkpointsUpdated
|
|
270
|
+
} satisfies StoreRenameResult;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return yield* program.pipe(
|
|
274
|
+
Effect.ensuring(
|
|
275
|
+
Ref.get(state).pipe(
|
|
276
|
+
Effect.flatMap(rollback),
|
|
277
|
+
Effect.catchAll(() => Effect.void),
|
|
278
|
+
Effect.uninterruptible
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
return StoreRenamer.of({ rename });
|
|
286
|
+
})
|
|
287
|
+
);
|
|
288
|
+
}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
import { FileSystem, Path } from "@effect/platform";
|
|
40
40
|
import { directorySize } from "./shared.js";
|
|
41
|
-
import { Context, Effect, Layer, Option } from "effect";
|
|
41
|
+
import { Context, Effect, Layer, Option, Order } from "effect";
|
|
42
42
|
import { AppConfigService } from "./app-config.js";
|
|
43
43
|
import { StoreManager } from "./store-manager.js";
|
|
44
44
|
import { StoreIndex } from "./store-index.js";
|
|
@@ -47,11 +47,12 @@ import { LineageStore } from "./lineage-store.js";
|
|
|
47
47
|
import { DerivationValidator } from "./derivation-validator.js";
|
|
48
48
|
import { StoreEventLog } from "./store-event-log.js";
|
|
49
49
|
import { SyncCheckpointStore } from "./sync-checkpoint-store.js";
|
|
50
|
-
import { DataSource } from "../domain/sync.js";
|
|
50
|
+
import { DataSource, type SyncCheckpoint } from "../domain/sync.js";
|
|
51
51
|
import { StoreName, type StorePath } from "../domain/primitives.js";
|
|
52
52
|
import { StoreRef } from "../domain/store.js";
|
|
53
53
|
import type { StoreLineage } from "../domain/derivation.js";
|
|
54
54
|
import { StoreIoError, type StoreIndexError } from "../domain/errors.js";
|
|
55
|
+
import { updatedAtOrder } from "../domain/order.js";
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* Detailed statistics for a single store.
|
|
@@ -257,9 +258,10 @@ const resolveSyncStatus = (
|
|
|
257
258
|
if (candidates.length === 0) {
|
|
258
259
|
return "unknown" as const;
|
|
259
260
|
}
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
const checkpointOrder = updatedAtOrder<SyncCheckpoint>();
|
|
262
|
+
const latest = candidates.reduce((acc, candidate) =>
|
|
263
|
+
Order.max(checkpointOrder)(acc, candidate)
|
|
264
|
+
);
|
|
263
265
|
if (!latest || !latest.lastEventSeq) {
|
|
264
266
|
return "stale" as const;
|
|
265
267
|
}
|