@mepuka/skygent 0.2.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 (114) hide show
  1. package/README.md +59 -0
  2. package/index.ts +146 -0
  3. package/package.json +56 -0
  4. package/src/cli/app.ts +75 -0
  5. package/src/cli/config-command.ts +140 -0
  6. package/src/cli/config.ts +91 -0
  7. package/src/cli/derive.ts +205 -0
  8. package/src/cli/doc/annotation.ts +36 -0
  9. package/src/cli/doc/filter.ts +69 -0
  10. package/src/cli/doc/index.ts +9 -0
  11. package/src/cli/doc/post.ts +155 -0
  12. package/src/cli/doc/primitives.ts +25 -0
  13. package/src/cli/doc/render.ts +18 -0
  14. package/src/cli/doc/table.ts +114 -0
  15. package/src/cli/doc/thread.ts +46 -0
  16. package/src/cli/doc/tree.ts +126 -0
  17. package/src/cli/errors.ts +59 -0
  18. package/src/cli/exit-codes.ts +52 -0
  19. package/src/cli/feed.ts +177 -0
  20. package/src/cli/filter-dsl.ts +1411 -0
  21. package/src/cli/filter-errors.ts +208 -0
  22. package/src/cli/filter-help.ts +70 -0
  23. package/src/cli/filter-input.ts +54 -0
  24. package/src/cli/filter.ts +435 -0
  25. package/src/cli/graph.ts +472 -0
  26. package/src/cli/help.ts +14 -0
  27. package/src/cli/interval.ts +35 -0
  28. package/src/cli/jetstream.ts +173 -0
  29. package/src/cli/layers.ts +180 -0
  30. package/src/cli/logging.ts +136 -0
  31. package/src/cli/output-format.ts +26 -0
  32. package/src/cli/output.ts +82 -0
  33. package/src/cli/parse.ts +80 -0
  34. package/src/cli/post.ts +193 -0
  35. package/src/cli/preferences.ts +11 -0
  36. package/src/cli/query-fields.ts +247 -0
  37. package/src/cli/query.ts +415 -0
  38. package/src/cli/range.ts +44 -0
  39. package/src/cli/search.ts +465 -0
  40. package/src/cli/shared-options.ts +169 -0
  41. package/src/cli/shared.ts +20 -0
  42. package/src/cli/store-errors.ts +80 -0
  43. package/src/cli/store-tree.ts +392 -0
  44. package/src/cli/store.ts +395 -0
  45. package/src/cli/sync-factory.ts +107 -0
  46. package/src/cli/sync.ts +366 -0
  47. package/src/cli/view-thread.ts +196 -0
  48. package/src/cli/view.ts +47 -0
  49. package/src/cli/watch.ts +344 -0
  50. package/src/db/migrations/store-catalog/001_init.ts +14 -0
  51. package/src/db/migrations/store-index/001_init.ts +34 -0
  52. package/src/db/migrations/store-index/002_event_log.ts +24 -0
  53. package/src/db/migrations/store-index/003_fts_and_derived.ts +52 -0
  54. package/src/db/migrations/store-index/004_query_indexes.ts +9 -0
  55. package/src/db/migrations/store-index/005_post_lang.ts +15 -0
  56. package/src/db/migrations/store-index/006_has_embed.ts +10 -0
  57. package/src/db/migrations/store-index/007_event_seq_and_checkpoints.ts +68 -0
  58. package/src/domain/bsky.ts +467 -0
  59. package/src/domain/config.ts +11 -0
  60. package/src/domain/credentials.ts +6 -0
  61. package/src/domain/defaults.ts +8 -0
  62. package/src/domain/derivation.ts +55 -0
  63. package/src/domain/errors.ts +71 -0
  64. package/src/domain/events.ts +55 -0
  65. package/src/domain/extract.ts +64 -0
  66. package/src/domain/filter-describe.ts +551 -0
  67. package/src/domain/filter-explain.ts +9 -0
  68. package/src/domain/filter.ts +797 -0
  69. package/src/domain/format.ts +91 -0
  70. package/src/domain/index.ts +13 -0
  71. package/src/domain/indexes.ts +17 -0
  72. package/src/domain/policies.ts +16 -0
  73. package/src/domain/post.ts +88 -0
  74. package/src/domain/primitives.ts +50 -0
  75. package/src/domain/raw.ts +140 -0
  76. package/src/domain/store.ts +103 -0
  77. package/src/domain/sync.ts +211 -0
  78. package/src/domain/text-width.ts +56 -0
  79. package/src/services/app-config.ts +278 -0
  80. package/src/services/bsky-client.ts +2113 -0
  81. package/src/services/credential-store.ts +408 -0
  82. package/src/services/derivation-engine.ts +502 -0
  83. package/src/services/derivation-settings.ts +61 -0
  84. package/src/services/derivation-validator.ts +68 -0
  85. package/src/services/filter-compiler.ts +269 -0
  86. package/src/services/filter-library.ts +371 -0
  87. package/src/services/filter-runtime.ts +821 -0
  88. package/src/services/filter-settings.ts +30 -0
  89. package/src/services/identity-resolver.ts +563 -0
  90. package/src/services/jetstream-sync.ts +636 -0
  91. package/src/services/lineage-store.ts +89 -0
  92. package/src/services/link-validator.ts +244 -0
  93. package/src/services/output-manager.ts +274 -0
  94. package/src/services/post-parser.ts +62 -0
  95. package/src/services/profile-resolver.ts +223 -0
  96. package/src/services/resource-monitor.ts +106 -0
  97. package/src/services/shared.ts +69 -0
  98. package/src/services/store-cleaner.ts +43 -0
  99. package/src/services/store-commit.ts +168 -0
  100. package/src/services/store-db.ts +248 -0
  101. package/src/services/store-event-log.ts +285 -0
  102. package/src/services/store-index-sql.ts +289 -0
  103. package/src/services/store-index.ts +1152 -0
  104. package/src/services/store-keys.ts +4 -0
  105. package/src/services/store-manager.ts +358 -0
  106. package/src/services/store-stats.ts +522 -0
  107. package/src/services/store-writer.ts +200 -0
  108. package/src/services/sync-checkpoint-store.ts +169 -0
  109. package/src/services/sync-engine.ts +547 -0
  110. package/src/services/sync-reporter.ts +16 -0
  111. package/src/services/sync-settings.ts +72 -0
  112. package/src/services/trending-topics.ts +226 -0
  113. package/src/services/view-checkpoint-store.ts +238 -0
  114. package/src/typeclass/chunk.ts +84 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Trending Topics Service
