@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,248 @@
1
+ import { FileSystem, Path } from "@effect/platform";
2
+ import { Context, Effect, Exit, Layer, Ref, Scope } from "effect";
3
+ import * as Reactivity from "@effect/experimental/Reactivity";
4
+ import * as Migrator from "@effect/sql/Migrator";
5
+ import * as MigratorFileSystem from "@effect/sql/Migrator/FileSystem";
6
+ import * as SqlClient from "@effect/sql/SqlClient";
7
+ import { SqliteClient } from "@effect/sql-sqlite-bun";
8
+ import { StoreIoError } from "../domain/errors.js";
9
+ import type { StoreRef } from "../domain/store.js";
10
+ import type { StorePath } from "../domain/primitives.js";
11
+ import { AppConfigService } from "./app-config.js";
12
+
13
+ /**
14
+ * Store Database Connection Management Service
15
+ *
16
+ * This module provides a centralized SQLite database connection manager for store indexes.
17
+ * It implements a connection caching/pooling pattern to efficiently manage database connections
18
+ * across multiple stores while ensuring proper resource cleanup.
19
+ *
20
+ * Key features:
21
+ * - Connection caching: Reuses existing connections to the same store
22
+ * - Thread-safe client access using semaphores for concurrent operations
23
+ * - Automatic migration execution on first connection to each store
24
+ * - Optimized SQLite pragmas for performance (WAL mode, memory-mapped I/O, cache sizing)
25
+ * - Graceful cleanup on service shutdown via finalizers
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * import { Effect } from "effect";
30
+ * import { StoreDb } from "./services/store-db.js";
31
+ * import type { StoreRef } from "./domain/store.js";
32
+ *
33
+ * const program = Effect.gen(function* () {
34
+ * const storeDb = yield* StoreDb;
35
+ * const store: StoreRef = { name: "myStore", root: "stores/myStore" };
36
+ *
37
+ * // Execute queries with automatic connection management
38
+ * const result = yield* storeDb.withClient(store, (client) =>
39
+ * client`SELECT * FROM posts WHERE id = ${postId}`
40
+ * );
41
+ *
42
+ * // Remove a store's connection when done
43
+ * yield* storeDb.removeClient(store.name);
44
+ * });
45
+ *
46
+ * const runnable = program.pipe(Effect.provide(StoreDb.layer));
47
+ * ```
48
+ *
49
+ * @module services/store-db
50
+ */
51
+
52
+ const migrationsDir = decodeURIComponent(
53
+ new URL("../db/migrations/store-index", import.meta.url).pathname
54
+ );
55
+
56
+ const toStoreIoError = (path: StorePath) => (cause: unknown) =>
57
+ StoreIoError.make({ path, cause });
58
+
59
+ /**
60
+ * Service for managing SQLite database connections to store indexes.
61
+ *
62
+ * Provides connection pooling/caching, automatic migrations, and optimized
63
+ * SQLite configuration for each store. Connections are lazily created and
64
+ * cached for reuse, with proper cleanup on service termination.
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * // Basic usage - execute a query
69
+ * const result = yield* storeDb.withClient(store, (client) =>
70
+ * client`SELECT COUNT(*) as count FROM posts`
71
+ * );
72
+ *
73
+ * // Batch operations with the same connection
74
+ * const results = yield* storeDb.withClient(store, (client) =>
75
+ * Effect.gen(function* () {
76
+ * yield* client`INSERT INTO posts (id, content) VALUES (${id1}, ${content1})`;
77
+ * yield* client`INSERT INTO posts (id, content) VALUES (${id2}, ${content2})`;
78
+ * return yield* client`SELECT * FROM posts`;
79
+ * })
80
+ * );
81
+ * ```
82
+ */
83
+ export class StoreDb extends Context.Tag("@skygent/StoreDb")<
84
+ StoreDb,
85
+ {
86
+ /**
87
+ * Execute a database operation with a cached client for the specified store.
88
+ *
89
+ * This method automatically retrieves an existing connection or creates a new one,
90
+ * runs the provided operation, and keeps the connection cached for future use.
91
+ * The connection remains open until explicitly removed via `removeClient` or
92
+ * service shutdown.
93
+ *
94
+ * @param store - Store reference containing name and root path
95
+ * @param run - Effect function that receives the SQL client and returns a result
96
+ * @returns Effect containing the operation result, potentially failing with StoreIoError
97
+ * @example
98
+ * ```typescript
99
+ * const posts = yield* storeDb.withClient(store, (client) =>
100
+ * client`SELECT * FROM posts LIMIT 10`
101
+ * );
102
+ * ```
103
+ */
104
+ readonly withClient: <A, E>(
105
+ store: StoreRef,
106
+ run: (client: SqlClient.SqlClient) => Effect.Effect<A, E>
107
+ ) => Effect.Effect<A, StoreIoError | E>;
108
+
109
+ /**
110
+ * Remove a cached database connection for a store.
111
+ *
112
+ * Closes the connection gracefully (running PRAGMA optimize first) and
113
+ * removes it from the cache. Subsequent calls to `withClient` for this
114
+ * store will create a new connection.
115
+ *
116
+ * @param storeName - The name of the store whose connection should be removed
117
+ * @returns Effect that completes when the connection is closed
118
+ * @example
119
+ * ```typescript
120
+ * // Clean up a store's connection when no longer needed
121
+ * yield* storeDb.removeClient("myStore");
122
+ * ```
123
+ */
124
+ readonly removeClient: (storeName: string) => Effect.Effect<void>;
125
+ }
126
+ >() {
127
+ static readonly layer = Layer.scoped(
128
+ StoreDb,
129
+ Effect.gen(function* () {
130
+ const config = yield* AppConfigService;
131
+ const fs = yield* FileSystem.FileSystem;
132
+ const path = yield* Path.Path;
133
+ const reactivity = yield* Reactivity.Reactivity;
134
+
135
+ type CachedClient = {
136
+ readonly client: SqlClient.SqlClient;
137
+ readonly scope: Scope.CloseableScope;
138
+ };
139
+ const clients = yield* Ref.make(new Map<string, CachedClient>());
140
+ const clientLock = yield* Effect.makeSemaphore(1);
141
+
142
+ const migrate = Migrator.make({})({
143
+ loader: MigratorFileSystem.fromFileSystem(migrationsDir)
144
+ });
145
+
146
+ const optimizeClient = (client: SqlClient.SqlClient) =>
147
+ client`PRAGMA optimize`.pipe(
148
+ Effect.catchAll((error) =>
149
+ Effect.logWarning("PRAGMA optimize failed", {
150
+ error: error instanceof Error ? error.message : String(error)
151
+ })
152
+ )
153
+ );
154
+
155
+ const closeCachedClient = ({ client, scope }: CachedClient) =>
156
+ optimizeClient(client).pipe(Effect.zipRight(Scope.close(scope, Exit.void)));
157
+
158
+ const closeAllClients = () =>
159
+ Ref.get(clients).pipe(
160
+ Effect.flatMap((current) =>
161
+ Effect.forEach(current.values(), closeCachedClient, { discard: true })
162
+ )
163
+ );
164
+
165
+ yield* Effect.addFinalizer(() => closeAllClients());
166
+
167
+ const openClient = (store: StoreRef) =>
168
+ Effect.gen(function* () {
169
+ const dbPath = path.join(config.storeRoot, store.root, "index.sqlite");
170
+ const dbDir = path.dirname(dbPath);
171
+ yield* fs.makeDirectory(dbDir, { recursive: true });
172
+
173
+ const clientScope = yield* Scope.make();
174
+ const client = yield* SqliteClient.make({ filename: dbPath }).pipe(
175
+ Effect.provideService(Scope.Scope, clientScope),
176
+ Effect.provideService(Reactivity.Reactivity, reactivity)
177
+ );
178
+
179
+ // Configure SQLite for optimal performance
180
+ yield* client`PRAGMA busy_timeout = 5000`;
181
+ yield* client`PRAGMA journal_mode = WAL`;
182
+ yield* client`PRAGMA synchronous = NORMAL`;
183
+ yield* client`PRAGMA temp_store = MEMORY`;
184
+ yield* client`PRAGMA cache_size = -64000`;
185
+ yield* client`PRAGMA mmap_size = 30000000000`;
186
+ yield* client`PRAGMA optimize=0x10002`;
187
+ yield* client`PRAGMA foreign_keys = ON`;
188
+ yield* migrate.pipe(
189
+ Effect.provideService(SqlClient.SqlClient, client),
190
+ Effect.provideService(FileSystem.FileSystem, fs)
191
+ );
192
+
193
+ return { client, scope: clientScope };
194
+ });
195
+
196
+ const getClient = (store: StoreRef) =>
197
+ Effect.gen(function* () {
198
+ const cached = (yield* Ref.get(clients)).get(store.name);
199
+ if (cached) {
200
+ return cached.client;
201
+ }
202
+
203
+ return yield* clientLock.withPermits(1)(
204
+ Effect.gen(function* () {
205
+ const current = yield* Ref.get(clients);
206
+ const existing = current.get(store.name);
207
+ if (existing) {
208
+ return existing.client;
209
+ }
210
+
211
+ const created = yield* openClient(store);
212
+
213
+ const next = new Map(current);
214
+ next.set(store.name, created);
215
+ yield* Ref.set(clients, next);
216
+
217
+ return created.client;
218
+ })
219
+ );
220
+ });
221
+
222
+ const withClient = <A, E>(
223
+ store: StoreRef,
224
+ run: (client: SqlClient.SqlClient) => Effect.Effect<A, E>
225
+ ) =>
226
+ getClient(store).pipe(
227
+ Effect.mapError(toStoreIoError(store.root)),
228
+ Effect.flatMap(run)
229
+ );
230
+
231
+ const removeClient = (storeName: string) =>
232
+ Ref.modify(clients, (current) => {
233
+ const next = new Map(current);
234
+ const existing = next.get(storeName);
235
+ if (existing) {
236
+ next.delete(storeName);
237
+ }
238
+ return [existing, next] as const;
239
+ }).pipe(
240
+ Effect.flatMap((existing) =>
241
+ existing ? closeCachedClient(existing) : Effect.void
242
+ )
243
+ );
244
+
245
+ return StoreDb.of({ withClient, removeClient });
246
+ })
247
+ ).pipe(Layer.provide(Reactivity.layer));
248
+ }
@@ -0,0 +1,285 @@
1
+ import { Context, Effect, Layer, Option, ParseResult, Schema, Stream } from "effect";
2
+ import * as SqlSchema from "@effect/sql/SqlSchema";
3
+ import { StoreIoError } from "../domain/errors.js";
4
+ import { type EventLogEntry, PostEventRecord } from "../domain/events.js";
5
+ import { EventSeq } from "../domain/primitives.js";
6
+ import type { StorePath } from "../domain/primitives.js";
7
+ import type { StoreRef } from "../domain/store.js";
8
+ import { StoreDb } from "./store-db.js";
9
+
10
+ /**
11
+ * Store Event Log Service
12
+ *
13
+ * This module provides event sourcing capabilities for stores by streaming events
14
+ * from the SQLite `event_log` table. It implements an append-only event log pattern
15
+ * for tracking all changes to a store's data, enabling event-driven architectures,
16
+ * audit trails, and data synchronization.
17
+ *
18
+ * Key features:
19
+ * - Paginated event streaming with configurable batch size (500 events per page)
20
+ * - Automatic event sequence tracking for resumable streams
21
+ * - JSON event payload serialization/deserialization
22
+ * - Event log clearing for data reset scenarios
23
+ * - Efficient last event sequence retrieval from the event log table
24
+ *
25
+ * The service supports the event sourcing pattern where all state changes are stored
26
+ * as immutable events, allowing for event replay, projections, and temporal queries.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { Effect, Stream } from "effect";
31
+ * import { StoreEventLog } from "./services/store-event-log.js";
32
+ * import type { StoreRef } from "./domain/store.js";
33
+ *
34
+ * const program = Effect.gen(function* () {
35
+ * const eventLog = yield* StoreEventLog;
36
+ * const store: StoreRef = { name: "myStore", root: "stores/myStore" };
37
+ *
38
+ * // Stream all events from the store
39
+ * const events = yield* eventLog.stream(store).pipe(
40
+ * Stream.runCollect
41
+ * );
42
+ *
43
+ * // Get the last event sequence for resuming streams
44
+ * const lastSeq = yield* eventLog.getLastEventSeq(store);
45
+ *
46
+ * // Clear the event log (use with caution)
47
+ * yield* eventLog.clear(store);
48
+ * });
49
+ *
50
+ * const runnable = program.pipe(Effect.provide(StoreEventLog.layer));
51
+ * ```
52
+ *
53
+ * @module services/store-event-log
54
+ */
55
+
56
+ const pageSize = 500;
57
+
58
+ const eventLogRow = Schema.Struct({
59
+ event_seq: EventSeq,
60
+ payload_json: Schema.String
61
+ });
62
+
63
+ const lastEventSeqRow = Schema.Struct({
64
+ value: EventSeq
65
+ });
66
+
67
+ const decodeEventJson = (raw: string) =>
68
+ Schema.decodeUnknown(Schema.parseJson(PostEventRecord))(raw);
69
+
70
+ const toStoreIoError = (path: StorePath) => (cause: unknown) =>
71
+ StoreIoError.make({ path, cause });
72
+
73
+ /**
74
+ * Service for managing and streaming store event logs.
75
+ *
76
+ * Provides event sourcing capabilities with paginated streaming, event
77
+ * tracking, and log management. Events are stored as JSON payloads in an
78
+ * SQLite table with sequential event numbers for ordering.
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * // Stream all events from a store
83
+ * const events = yield* eventLog.stream(store).pipe(
84
+ * Stream.runCollect
85
+ * );
86
+ *
87
+ * // Process events incrementally
88
+ * yield* eventLog.stream(store).pipe(
89
+ * Stream.tap((event) => Effect.sync(() => console.log(event))),
90
+ * Stream.runDrain
91
+ * );
92
+ * ```
93
+ */
94
+ export class StoreEventLog extends Context.Tag("@skygent/StoreEventLog")<
95
+ StoreEventLog,
96
+ {
97
+ /**
98
+ * Stream all events from a store's event log.
99
+ *
100
+ * Returns a Stream that emits all `PostEventRecord` events from the store's
101
+ * event_log table in ascending order by event_seq. Events are fetched in
102
+ * paginated batches of 500 for memory efficiency. The stream automatically
103
+ * handles pagination and continues until all events are emitted.
104
+ *
105
+ * @param store - Store reference to stream events from
106
+ * @returns Stream of PostEventRecord events, failing with StoreIoError on database errors
107
+ * @example
108
+ * ```typescript
109
+ * // Collect all events into an array
110
+ * const allEvents = yield* eventLog.stream(store).pipe(
111
+ * Stream.runCollect
112
+ * );
113
+ *
114
+ * // Process events with backpressure
115
+ * yield* eventLog.stream(store).pipe(
116
+ * Stream.grouped(10),
117
+ * Stream.tap((batch) => processBatch(batch)),
118
+ * Stream.runDrain
119
+ * );
120
+ * ```
121
+ */
122
+ readonly stream: (
123
+ store: StoreRef
124
+ ) => Stream.Stream<EventLogEntry, StoreIoError>;
125
+
126
+ /**
127
+ * Clear all events from a store's event log.
128
+ *
129
+ * Deletes all records from the event_log table.
130
+ * This operation is irreversible and should be used with caution, typically
131
+ * for data resets or re-initialization scenarios.
132
+ *
133
+ * @param store - Store reference whose event log should be cleared
134
+ * @returns Effect that completes when the event log is cleared, failing with StoreIoError
135
+ * @example
136
+ * ```typescript
137
+ * // Clear the event log before re-syncing
138
+ * yield* eventLog.clear(store);
139
+ * // Now ready to start fresh event sourcing
140
+ * ```
141
+ */
142
+ readonly clear: (store: StoreRef) => Effect.Effect<void, StoreIoError>;
143
+
144
+ /**
145
+ * Retrieve the last event sequence from a store's event log.
146
+ *
147
+ * Queries the event_log table directly for the highest event_seq.
148
+ * Returns None if no events exist.
149
+ *
150
+ * This is useful for resuming event streams from a known position.
151
+ *
152
+ * @param store - Store reference to get the last event sequence from
153
+ * @returns Effect containing Option of EventSeq (None if no events), failing with StoreIoError
154
+ * @example
155
+ * ```typescript
156
+ * // Get last event sequence for incremental processing
157
+ * const lastSeq = yield* eventLog.getLastEventSeq(store);
158
+ *
159
+ * // Use with Option.match to handle empty event log
160
+ * yield* Option.match(lastId, {
161
+ * onNone: () => syncAllEvents(),
162
+ * onSome: (id) => syncEventsSince(id)
163
+ * });
164
+ * ```
165
+ */
166
+ readonly getLastEventSeq: (
167
+ store: StoreRef
168
+ ) => Effect.Effect<Option.Option<EventSeq>, StoreIoError>;
169
+ }
170
+ >() {
171
+ static readonly layer = Layer.effect(
172
+ StoreEventLog,
173
+ Effect.gen(function* () {
174
+ const storeDb = yield* StoreDb;
175
+
176
+ const stream = (store: StoreRef) =>
177
+ Stream.unwrap(
178
+ storeDb.withClient(store, (client) =>
179
+ Effect.gen(function* () {
180
+ const firstPage = SqlSchema.findAll({
181
+ Request: Schema.Void,
182
+ Result: eventLogRow,
183
+ execute: () =>
184
+ client`SELECT event_seq, payload_json
185
+ FROM event_log
186
+ ORDER BY event_seq ASC
187
+ LIMIT ${pageSize}`
188
+ });
189
+ const nextPage = SqlSchema.findAll({
190
+ Request: EventSeq,
191
+ Result: eventLogRow,
192
+ execute: (after) =>
193
+ client`SELECT event_seq, payload_json
194
+ FROM event_log
195
+ WHERE event_seq > ${after}
196
+ ORDER BY event_seq ASC
197
+ LIMIT ${pageSize}`
198
+ });
199
+
200
+ const decodeRows = (rows: ReadonlyArray<typeof eventLogRow.Type>) =>
201
+ Effect.forEach(
202
+ rows,
203
+ (row) =>
204
+ decodeEventJson(row.payload_json).pipe(
205
+ Effect.map((record) => ({
206
+ seq: row.event_seq,
207
+ record
208
+ }) satisfies EventLogEntry)
209
+ ),
210
+ { discard: false }
211
+ );
212
+
213
+ const toPage = (
214
+ rows: ReadonlyArray<typeof eventLogRow.Type>
215
+ ): Effect.Effect<
216
+ Option.Option<readonly [ReadonlyArray<EventLogEntry>, Option.Option<EventSeq>]>,
217
+ ParseResult.ParseError
218
+ > =>
219
+ rows.length === 0
220
+ ? Effect.succeed(Option.none())
221
+ : decodeRows(rows).pipe(
222
+ Effect.map((records) => {
223
+ const lastSeq = rows[rows.length - 1]!.event_seq;
224
+ return Option.some([
225
+ records,
226
+ Option.some(lastSeq)
227
+ ] as const);
228
+ })
229
+ );
230
+
231
+ return Stream.unfoldEffect(
232
+ Option.none<EventSeq>(),
233
+ (cursor) =>
234
+ Option.match(cursor, {
235
+ onNone: () => firstPage(undefined).pipe(Effect.flatMap(toPage)),
236
+ onSome: (after) => nextPage(after).pipe(Effect.flatMap(toPage))
237
+ })
238
+ ).pipe(
239
+ Stream.mapConcat((records) => records),
240
+ Stream.mapError(toStoreIoError(store.root))
241
+ );
242
+ })
243
+ ).pipe(Effect.mapError(toStoreIoError(store.root)))
244
+ );
245
+
246
+ const clear = Effect.fn("StoreEventLog.clear")((store: StoreRef) =>
247
+ storeDb
248
+ .withClient(store, (client) =>
249
+ Effect.gen(function* () {
250
+ yield* client`DELETE FROM event_log`;
251
+ yield* client`DELETE FROM event_log_meta`;
252
+ }).pipe(Effect.asVoid)
253
+ )
254
+ .pipe(Effect.mapError(toStoreIoError(store.root)))
255
+ );
256
+
257
+ const getLastEventSeq = Effect.fn("StoreEventLog.getLastEventSeq")(
258
+ (store: StoreRef) =>
259
+ storeDb
260
+ .withClient(store, (client) => {
261
+ const findLast = SqlSchema.findAll({
262
+ Request: Schema.Void,
263
+ Result: lastEventSeqRow,
264
+ execute: () =>
265
+ client`SELECT event_seq as value
266
+ FROM event_log
267
+ ORDER BY event_seq DESC
268
+ LIMIT 1`
269
+ });
270
+
271
+ return findLast(undefined).pipe(
272
+ Effect.flatMap((rows) =>
273
+ rows.length > 0
274
+ ? Effect.succeed(Option.some(rows[0]!.value))
275
+ : Effect.succeed(Option.none<EventSeq>())
276
+ )
277
+ );
278
+ })
279
+ .pipe(Effect.mapError(toStoreIoError(store.root)))
280
+ );
281
+
282
+ return StoreEventLog.of({ stream, clear, getLastEventSeq });
283
+ })
284
+ );
285
+ }