@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.
- package/README.md +59 -0
- package/index.ts +146 -0
- package/package.json +56 -0
- package/src/cli/app.ts +75 -0
- package/src/cli/config-command.ts +140 -0
- package/src/cli/config.ts +91 -0
- package/src/cli/derive.ts +205 -0
- package/src/cli/doc/annotation.ts +36 -0
- package/src/cli/doc/filter.ts +69 -0
- package/src/cli/doc/index.ts +9 -0
- package/src/cli/doc/post.ts +155 -0
- package/src/cli/doc/primitives.ts +25 -0
- package/src/cli/doc/render.ts +18 -0
- package/src/cli/doc/table.ts +114 -0
- package/src/cli/doc/thread.ts +46 -0
- package/src/cli/doc/tree.ts +126 -0
- package/src/cli/errors.ts +59 -0
- package/src/cli/exit-codes.ts +52 -0
- package/src/cli/feed.ts +177 -0
- package/src/cli/filter-dsl.ts +1411 -0
- package/src/cli/filter-errors.ts +208 -0
- package/src/cli/filter-help.ts +70 -0
- package/src/cli/filter-input.ts +54 -0
- package/src/cli/filter.ts +435 -0
- package/src/cli/graph.ts +472 -0
- package/src/cli/help.ts +14 -0
- package/src/cli/interval.ts +35 -0
- package/src/cli/jetstream.ts +173 -0
- package/src/cli/layers.ts +180 -0
- package/src/cli/logging.ts +136 -0
- package/src/cli/output-format.ts +26 -0
- package/src/cli/output.ts +82 -0
- package/src/cli/parse.ts +80 -0
- package/src/cli/post.ts +193 -0
- package/src/cli/preferences.ts +11 -0
- package/src/cli/query-fields.ts +247 -0
- package/src/cli/query.ts +415 -0
- package/src/cli/range.ts +44 -0
- package/src/cli/search.ts +465 -0
- package/src/cli/shared-options.ts +169 -0
- package/src/cli/shared.ts +20 -0
- package/src/cli/store-errors.ts +80 -0
- package/src/cli/store-tree.ts +392 -0
- package/src/cli/store.ts +395 -0
- package/src/cli/sync-factory.ts +107 -0
- package/src/cli/sync.ts +366 -0
- package/src/cli/view-thread.ts +196 -0
- package/src/cli/view.ts +47 -0
- package/src/cli/watch.ts +344 -0
- package/src/db/migrations/store-catalog/001_init.ts +14 -0
- package/src/db/migrations/store-index/001_init.ts +34 -0
- package/src/db/migrations/store-index/002_event_log.ts +24 -0
- package/src/db/migrations/store-index/003_fts_and_derived.ts +52 -0
- package/src/db/migrations/store-index/004_query_indexes.ts +9 -0
- package/src/db/migrations/store-index/005_post_lang.ts +15 -0
- package/src/db/migrations/store-index/006_has_embed.ts +10 -0
- package/src/db/migrations/store-index/007_event_seq_and_checkpoints.ts +68 -0
- package/src/domain/bsky.ts +467 -0
- package/src/domain/config.ts +11 -0
- package/src/domain/credentials.ts +6 -0
- package/src/domain/defaults.ts +8 -0
- package/src/domain/derivation.ts +55 -0
- package/src/domain/errors.ts +71 -0
- package/src/domain/events.ts +55 -0
- package/src/domain/extract.ts +64 -0
- package/src/domain/filter-describe.ts +551 -0
- package/src/domain/filter-explain.ts +9 -0
- package/src/domain/filter.ts +797 -0
- package/src/domain/format.ts +91 -0
- package/src/domain/index.ts +13 -0
- package/src/domain/indexes.ts +17 -0
- package/src/domain/policies.ts +16 -0
- package/src/domain/post.ts +88 -0
- package/src/domain/primitives.ts +50 -0
- package/src/domain/raw.ts +140 -0
- package/src/domain/store.ts +103 -0
- package/src/domain/sync.ts +211 -0
- package/src/domain/text-width.ts +56 -0
- package/src/services/app-config.ts +278 -0
- package/src/services/bsky-client.ts +2113 -0
- package/src/services/credential-store.ts +408 -0
- package/src/services/derivation-engine.ts +502 -0
- package/src/services/derivation-settings.ts +61 -0
- package/src/services/derivation-validator.ts +68 -0
- package/src/services/filter-compiler.ts +269 -0
- package/src/services/filter-library.ts +371 -0
- package/src/services/filter-runtime.ts +821 -0
- package/src/services/filter-settings.ts +30 -0
- package/src/services/identity-resolver.ts +563 -0
- package/src/services/jetstream-sync.ts +636 -0
- package/src/services/lineage-store.ts +89 -0
- package/src/services/link-validator.ts +244 -0
- package/src/services/output-manager.ts +274 -0
- package/src/services/post-parser.ts +62 -0
- package/src/services/profile-resolver.ts +223 -0
- package/src/services/resource-monitor.ts +106 -0
- package/src/services/shared.ts +69 -0
- package/src/services/store-cleaner.ts +43 -0
- package/src/services/store-commit.ts +168 -0
- package/src/services/store-db.ts +248 -0
- package/src/services/store-event-log.ts +285 -0
- package/src/services/store-index-sql.ts +289 -0
- package/src/services/store-index.ts +1152 -0
- package/src/services/store-keys.ts +4 -0
- package/src/services/store-manager.ts +358 -0
- package/src/services/store-stats.ts +522 -0
- package/src/services/store-writer.ts +200 -0
- package/src/services/sync-checkpoint-store.ts +169 -0
- package/src/services/sync-engine.ts +547 -0
- package/src/services/sync-reporter.ts +16 -0
- package/src/services/sync-settings.ts +72 -0
- package/src/services/trending-topics.ts +226 -0
- package/src/services/view-checkpoint-store.ts +238 -0
- package/src/typeclass/chunk.ts +84 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Clock, Context, Effect, Layer, Option, Random, Schema, SynchronizedRef } from "effect";
|
|
2
|
+
import type * as SqlClient from "@effect/sql/SqlClient";
|
|
3
|
+
import * as SqlSchema from "@effect/sql/SqlSchema";
|
|
4
|
+
import { StoreIoError } from "../domain/errors.js";
|
|
5
|
+
import { type EventLogEntry, PostEvent, PostEventRecord } from "../domain/events.js";
|
|
6
|
+
import { EventId, EventSeq, PostUri } from "../domain/primitives.js";
|
|
7
|
+
import type { StorePath } from "../domain/primitives.js";
|
|
8
|
+
import type { StoreRef } from "../domain/store.js";
|
|
9
|
+
import { StoreDb } from "./store-db.js";
|
|
10
|
+
|
|
11
|
+
const ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
12
|
+
const MAX_ULID_TIME = 0xffff_ffff_ffff;
|
|
13
|
+
|
|
14
|
+
class UlidTimeError extends Schema.TaggedError<UlidTimeError>()("UlidTimeError", {
|
|
15
|
+
time: Schema.String,
|
|
16
|
+
message: Schema.String
|
|
17
|
+
}) {}
|
|
18
|
+
|
|
19
|
+
const encodeTime = (time: number) => {
|
|
20
|
+
if (!Number.isFinite(time) || time < 0 || time > MAX_ULID_TIME) {
|
|
21
|
+
return Effect.fail(
|
|
22
|
+
UlidTimeError.make({
|
|
23
|
+
time: String(time),
|
|
24
|
+
message: `ULID time out of range: ${time}`
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return Effect.sync(() => {
|
|
29
|
+
let value = BigInt(Math.trunc(time));
|
|
30
|
+
let output = "";
|
|
31
|
+
for (let i = 0; i < 10; i += 1) {
|
|
32
|
+
const mod = Number(value % 32n);
|
|
33
|
+
output = `${ULID_ALPHABET[mod]}${output}`;
|
|
34
|
+
value = value / 32n;
|
|
35
|
+
}
|
|
36
|
+
return output;
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const encodeRandomDigits = (digits: ReadonlyArray<number>) =>
|
|
41
|
+
digits.map((digit) => ULID_ALPHABET[digit]).join("");
|
|
42
|
+
|
|
43
|
+
const incrementRandomDigits = (digits: ReadonlyArray<number>) => {
|
|
44
|
+
const next = digits.slice();
|
|
45
|
+
for (let i = next.length - 1; i >= 0; i -= 1) {
|
|
46
|
+
const value = next[i];
|
|
47
|
+
if (typeof value !== "number") {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (value === 31) {
|
|
51
|
+
next[i] = 0;
|
|
52
|
+
if (i === 0) {
|
|
53
|
+
return { digits: next, overflow: true } as const;
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
next[i] = value + 1;
|
|
57
|
+
return { digits: next, overflow: false } as const;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { digits: next, overflow: true } as const;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const eventLogInsertRow = Schema.Struct({
|
|
64
|
+
event_id: EventId,
|
|
65
|
+
event_type: Schema.String,
|
|
66
|
+
post_uri: PostUri,
|
|
67
|
+
payload_json: Schema.String,
|
|
68
|
+
created_at: Schema.String,
|
|
69
|
+
source: Schema.String
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const eventLogSeqRow = Schema.Struct({
|
|
73
|
+
event_seq: EventSeq
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const toStoreIoError = (path: StorePath) => (cause: unknown) =>
|
|
77
|
+
StoreIoError.make({ path, cause });
|
|
78
|
+
|
|
79
|
+
export class StoreWriter extends Context.Tag("@skygent/StoreWriter")<
|
|
80
|
+
StoreWriter,
|
|
81
|
+
{
|
|
82
|
+
readonly append: (
|
|
83
|
+
store: StoreRef,
|
|
84
|
+
event: PostEvent
|
|
85
|
+
) => Effect.Effect<EventLogEntry, StoreIoError>;
|
|
86
|
+
readonly appendWithClient: (
|
|
87
|
+
client: SqlClient.SqlClient,
|
|
88
|
+
event: PostEvent
|
|
89
|
+
) => Effect.Effect<EventLogEntry, unknown>;
|
|
90
|
+
}
|
|
91
|
+
>() {
|
|
92
|
+
static readonly layer = Layer.effect(
|
|
93
|
+
StoreWriter,
|
|
94
|
+
Effect.gen(function* () {
|
|
95
|
+
const storeDb = yield* StoreDb;
|
|
96
|
+
const idState = yield* SynchronizedRef.make({
|
|
97
|
+
lastTime: 0,
|
|
98
|
+
lastRandom: [] as ReadonlyArray<number>
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const nextRandomDigits = () =>
|
|
102
|
+
Effect.forEach(
|
|
103
|
+
Array.from({ length: 16 }),
|
|
104
|
+
() => Random.nextIntBetween(0, 32)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const generateEventId = Effect.fn("StoreWriter.generateEventId")(() =>
|
|
108
|
+
SynchronizedRef.modifyEffect(idState, (state) =>
|
|
109
|
+
Effect.gen(function* () {
|
|
110
|
+
const now = yield* Clock.currentTimeMillis;
|
|
111
|
+
let time = state.lastTime;
|
|
112
|
+
let digits = state.lastRandom;
|
|
113
|
+
|
|
114
|
+
if (digits.length === 0 || now > state.lastTime) {
|
|
115
|
+
time = now;
|
|
116
|
+
digits = yield* nextRandomDigits();
|
|
117
|
+
} else {
|
|
118
|
+
const incremented = incrementRandomDigits(digits);
|
|
119
|
+
if (incremented.overflow) {
|
|
120
|
+
time = state.lastTime + 1;
|
|
121
|
+
digits = yield* nextRandomDigits();
|
|
122
|
+
} else {
|
|
123
|
+
time = state.lastTime;
|
|
124
|
+
digits = incremented.digits;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const timeEncoded = yield* encodeTime(time);
|
|
129
|
+
const id = `${timeEncoded}${encodeRandomDigits(digits)}`;
|
|
130
|
+
const decoded = yield* Schema.decodeUnknown(EventId)(id);
|
|
131
|
+
return [
|
|
132
|
+
decoded,
|
|
133
|
+
{ lastTime: time, lastRandom: digits }
|
|
134
|
+
] as const;
|
|
135
|
+
})
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const appendWithClient = Effect.fn("StoreWriter.appendWithClient")(
|
|
140
|
+
(client: SqlClient.SqlClient, event: PostEvent) =>
|
|
141
|
+
Effect.gen(function* () {
|
|
142
|
+
const id = yield* generateEventId();
|
|
143
|
+
const record = PostEventRecord.make({
|
|
144
|
+
id,
|
|
145
|
+
version: 1,
|
|
146
|
+
event
|
|
147
|
+
});
|
|
148
|
+
const payloadJson = yield* Schema.encode(
|
|
149
|
+
Schema.parseJson(PostEventRecord)
|
|
150
|
+
)(record);
|
|
151
|
+
const postUri =
|
|
152
|
+
record.event._tag === "PostUpsert"
|
|
153
|
+
? record.event.post.uri
|
|
154
|
+
: record.event.uri;
|
|
155
|
+
const createdAt =
|
|
156
|
+
record.event.meta.createdAt instanceof Date
|
|
157
|
+
? record.event.meta.createdAt.toISOString()
|
|
158
|
+
: new Date(record.event.meta.createdAt).toISOString();
|
|
159
|
+
|
|
160
|
+
const insertEvent = SqlSchema.findOne({
|
|
161
|
+
Request: eventLogInsertRow,
|
|
162
|
+
Result: eventLogSeqRow,
|
|
163
|
+
execute: (row) =>
|
|
164
|
+
client`INSERT INTO event_log ${client.insert(row)}
|
|
165
|
+
RETURNING event_seq`
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const inserted = yield* insertEvent({
|
|
169
|
+
event_id: record.id,
|
|
170
|
+
event_type: record.event._tag,
|
|
171
|
+
post_uri: postUri,
|
|
172
|
+
payload_json: payloadJson,
|
|
173
|
+
created_at: createdAt,
|
|
174
|
+
source: record.event.meta.source
|
|
175
|
+
}).pipe(
|
|
176
|
+
Effect.flatMap(
|
|
177
|
+
Option.match({
|
|
178
|
+
onNone: () =>
|
|
179
|
+
Effect.dieMessage("Expected event_seq from event_log insert."),
|
|
180
|
+
onSome: (row) => Effect.succeed(row)
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
return { seq: inserted.event_seq, record } satisfies EventLogEntry;
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const append = Effect.fn("StoreWriter.append")(
|
|
189
|
+
(store: StoreRef, event: PostEvent) =>
|
|
190
|
+
storeDb
|
|
191
|
+
.withClient(store, (client) =>
|
|
192
|
+
appendWithClient(client, event).pipe(client.withTransaction)
|
|
193
|
+
)
|
|
194
|
+
.pipe(Effect.mapError(toStoreIoError(store.root)))
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return StoreWriter.of({ append, appendWithClient });
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Checkpoint Store Service
|
|
3
|
+
*
|
|
4
|
+
* Manages checkpoints for sync operations to enable resumable data synchronization.
|
|
5
|
+
* Stores the last sync position (cursor/timestamp) for each data source, allowing
|
|
6
|
+
* sync operations to resume from where they left off rather than starting over.
|
|
7
|
+
*
|
|
8
|
+
* Checkpoints are stored per-store and per-data-source combination, using a
|
|
9
|
+
* prefixed key-value store structure for isolation.
|
|
10
|
+
*
|
|
11
|
+
* @module services/sync-checkpoint-store
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Context, Effect, Layer, Option, Schema } from "effect";
|
|
15
|
+
import { StoreIoError } from "../domain/errors.js";
|
|
16
|
+
import { DataSourceSchema, SyncCheckpoint, type DataSource, dataSourceKey } from "../domain/sync.js";
|
|
17
|
+
import { EventSeq, Timestamp, type StorePath } from "../domain/primitives.js";
|
|
18
|
+
import type { StoreRef } from "../domain/store.js";
|
|
19
|
+
import { StoreDb } from "./store-db.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Converts an error to a StoreIoError with the given path.
|
|
23
|
+
* @param path - The store path for error context
|
|
24
|
+
* @returns A function that creates StoreIoError from any cause
|
|
25
|
+
*/
|
|
26
|
+
const toStoreIoError = (path: StorePath) => (cause: unknown) =>
|
|
27
|
+
StoreIoError.make({ path, cause });
|
|
28
|
+
|
|
29
|
+
const checkpointRow = Schema.Struct({
|
|
30
|
+
source_key: Schema.String,
|
|
31
|
+
source_json: Schema.String,
|
|
32
|
+
cursor: Schema.NullOr(Schema.String),
|
|
33
|
+
last_event_seq: Schema.NullOr(EventSeq),
|
|
34
|
+
filter_hash: Schema.NullOr(Schema.String),
|
|
35
|
+
updated_at: Schema.String
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const decodeSource = (raw: string) =>
|
|
39
|
+
Schema.decodeUnknown(Schema.parseJson(DataSourceSchema))(raw);
|
|
40
|
+
|
|
41
|
+
const encodeSource = (source: DataSource) =>
|
|
42
|
+
Schema.encode(Schema.parseJson(DataSourceSchema))(source);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Service for managing sync operation checkpoints.
|
|
46
|
+
*
|
|
47
|
+
* This service provides methods to load and save sync checkpoints, which track
|
|
48
|
+
* the last successfully processed position for each data source. This enables
|
|
49
|
+
* resumable sync operations that can pick up where they left off.
|
|
50
|
+
*/
|
|
51
|
+
export class SyncCheckpointStore extends Context.Tag("@skygent/SyncCheckpointStore")<
|
|
52
|
+
SyncCheckpointStore,
|
|
53
|
+
{
|
|
54
|
+
/**
|
|
55
|
+
* Loads the sync checkpoint for a given store and data source.
|
|
56
|
+
*
|
|
57
|
+
* @param store - The store reference to load from
|
|
58
|
+
* @param source - The data source to get checkpoint for
|
|
59
|
+
* @returns Effect resolving to Option of SyncCheckpoint, or StoreIoError on failure
|
|
60
|
+
*/
|
|
61
|
+
readonly load: (
|
|
62
|
+
store: StoreRef,
|
|
63
|
+
source: DataSource
|
|
64
|
+
) => Effect.Effect<Option.Option<SyncCheckpoint>, StoreIoError>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Saves a sync checkpoint for resumable operations.
|
|
68
|
+
*
|
|
69
|
+
* @param store - The store reference to save to
|
|
70
|
+
* @param checkpoint - The checkpoint data to persist
|
|
71
|
+
* @returns Effect resolving to void, or StoreIoError on failure
|
|
72
|
+
*/
|
|
73
|
+
readonly save: (
|
|
74
|
+
store: StoreRef,
|
|
75
|
+
checkpoint: SyncCheckpoint
|
|
76
|
+
) => Effect.Effect<void, StoreIoError>;
|
|
77
|
+
}
|
|
78
|
+
>() {
|
|
79
|
+
static readonly layer = Layer.effect(
|
|
80
|
+
SyncCheckpointStore,
|
|
81
|
+
Effect.gen(function* () {
|
|
82
|
+
const storeDb = yield* StoreDb;
|
|
83
|
+
|
|
84
|
+
const load = Effect.fn("SyncCheckpointStore.load")(
|
|
85
|
+
(store: StoreRef, source: DataSource) => {
|
|
86
|
+
const key = dataSourceKey(source);
|
|
87
|
+
return storeDb
|
|
88
|
+
.withClient(store, (client) =>
|
|
89
|
+
Effect.gen(function* () {
|
|
90
|
+
const rows = yield* client`SELECT
|
|
91
|
+
source_key,
|
|
92
|
+
source_json,
|
|
93
|
+
cursor,
|
|
94
|
+
last_event_seq,
|
|
95
|
+
filter_hash,
|
|
96
|
+
updated_at
|
|
97
|
+
FROM sync_checkpoints
|
|
98
|
+
WHERE source_key = ${key}`;
|
|
99
|
+
if (rows.length === 0) {
|
|
100
|
+
return Option.none<SyncCheckpoint>();
|
|
101
|
+
}
|
|
102
|
+
const decodedRows = yield* Schema.decodeUnknown(
|
|
103
|
+
Schema.Array(checkpointRow)
|
|
104
|
+
)(rows);
|
|
105
|
+
const row = decodedRows[0]!;
|
|
106
|
+
const decodedSource = yield* decodeSource(row.source_json);
|
|
107
|
+
const updatedAt = yield* Schema.decodeUnknown(Timestamp)(row.updated_at);
|
|
108
|
+
const checkpoint = SyncCheckpoint.make({
|
|
109
|
+
source: decodedSource,
|
|
110
|
+
cursor: row.cursor ?? undefined,
|
|
111
|
+
lastEventSeq: row.last_event_seq ?? undefined,
|
|
112
|
+
filterHash: row.filter_hash ?? undefined,
|
|
113
|
+
updatedAt
|
|
114
|
+
});
|
|
115
|
+
return Option.some(checkpoint);
|
|
116
|
+
})
|
|
117
|
+
)
|
|
118
|
+
.pipe(Effect.mapError(toStoreIoError(store.root)));
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const save = Effect.fn("SyncCheckpointStore.save")(
|
|
123
|
+
(store: StoreRef, checkpoint: SyncCheckpoint) => {
|
|
124
|
+
const key = dataSourceKey(checkpoint.source);
|
|
125
|
+
return storeDb
|
|
126
|
+
.withClient(store, (client) =>
|
|
127
|
+
Effect.gen(function* () {
|
|
128
|
+
const sourceJson = yield* encodeSource(checkpoint.source);
|
|
129
|
+
const updatedAt = checkpoint.updatedAt.toISOString();
|
|
130
|
+
const lastSeq = checkpoint.lastEventSeq ?? null;
|
|
131
|
+
const cursor = checkpoint.cursor ?? null;
|
|
132
|
+
const filterHash = checkpoint.filterHash ?? null;
|
|
133
|
+
yield* client`INSERT INTO sync_checkpoints (
|
|
134
|
+
source_key,
|
|
135
|
+
source_json,
|
|
136
|
+
cursor,
|
|
137
|
+
last_event_seq,
|
|
138
|
+
filter_hash,
|
|
139
|
+
updated_at
|
|
140
|
+
)
|
|
141
|
+
VALUES (
|
|
142
|
+
${key},
|
|
143
|
+
${sourceJson},
|
|
144
|
+
${cursor},
|
|
145
|
+
${lastSeq},
|
|
146
|
+
${filterHash},
|
|
147
|
+
${updatedAt}
|
|
148
|
+
)
|
|
149
|
+
ON CONFLICT(source_key) DO UPDATE SET
|
|
150
|
+
source_json = excluded.source_json,
|
|
151
|
+
cursor = excluded.cursor,
|
|
152
|
+
filter_hash = excluded.filter_hash,
|
|
153
|
+
updated_at = excluded.updated_at,
|
|
154
|
+
last_event_seq = CASE
|
|
155
|
+
WHEN excluded.last_event_seq IS NULL THEN sync_checkpoints.last_event_seq
|
|
156
|
+
WHEN sync_checkpoints.last_event_seq IS NULL THEN excluded.last_event_seq
|
|
157
|
+
WHEN excluded.last_event_seq >= sync_checkpoints.last_event_seq THEN excluded.last_event_seq
|
|
158
|
+
ELSE sync_checkpoints.last_event_seq
|
|
159
|
+
END`;
|
|
160
|
+
})
|
|
161
|
+
)
|
|
162
|
+
.pipe(Effect.mapError(toStoreIoError(store.root)));
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return SyncCheckpointStore.of({ load, save });
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
}
|