3
+ *
4
+ * Provides functionality to check if hashtags are trending on Bluesky.
5
+ * Uses the Bluesky API to fetch trending topics and caches results for 15 minutes
6
+ * to reduce API calls and improve performance.
7
+ *
8
+ * This service is primarily used by the Trending filter to determine if posts
9
+ * contain hashtags that are currently trending.
10
+ *
11
+ * @module services/trending-topics
12
+ * @example
13
+ * ```ts
14
+ * import { Effect } from "effect";
15
+ * import { TrendingTopics } from "./services/trending-topics.js";
16
+ * import { Hashtag } from "./domain/primitives.js";
17
+ *
18
+ * const program = Effect.gen(function* () {
19
+ * const trending = yield* TrendingTopics;
20
+ *
21
+ * // Get all trending topics
22
+ * const topics = yield* trending.getTopics();
23
+ * console.log("Trending topics:", topics);
24
+ *
25
+ * // Check if a specific hashtag is trending
26
+ * const isTrending = yield* trending.isTrending(Hashtag.make("effect"));
27
+ * console.log("Is #effect trending?", isTrending);
28
+ * });
29
+ * ```
30
+ */
31
+
32
+ import * as KeyValueStore from "@effect/platform/KeyValueStore";
33
+ import {
34
+ Clock,
35
+ Context,
36
+ Duration,
37
+ Effect,
38
+ Layer,
39
+ Option,
40
+ Schema
41
+ } from "effect";
42
+ import { FilterEvalError } from "../domain/errors.js";
43
+ import { Hashtag } from "../domain/primitives.js";
44
+ import { BskyClient } from "./bsky-client.js";
45
+
46
+ /** Cache key prefix for trending topics storage */
47
+ const cachePrefix = "cache/trending/";
48
+
49
+ /** Cache key for storing trending topics */
50
+ const cacheKey = "topics";
51
+
52
+ /** Cache TTL - trending topics are cached for 15 minutes */
53
+ const cacheTtl = Duration.minutes(15);
54
+
55
+ /**
56
+ * Converts an error to a FilterEvalError with the given message.
57
+ * @param message - Error message describing the failure context
58
+ * @returns A function that takes a cause and creates a FilterEvalError
59
+ */
60
+ const toFilterEvalError = (message: string) => (cause: unknown) =>
61
+ FilterEvalError.make({ message, cause });
62
+
63
+ /**
64
+ * Normalizes a topic string for comparison by trimming whitespace,
65
+ * converting to lowercase, and removing the leading '#' if present.
66
+ * @param topic - The topic string to normalize
67
+ * @returns Normalized topic string
68
+ * @example
69
+ * ```ts
70
+ * normalizeTopic("#Effect") // returns "effect"
71
+ * normalizeTopic(" BSKY ") // returns "bsky"
72
+ * ```
73
+ */
74
+ const normalizeTopic = (topic: string) =>
75
+ topic.trim().toLowerCase().replace(/^#/, "");
76
+
77
+ /**
78
+ * Schema for cached trending topics data.
79
+ * Stores the list of topics and when they were fetched.
80
+ */
81
+ class TrendingCacheEntry extends Schema.Class<TrendingCacheEntry>("TrendingCacheEntry")({
82
+ /** Array of trending topic strings */
83
+ topics: Schema.Array(Schema.String),
84
+ /** Timestamp when the cache entry was created */
85
+ checkedAt: Schema.DateFromString
86
+ }) {}
87
+
88
+ /**
89
+ * Interface for the Trending Topics service.
90
+ * Provides methods to fetch and check trending topics on Bluesky.
91
+ */
92
+ export type TrendingTopicsService = {
93
+ /**
94
+ * Fetches the current trending topics from Bluesky.
95
+ * Uses a 15-minute cache to avoid excessive API calls.
96
+ * @returns An Effect that resolves to an array of trending topic strings
97
+ * @throws {FilterEvalError} If fetching topics fails
98
+ */
99
+ readonly getTopics: () => Effect.Effect<ReadonlyArray<string>, FilterEvalError>;
100
+
101
+ /**
102
+ * Checks if a specific hashtag is currently trending.
103
+ * @param tag - The hashtag to check
104
+ * @returns An Effect that resolves to true if the hashtag is trending, false otherwise
105
+ * @throws {FilterEvalError} If checking trending status fails
106
+ * @example
107
+ * ```ts
108
+ * const isTrending = yield* trending.isTrending(Hashtag.make("typescript"));
109
+ * ```
110
+ */
111
+ readonly isTrending: (tag: Hashtag) => Effect.Effect<boolean, FilterEvalError>;
112
+ };
113
+
114
+ /**
115
+ * Effect Context Tag for the Trending Topics service.
116
+ * Provides trending topic functionality with caching for Bluesky hashtags.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * // Use in an Effect program
121
+ * const program = Effect.gen(function* () {
122
+ * const trending = yield* TrendingTopics;
123
+ * const topics = yield* trending.getTopics();
124
+ * });
125
+ *
126
+ * // Provide the layer
127
+ * const runnable = program.pipe(Effect.provide(TrendingTopics.layer));
128
+ * ```
129
+ */
130
+ export class TrendingTopics extends Context.Tag("@skygent/TrendingTopics")<
131
+ TrendingTopics,
132
+ TrendingTopicsService
133
+ >() {
134
+ /**
135
+ * Production layer that provides the TrendingTopics service.
136
+ * Requires KeyValueStore and BskyClient to be provided.
137
+ *
138
+ * The implementation:
139
+ * - Fetches trending topics from Bluesky API
140
+ * - Caches results for 15 minutes
141
+ * - Normalizes topic names for case-insensitive comparison
142
+ */
143
+ static readonly layer = Layer.effect(
144
+ TrendingTopics,
145
+ Effect.gen(function* () {
146
+ const kv = yield* KeyValueStore.KeyValueStore;
147
+ const bsky = yield* BskyClient;
148
+ const store = KeyValueStore.prefix(
149
+ kv.forSchema(TrendingCacheEntry),
150
+ cachePrefix
151
+ );
152
+
153
+ /**
154
+ * Fetches trending topics from Bluesky API.
155
+ * Wrapped with error mapping for consistent error handling.
156
+ */
157
+ const fetchTopics = bsky.getTrendingTopics().pipe(
158
+ Effect.mapError(toFilterEvalError("Trending topics fetch failed"))
159
+ );
160
+
161
+ /**
162
+ * Checks if a cache entry is still fresh (within TTL).
163
+ * @param entry - The cached trending topics entry
164
+ * @param now - Current timestamp in milliseconds
165
+ * @returns True if the entry is still fresh
166
+ */
167
+ const isFresh = (entry: TrendingCacheEntry, now: number) =>
168
+ now - entry.checkedAt.getTime() < Duration.toMillis(cacheTtl);
169
+
170
+ /**
171
+ * Gets trending topics, using cache if available and fresh.
172
+ * Fetches fresh data if cache is expired or missing.
173
+ */
174
+ const getTopics = Effect.fn("TrendingTopics.getTopics")(() =>
175
+ Effect.gen(function* () {
176
+ const cached = yield* store
177
+ .get(cacheKey)
178
+ .pipe(Effect.mapError(toFilterEvalError("Trending cache read failed")));
179
+
180
+ const now = yield* Clock.currentTimeMillis;
181
+ if (Option.isSome(cached) && isFresh(cached.value, now)) {
182
+ return cached.value.topics;
183
+ }
184
+
185
+ const topics = yield* fetchTopics;
186
+ const entry = TrendingCacheEntry.make({
187
+ topics,
188
+ checkedAt: new Date(now)
189
+ });
190
+ yield* store
191
+ .set(cacheKey, entry)
192
+ .pipe(Effect.mapError(toFilterEvalError("Trending cache write failed")));
193
+
194
+ return topics;
195
+ })
196
+ );
197
+
198
+ /**
199
+ * Checks if a specific hashtag is in the trending topics list.
200
+ * Uses normalized comparison for case-insensitive matching.
201
+ */
202
+ const isTrending = Effect.fn("TrendingTopics.isTrending")((tag: Hashtag) =>
203
+ getTopics().pipe(
204
+ Effect.map((topics) =>
205
+ topics.includes(normalizeTopic(String(tag)))
206
+ )
207
+ )
208
+ );
209
+
210
+ return TrendingTopics.of({ getTopics, isTrending });
211
+ })
212
+ );
213
+
214
+ /**
215
+ * Test layer with mock trending topics for testing.
216
+ * Returns static data: ["effect", "bsky"]
217
+ */
218
+ static readonly testLayer = Layer.succeed(
219
+ TrendingTopics,
220
+ TrendingTopics.of({
221
+ getTopics: () => Effect.succeed(["effect", "bsky"]),
222
+ isTrending: (tag) =>
223
+ Effect.succeed(["effect", "bsky"].includes(normalizeTopic(String(tag))))
224
+ })
225
+ );
226
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * View Checkpoint Store Service
3
+ *
4
+ * Manages checkpoints for store derivation (view) operations. Tracks the last
5
+ * processed event from a source store to enable incremental derivation.
6
+ *
7
+ * Unlike sync checkpoints which track data sources, view checkpoints track
8
+ * the processing progress when deriving one store from another, allowing
9
+ * incremental updates rather than full re-derivation.
10
+ *
11
+ * @module services/view-checkpoint-store
12
+ */
13
+
14
+ import { Context, Effect, Layer, Option, Schema } from "effect";
15
+ import { DerivationCheckpoint, FilterEvaluationMode } from "../domain/derivation.js";
16
+ import { EventSeq, StoreName, StorePath, Timestamp } from "../domain/primitives.js";
17
+ import { StoreIoError } from "../domain/errors.js";
18
+ import { StoreDb } from "./store-db.js";
19
+ import { StoreManager } from "./store-manager.js";
20
+
21
+ /**
22
+ * Converts an error to a StoreIoError with context for the checkpoint path.
23
+ * @param viewName - The name of the derived store
24
+ * @param sourceName - The name of the source store
25
+ * @returns A function that creates StoreIoError from any cause
26
+ */
27
+ const toStoreIoError = (viewName: StoreName, sourceName: StoreName) => (cause: unknown) => {
28
+ const path = Schema.decodeUnknownSync(StorePath)(
29
+ `stores/${viewName}/checkpoints/derivation/${sourceName}`
30
+ );
31
+ return StoreIoError.make({ path, cause });
32
+ };
33
+
34
+ const checkpointRow = Schema.Struct({
35
+ view_name: StoreName,
36
+ source_store: StoreName,
37
+ target_store: StoreName,
38
+ filter_hash: Schema.String,
39
+ evaluation_mode: FilterEvaluationMode,
40
+ last_source_event_seq: Schema.NullOr(EventSeq),
41
+ events_processed: Schema.NonNegativeInt,
42
+ events_matched: Schema.NonNegativeInt,
43
+ deletes_propagated: Schema.NonNegativeInt,
44
+ updated_at: Schema.String
45
+ });
46
+
47
+ /**
48
+ * Service for managing derivation (view) checkpoints.
49
+ *
50
+ * This service tracks the processing progress when deriving one store from
51
+ * another, storing checkpoints that record which events from the source store
52
+ * have been processed. This enables efficient incremental derivation.
53
+ */
54
+ export class ViewCheckpointStore extends Context.Tag("@skygent/ViewCheckpointStore")<
55
+ ViewCheckpointStore,
56
+ {
57
+ /**
58
+ * Loads the derivation checkpoint for a view and its source store.
59
+ *
60
+ * @param viewName - The name of the derived store
61
+ * @param sourceName - The name of the source store
62
+ * @returns Effect resolving to Option of DerivationCheckpoint, or StoreIoError on failure
63
+ */
64
+ readonly load: (
65
+ viewName: StoreName,
66
+ sourceName: StoreName
67
+ ) => Effect.Effect<Option.Option<DerivationCheckpoint>, StoreIoError>;
68
+
69
+ /**
70
+ * Saves a derivation checkpoint.
71
+ *
72
+ * @param checkpoint - The checkpoint data to persist
73
+ * @returns Effect resolving to void, or StoreIoError on failure
74
+ */
75
+ readonly save: (
76
+ checkpoint: DerivationCheckpoint
77
+ ) => Effect.Effect<void, StoreIoError>;
78
+
79
+ /**
80
+ * Removes a derivation checkpoint.
81
+ *
82
+ * @param viewName - The name of the derived store
83
+ * @param sourceName - The name of the source store
84
+ * @returns Effect resolving to void, or StoreIoError on failure
85
+ */
86
+ readonly remove: (
87
+ viewName: StoreName,
88
+ sourceName: StoreName
89
+ ) => Effect.Effect<void, StoreIoError>;
90
+ }
91
+ >() {
92
+ static readonly layer = Layer.effect(
93
+ ViewCheckpointStore,
94
+ Effect.gen(function* () {
95
+ const storeDb = yield* StoreDb;
96
+ const manager = yield* StoreManager;
97
+
98
+ const resolveStore = (viewName: StoreName, sourceName: StoreName) =>
99
+ manager
100
+ .getStore(viewName)
101
+ .pipe(Effect.mapError(toStoreIoError(viewName, sourceName)));
102
+
103
+ const load = Effect.fn("ViewCheckpointStore.load")(
104
+ (viewName: StoreName, sourceName: StoreName) =>
105
+ resolveStore(viewName, sourceName).pipe(
106
+ Effect.flatMap(
107
+ Option.match({
108
+ onNone: () => Effect.succeed(Option.none<DerivationCheckpoint>()),
109
+ onSome: (storeRef) =>
110
+ storeDb.withClient(storeRef, (client) =>
111
+ Effect.gen(function* () {
112
+ const rows = yield* client`SELECT
113
+ view_name,
114
+ source_store,
115
+ target_store,
116
+ filter_hash,
117
+ evaluation_mode,
118
+ last_source_event_seq,
119
+ events_processed,
120
+ events_matched,
121
+ deletes_propagated,
122
+ updated_at
123
+ FROM derivation_checkpoints
124
+ WHERE view_name = ${viewName} AND source_store = ${sourceName}`;
125
+ if (rows.length === 0) {
126
+ return Option.none<DerivationCheckpoint>();
127
+ }
128
+ const decoded = yield* Schema.decodeUnknown(
129
+ Schema.Array(checkpointRow)
130
+ )(rows);
131
+ const row = decoded[0]!;
132
+ const updatedAt = yield* Schema.decodeUnknown(Timestamp)(row.updated_at);
133
+ return Option.some(
134
+ DerivationCheckpoint.make({
135
+ viewName: row.view_name,
136
+ sourceStore: row.source_store,
137
+ targetStore: row.target_store,
138
+ filterHash: row.filter_hash,
139
+ evaluationMode: row.evaluation_mode,
140
+ lastSourceEventSeq: row.last_source_event_seq ?? undefined,
141
+ eventsProcessed: row.events_processed,
142
+ eventsMatched: row.events_matched,
143
+ deletesPropagated: row.deletes_propagated,
144
+ updatedAt
145
+ })
146
+ );
147
+ })
148
+ )
149
+ })
150
+ ),
151
+ Effect.mapError(toStoreIoError(viewName, sourceName))
152
+ )
153
+ );
154
+
155
+ const save = Effect.fn("ViewCheckpointStore.save")(
156
+ (checkpoint: DerivationCheckpoint) =>
157
+ resolveStore(checkpoint.viewName, checkpoint.sourceStore).pipe(
158
+ Effect.flatMap(
159
+ Option.match({
160
+ onNone: () =>
161
+ Effect.fail(
162
+ toStoreIoError(checkpoint.viewName, checkpoint.sourceStore)(
163
+ new Error(`Store "${checkpoint.viewName}" not found`)
164
+ )
165
+ ),
166
+ onSome: (storeRef) =>
167
+ storeDb.withClient(storeRef, (client) =>
168
+ Effect.gen(function* () {
169
+ const updatedAt = checkpoint.updatedAt.toISOString();
170
+ yield* client`INSERT INTO derivation_checkpoints (
171
+ view_name,
172
+ source_store,
173
+ target_store,
174
+ filter_hash,
175
+ evaluation_mode,
176
+ last_source_event_seq,
177
+ events_processed,
178
+ events_matched,
179
+ deletes_propagated,
180
+ updated_at
181
+ )
182
+ VALUES (
183
+ ${checkpoint.viewName},
184
+ ${checkpoint.sourceStore},
185
+ ${checkpoint.targetStore},
186
+ ${checkpoint.filterHash},
187
+ ${checkpoint.evaluationMode},
188
+ ${checkpoint.lastSourceEventSeq ?? null},
189
+ ${checkpoint.eventsProcessed},
190
+ ${checkpoint.eventsMatched},
191
+ ${checkpoint.deletesPropagated},
192
+ ${updatedAt}
193
+ )
194
+ ON CONFLICT(view_name, source_store) DO UPDATE SET
195
+ target_store = excluded.target_store,
196
+ filter_hash = excluded.filter_hash,
197
+ evaluation_mode = excluded.evaluation_mode,
198
+ last_source_event_seq = CASE
199
+ WHEN excluded.last_source_event_seq IS NULL THEN derivation_checkpoints.last_source_event_seq
200
+ WHEN derivation_checkpoints.last_source_event_seq IS NULL THEN excluded.last_source_event_seq
201
+ WHEN excluded.last_source_event_seq >= derivation_checkpoints.last_source_event_seq THEN excluded.last_source_event_seq
202
+ ELSE derivation_checkpoints.last_source_event_seq
203
+ END,
204
+ events_processed = excluded.events_processed,
205
+ events_matched = excluded.events_matched,
206
+ deletes_propagated = excluded.deletes_propagated,
207
+ updated_at = excluded.updated_at`;
208
+ })
209
+ )
210
+ })
211
+ ),
212
+ Effect.mapError(toStoreIoError(checkpoint.viewName, checkpoint.sourceStore))
213
+ )
214
+ );
215
+
216
+ const remove = Effect.fn("ViewCheckpointStore.remove")(
217
+ (viewName: StoreName, sourceName: StoreName) =>
218
+ resolveStore(viewName, sourceName).pipe(
219
+ Effect.flatMap(
220
+ Option.match({
221
+ onNone: () => Effect.void,
222
+ onSome: (storeRef) =>
223
+ storeDb.withClient(storeRef, (client) =>
224
+ client`DELETE FROM derivation_checkpoints
225
+ WHERE view_name = ${viewName} AND source_store = ${sourceName}`.pipe(
226
+ Effect.asVoid
227
+ )
228
+ )
229
+ })
230
+ ),
231
+ Effect.mapError(toStoreIoError(viewName, sourceName))
232
+ )
233
+ );
234
+
235
+ return ViewCheckpointStore.of({ load, save, remove });
236
+ })
237
+ );
238
+ }
@@ -0,0 +1,84 @@
1
+ import type { Applicative } from "@effect/typeclass/Applicative";
2
+ import { getApplicative, type ConcurrencyOptions } from "@effect/typeclass/data/Effect";
3
+ import type * as Filterable from "@effect/typeclass/Filterable";
4
+ import type * as Traversable from "@effect/typeclass/Traversable";
5
+ import type * as TraversableFilterable from "@effect/typeclass/TraversableFilterable";
6
+ import { Chunk, Effect, Option } from "effect";
7
+ import type { ChunkTypeLambda } from "effect/Chunk";
8
+ import { dual } from "effect/Function";
9
+ import type { Kind, TypeLambda } from "effect/HKT";
10
+ import type { Either } from "effect/Either";
11
+
12
+ export const ChunkFilterable: Filterable.Filterable<ChunkTypeLambda> = {
13
+ partitionMap: Chunk.partitionMap,
14
+ filterMap: Chunk.filterMap
15
+ };
16
+
17
+ const traverse = <F extends TypeLambda>(F: Applicative<F>) =>
18
+ dual(
19
+ 2,
20
+ <A, R, O, E, B>(
21
+ self: Chunk.Chunk<A>,
22
+ f: (a: A) => Kind<F, R, O, E, B>
23
+ ): Kind<F, R, O, E, Chunk.Chunk<B>> =>
24
+ F.map(
25
+ F.productAll(Chunk.toReadonlyArray(self).map(f)),
26
+ Chunk.fromIterable
27
+ )
28
+ );
29
+
30
+ export const ChunkTraversable: Traversable.Traversable<ChunkTypeLambda> = {
31
+ traverse
32
+ };
33
+
34
+ const traversePartitionMap = <F extends TypeLambda>(F: Applicative<F>) =>
35
+ dual(
36
+ 2,
37
+ <A, R, O, E, B, C>(
38
+ self: Chunk.Chunk<A>,
39
+ f: (a: A) => Kind<F, R, O, E, Either<C, B>>
40
+ ): Kind<F, R, O, E, [Chunk.Chunk<B>, Chunk.Chunk<C>]> =>
41
+ F.map(
42
+ traverse(F)(self, f) as Kind<F, R, O, E, Chunk.Chunk<Either<C, B>>>,
43
+ (chunk) => Chunk.separate(chunk)
44
+ )
45
+ );
46
+
47
+ const traverseFilterMap = <F extends TypeLambda>(F: Applicative<F>) =>
48
+ dual(
49
+ 2,
50
+ <A, R, O, E, B>(
51
+ self: Chunk.Chunk<A>,
52
+ f: (a: A) => Kind<F, R, O, E, Option.Option<B>>
53
+ ): Kind<F, R, O, E, Chunk.Chunk<B>> =>
54
+ F.map(
55
+ traverse(F)(self, f) as Kind<F, R, O, E, Chunk.Chunk<Option.Option<B>>>,
56
+ (chunk) => Chunk.compact(chunk)
57
+ )
58
+ );
59
+
60
+ export const ChunkTraversableFilterable: TraversableFilterable.TraversableFilterable<ChunkTypeLambda> = {
61
+ traversePartitionMap,
62
+ traverseFilterMap
63
+ };
64
+
65
+ export const traverseFilterEffect = <A, E, R>(
66
+ items: Chunk.Chunk<A>,
67
+ predicate: (item: A) => Effect.Effect<boolean, E, R>,
68
+ options?: ConcurrencyOptions
69
+ ): Effect.Effect<Chunk.Chunk<A>, E, R> =>
70
+ ChunkTraversableFilterable.traverseFilterMap(getApplicative(options))(
71
+ items,
72
+ (item) =>
73
+ Effect.map(predicate(item), (keep) =>
74
+ keep ? Option.some(item) : Option.none()
75
+ )
76
+ );
77
+
78
+ export const filterByFlags = <A>(
79
+ items: Chunk.Chunk<A>,
80
+ flags: Chunk.Chunk<boolean>
81
+ ): Chunk.Chunk<A> =>
82
+ ChunkFilterable.filterMap(Chunk.zip(items, flags), (tuple) =>
83
+ tuple[1] ? Option.some(tuple[0]) : Option.none()
84
+ );