@trestleinc/replicate 1.1.2 → 1.2.0-preview.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.
Files changed (39) hide show
  1. package/README.md +40 -41
  2. package/dist/client/index.d.ts +34 -26
  3. package/dist/client/index.js +904 -732
  4. package/dist/component/_generated/api.d.ts +2 -2
  5. package/dist/component/_generated/component.d.ts +84 -27
  6. package/dist/component/convex.config.d.ts +2 -2
  7. package/dist/component/mutations.d.ts +131 -0
  8. package/dist/component/mutations.js +493 -0
  9. package/dist/component/schema.d.ts +71 -31
  10. package/dist/component/schema.js +37 -14
  11. package/dist/server/index.d.ts +58 -47
  12. package/dist/server/index.js +227 -132
  13. package/package.json +3 -1
  14. package/src/client/collection.ts +334 -523
  15. package/src/client/errors.ts +1 -1
  16. package/src/client/index.ts +4 -7
  17. package/src/client/merge.ts +2 -2
  18. package/src/client/persistence/indexeddb.ts +10 -14
  19. package/src/client/prose.ts +147 -203
  20. package/src/client/services/awareness.ts +373 -0
  21. package/src/client/services/context.ts +114 -0
  22. package/src/client/services/seq.ts +78 -0
  23. package/src/client/services/session.ts +20 -0
  24. package/src/client/services/sync.ts +122 -0
  25. package/src/client/subdocs.ts +263 -0
  26. package/src/component/_generated/api.ts +2 -2
  27. package/src/component/_generated/component.ts +73 -28
  28. package/src/component/mutations.ts +734 -0
  29. package/src/component/schema.ts +31 -14
  30. package/src/server/collection.ts +98 -0
  31. package/src/server/index.ts +2 -2
  32. package/src/server/{storage.ts → replicate.ts} +214 -75
  33. package/dist/component/public.d.ts +0 -83
  34. package/dist/component/public.js +0 -325
  35. package/src/client/prose-schema.ts +0 -55
  36. package/src/client/services/cursor.ts +0 -109
  37. package/src/component/public.ts +0 -453
  38. package/src/server/builder.ts +0 -98
  39. /package/src/client/{replicate.ts → ops.ts} +0 -0
@@ -0,0 +1,373 @@
1
+ import * as Y from "yjs";
2
+ import { Awareness } from "y-protocols/awareness";
3
+ import type { ConvexClient } from "convex/browser";
4
+ import type { FunctionReference } from "convex/server";
5
+
6
+ const DEFAULT_HEARTBEAT_INTERVAL = 10000;
7
+ const DEFAULT_THROTTLE_MS = 50;
8
+
9
+ interface AwarenessApi {
10
+ mark: FunctionReference<"mutation">;
11
+ sessions: FunctionReference<"query">;
12
+ cursors: FunctionReference<"query">;
13
+ leave: FunctionReference<"mutation">;
14
+ }
15
+
16
+ export interface ConvexAwarenessConfig {
17
+ convexClient: ConvexClient;
18
+ api: AwarenessApi;
19
+ document: string;
20
+ client: string;
21
+ ydoc: Y.Doc;
22
+ interval?: number;
23
+ syncReady?: Promise<void>;
24
+ }
25
+
26
+ export interface ConvexAwarenessProvider {
27
+ awareness: Awareness;
28
+ document: Y.Doc;
29
+ destroy: () => void;
30
+ }
31
+
32
+ /**
33
+ * Creates a Yjs Awareness instance backed by Convex for transport.
34
+ * This provider syncs awareness state (cursors, user info) via Convex
35
+ * mutations and queries instead of WebSocket.
36
+ *
37
+ * Compatible with TipTap's CollaborationCursor and BlockNote's collaboration.
38
+ */
39
+ export function createAwarenessProvider(
40
+ config: ConvexAwarenessConfig,
41
+ ): ConvexAwarenessProvider {
42
+ const {
43
+ convexClient,
44
+ api,
45
+ document,
46
+ client,
47
+ ydoc,
48
+ interval = DEFAULT_HEARTBEAT_INTERVAL,
49
+ syncReady,
50
+ } = config;
51
+
52
+ const awareness = new Awareness(ydoc);
53
+ let destroyed = false;
54
+ let visible = true;
55
+ let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
56
+ let pendingUpdate: ReturnType<typeof setTimeout> | null = null;
57
+ let unsubscribeCursors: (() => void) | undefined;
58
+ let unsubscribeVisibility: (() => void) | undefined;
59
+ let unsubscribePageHide: (() => void) | undefined;
60
+
61
+ // Track remote client IDs we know about
62
+ const remoteClientIds = new Map<string, number>();
63
+
64
+ const getVector = (): ArrayBuffer | undefined => {
65
+ return Y.encodeStateVector(ydoc).buffer as ArrayBuffer;
66
+ };
67
+
68
+ /**
69
+ * Extract cursor from awareness state for Convex storage.
70
+ * y-prosemirror stores cursor as RelativePosition class instances.
71
+ * We serialize to plain JSON objects for Convex storage.
72
+ */
73
+ const extractCursorFromState = (state: Record<string, unknown> | null): {
74
+ anchor: unknown;
75
+ head: unknown;
76
+ } | undefined => {
77
+ if (!state) return undefined;
78
+
79
+ const cursor = state.cursor as {
80
+ anchor?: unknown;
81
+ head?: unknown;
82
+ } | undefined | null;
83
+
84
+ if (cursor?.anchor === undefined || cursor.head === undefined) {
85
+ return undefined;
86
+ }
87
+
88
+ try {
89
+ const serialized = {
90
+ anchor: JSON.parse(JSON.stringify(cursor.anchor)),
91
+ head: JSON.parse(JSON.stringify(cursor.head)),
92
+ };
93
+ return serialized;
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ };
99
+
100
+ /**
101
+ * Extract user profile from awareness state.
102
+ */
103
+ const extractUserFromState = (state: Record<string, unknown> | null): {
104
+ user?: string;
105
+ profile?: { name?: string; color?: string; avatar?: string };
106
+ } => {
107
+ if (!state) return {};
108
+
109
+ const user = state.user as
110
+ | { name?: string; color?: string; [key: string]: unknown }
111
+ | undefined;
112
+ if (user) {
113
+ return {
114
+ profile: {
115
+ name: user.name,
116
+ color: user.color,
117
+ avatar: user.avatar as string | undefined,
118
+ },
119
+ };
120
+ }
121
+
122
+ return {};
123
+ };
124
+
125
+ const sendToServer = () => {
126
+ if (destroyed || !visible) return;
127
+
128
+ const localState = awareness.getLocalState();
129
+ const cursor = extractCursorFromState(localState);
130
+ const { user, profile } = extractUserFromState(localState);
131
+ const vector = getVector();
132
+
133
+ convexClient.mutation(api.mark, {
134
+ document,
135
+ client,
136
+ cursor,
137
+ user,
138
+ profile,
139
+ interval,
140
+ vector,
141
+ });
142
+ };
143
+
144
+ /**
145
+ * Throttled version of sendToServer for frequent updates.
146
+ */
147
+ const throttledSend = () => {
148
+ if (pendingUpdate) return;
149
+
150
+ pendingUpdate = setTimeout(() => {
151
+ pendingUpdate = null;
152
+ sendToServer();
153
+ }, DEFAULT_THROTTLE_MS);
154
+ };
155
+
156
+ /**
157
+ * Handle local awareness changes.
158
+ */
159
+ const onLocalAwarenessUpdate = (
160
+ changes: { added: number[]; updated: number[]; removed: number[] },
161
+ origin: unknown,
162
+ ) => {
163
+ // Only send if the change is local (not from applying remote state)
164
+ if (origin === "remote") return;
165
+
166
+ // Check if our client was updated
167
+ const localClientId = awareness.clientID;
168
+ if (
169
+ changes.added.includes(localClientId)
170
+ || changes.updated.includes(localClientId)
171
+ ) {
172
+ throttledSend();
173
+ }
174
+ };
175
+
176
+ const subscribeToPresence = () => {
177
+ unsubscribeCursors = convexClient.onUpdate(
178
+ api.sessions,
179
+ { document, connected: true, exclude: client },
180
+ (remotes: {
181
+ client: string;
182
+ document: string;
183
+ user?: string;
184
+ profile?: { name?: string; color?: string; avatar?: string };
185
+ cursor?: { anchor: unknown; head: unknown; field?: string };
186
+ }[]) => {
187
+ if (destroyed) return;
188
+
189
+ const validRemotes = remotes.filter(r => r.document === document);
190
+
191
+ const currentRemotes = new Set<string>();
192
+
193
+ for (const remote of validRemotes) {
194
+ currentRemotes.add(remote.client);
195
+
196
+ let remoteClientId = remoteClientIds.get(remote.client);
197
+ if (!remoteClientId) {
198
+ remoteClientId = hashStringToNumber(remote.client);
199
+ remoteClientIds.set(remote.client, remoteClientId);
200
+ }
201
+
202
+ const remoteState: Record<string, unknown> = {
203
+ user: {
204
+ name: remote.profile?.name ?? remote.user ?? "Anonymous",
205
+ color: remote.profile?.color ?? getColorForClient(remote.client),
206
+ avatar: remote.profile?.avatar,
207
+ clientId: remote.client,
208
+ },
209
+ };
210
+
211
+ if (remote.cursor) {
212
+ remoteState.cursor = remote.cursor;
213
+ }
214
+
215
+ awareness.states.set(remoteClientId, remoteState);
216
+ }
217
+
218
+ for (const [clientStr, clientId] of remoteClientIds) {
219
+ if (!currentRemotes.has(clientStr)) {
220
+ awareness.states.delete(clientId);
221
+ remoteClientIds.delete(clientStr);
222
+ }
223
+ }
224
+
225
+ awareness.emit("update", [
226
+ { added: [], updated: Array.from(remoteClientIds.values()), removed: [] },
227
+ "remote",
228
+ ]);
229
+ },
230
+ );
231
+ };
232
+
233
+ const setupVisibilityHandler = () => {
234
+ if (typeof globalThis.document === "undefined") return;
235
+
236
+ const handler = () => {
237
+ const wasVisible = visible;
238
+ visible = globalThis.document.visibilityState === "visible";
239
+
240
+ if (wasVisible && !visible) {
241
+ convexClient.mutation(api.mark, {
242
+ document,
243
+ client,
244
+ cursor: undefined,
245
+ interval,
246
+ vector: getVector(),
247
+ });
248
+ }
249
+ else if (!wasVisible && visible) {
250
+ sendToServer();
251
+ }
252
+ };
253
+
254
+ globalThis.document.addEventListener("visibilitychange", handler);
255
+ unsubscribeVisibility = () => {
256
+ globalThis.document.removeEventListener("visibilitychange", handler);
257
+ };
258
+ };
259
+
260
+ const setupPageHideHandler = () => {
261
+ if (typeof globalThis.window === "undefined") return;
262
+
263
+ const handler = (e: PageTransitionEvent) => {
264
+ if (e.persisted) return;
265
+ if (destroyed) return;
266
+
267
+ convexClient.mutation(api.leave, { document, client });
268
+ };
269
+
270
+ globalThis.window.addEventListener("pagehide", handler);
271
+ unsubscribePageHide = () => {
272
+ globalThis.window.removeEventListener("pagehide", handler);
273
+ };
274
+ };
275
+
276
+ /**
277
+ * Start periodic heartbeat to keep presence alive.
278
+ */
279
+ const startHeartbeat = () => {
280
+ sendToServer();
281
+ heartbeatTimer = setInterval(sendToServer, interval);
282
+ };
283
+
284
+ /**
285
+ * Stop heartbeat.
286
+ */
287
+ const stopHeartbeat = () => {
288
+ if (heartbeatTimer) {
289
+ clearInterval(heartbeatTimer);
290
+ heartbeatTimer = null;
291
+ }
292
+ };
293
+
294
+ awareness.on("update", onLocalAwarenessUpdate);
295
+ subscribeToPresence();
296
+ setupVisibilityHandler();
297
+ setupPageHideHandler();
298
+
299
+ let startTimeout: ReturnType<typeof setTimeout> | null = null;
300
+
301
+ const initHeartbeat = async () => {
302
+ if (syncReady) {
303
+ await syncReady;
304
+ }
305
+ if (!destroyed) {
306
+ startHeartbeat();
307
+ }
308
+ };
309
+
310
+ startTimeout = setTimeout(() => {
311
+ initHeartbeat();
312
+ }, 0);
313
+
314
+ return {
315
+ awareness,
316
+ document: ydoc,
317
+
318
+ destroy: () => {
319
+ if (destroyed) return;
320
+ destroyed = true;
321
+
322
+ if (startTimeout) {
323
+ clearTimeout(startTimeout);
324
+ startTimeout = null;
325
+ }
326
+ if (pendingUpdate) {
327
+ clearTimeout(pendingUpdate);
328
+ pendingUpdate = null;
329
+ }
330
+ stopHeartbeat();
331
+ awareness.off("update", onLocalAwarenessUpdate);
332
+ unsubscribeCursors?.();
333
+ unsubscribeVisibility?.();
334
+ unsubscribePageHide?.();
335
+
336
+ for (const clientId of remoteClientIds.values()) {
337
+ awareness.states.delete(clientId);
338
+ }
339
+ remoteClientIds.clear();
340
+ awareness.emit("update", [{ added: [], updated: [], removed: [] }, "remote"]);
341
+
342
+ convexClient.mutation(api.leave, { document, client });
343
+
344
+ awareness.destroy();
345
+ },
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Hash a string to a positive number for use as clientId.
351
+ */
352
+ function hashStringToNumber(str: string): number {
353
+ let hash = 0;
354
+ for (let i = 0; i < str.length; i++) {
355
+ const char = str.charCodeAt(i);
356
+ hash = ((hash << 5) - hash) + char;
357
+ hash = hash & hash; // Convert to 32-bit integer
358
+ }
359
+ return Math.abs(hash);
360
+ }
361
+
362
+ /**
363
+ * Generate a color for a client based on their ID.
364
+ */
365
+ const DEFAULT_COLORS = [
366
+ "#F87171", "#FB923C", "#FBBF24", "#A3E635",
367
+ "#34D399", "#22D3EE", "#60A5FA", "#A78BFA", "#F472B6",
368
+ ];
369
+
370
+ function getColorForClient(clientId: string): string {
371
+ const hash = hashStringToNumber(clientId);
372
+ return DEFAULT_COLORS[hash % DEFAULT_COLORS.length];
373
+ }
@@ -0,0 +1,114 @@
1
+ import { createMutex } from "lib0/mutex";
2
+ import type { ConvexClient } from "convex/browser";
3
+ import type { FunctionReference } from "convex/server";
4
+ import type { Collection } from "@tanstack/db";
5
+ import type { Persistence } from "$/client/persistence/types";
6
+ import type { SubdocManager } from "$/client/subdocs";
7
+
8
+ interface ConvexCollectionApi {
9
+ stream: FunctionReference<"query">;
10
+ insert: FunctionReference<"mutation">;
11
+ update: FunctionReference<"mutation">;
12
+ remove: FunctionReference<"mutation">;
13
+ recovery: FunctionReference<"query">;
14
+ mark: FunctionReference<"mutation">;
15
+ compact: FunctionReference<"mutation">;
16
+ material?: FunctionReference<"query">;
17
+ sessions?: FunctionReference<"query">;
18
+ cursors?: FunctionReference<"query">;
19
+ leave?: FunctionReference<"mutation">;
20
+ }
21
+
22
+ export interface ProseState {
23
+ applyingFromServer: Map<string, boolean>;
24
+ debounceTimers: Map<string, ReturnType<typeof setTimeout>>;
25
+ lastSyncedVectors: Map<string, Uint8Array>;
26
+ pendingState: Map<string, boolean>;
27
+ pendingListeners: Map<string, Set<(pending: boolean) => void>>;
28
+ fragmentObservers: Map<string, () => void>;
29
+ failedSyncQueue: Map<string, boolean>;
30
+ }
31
+
32
+ export interface CollectionContext {
33
+ collection: string;
34
+ subdocs: SubdocManager;
35
+ client: ConvexClient;
36
+ api: ConvexCollectionApi;
37
+ persistence: Persistence;
38
+ fields: Set<string>;
39
+ mutex: ReturnType<typeof createMutex>;
40
+ debounce: number;
41
+ prose: ProseState;
42
+ cleanup?: () => void;
43
+ clientId?: string;
44
+ ref?: Collection<any>;
45
+ vector?: Uint8Array;
46
+ synced?: Promise<void>;
47
+ resolve?: () => void;
48
+ }
49
+
50
+ const contexts = new Map<string, CollectionContext>();
51
+
52
+ export function getContext(collection: string): CollectionContext {
53
+ const ctx = contexts.get(collection);
54
+ if (!ctx) throw new Error(`Collection ${collection} not initialized`);
55
+ return ctx;
56
+ }
57
+
58
+ export function hasContext(collection: string): boolean {
59
+ return contexts.has(collection);
60
+ }
61
+
62
+ type InitContextConfig = Omit<
63
+ CollectionContext,
64
+ | "mutex"
65
+ | "prose"
66
+ | "cleanup"
67
+ | "clientId"
68
+ | "ref"
69
+ | "vector"
70
+ >;
71
+
72
+ function createProseState(): ProseState {
73
+ return {
74
+ applyingFromServer: new Map(),
75
+ debounceTimers: new Map(),
76
+ lastSyncedVectors: new Map(),
77
+ pendingState: new Map(),
78
+ pendingListeners: new Map(),
79
+ fragmentObservers: new Map(),
80
+ failedSyncQueue: new Map(),
81
+ };
82
+ }
83
+
84
+ export function initContext(config: InitContextConfig): CollectionContext {
85
+ let resolver: () => void;
86
+ const synced = new Promise<void>((r) => {
87
+ resolver = r;
88
+ });
89
+
90
+ const ctx: CollectionContext = {
91
+ ...config,
92
+ mutex: createMutex(),
93
+ prose: createProseState(),
94
+ synced,
95
+ resolve: resolver!,
96
+ };
97
+ contexts.set(config.collection, ctx);
98
+ return ctx;
99
+ }
100
+
101
+ export function deleteContext(collection: string): void {
102
+ contexts.delete(collection);
103
+ }
104
+
105
+ type UpdateableFields = "clientId" | "ref" | "vector" | "cleanup";
106
+
107
+ export function updateContext(
108
+ collection: string,
109
+ updates: Partial<Pick<CollectionContext, UpdateableFields>>,
110
+ ): CollectionContext {
111
+ const ctx = getContext(collection);
112
+ Object.assign(ctx, updates);
113
+ return ctx;
114
+ }
@@ -0,0 +1,78 @@
1
+ import { Effect, Context, Layer } from "effect";
2
+ import { IDBError, IDBWriteError } from "$/client/errors";
3
+ import type { KeyValueStore } from "$/client/persistence/types";
4
+
5
+ export type Seq = number;
6
+
7
+ export class SeqService extends Context.Tag("SeqService")<
8
+ SeqService,
9
+ {
10
+ readonly load: (collection: string) => Effect.Effect<Seq, IDBError>;
11
+ readonly save: (collection: string, seq: Seq) => Effect.Effect<void, IDBWriteError>;
12
+ readonly clear: (collection: string) => Effect.Effect<void, IDBError>;
13
+ }
14
+ >() {}
15
+
16
+ export function createSeqLayer(kv: KeyValueStore) {
17
+ return Layer.succeed(
18
+ SeqService,
19
+ SeqService.of({
20
+ load: (collection: string) =>
21
+ Effect.gen(function* (_) {
22
+ const key = `cursor:${collection}`;
23
+ const stored = yield* _(
24
+ Effect.tryPromise({
25
+ try: () => kv.get<Seq>(key),
26
+ catch: cause => new IDBError({ operation: "get", key, cause }),
27
+ }),
28
+ );
29
+
30
+ if (stored !== undefined) {
31
+ yield* _(
32
+ Effect.logDebug("Loaded seq from storage", {
33
+ collection,
34
+ seq: stored,
35
+ }),
36
+ );
37
+ return stored;
38
+ }
39
+
40
+ yield* _(
41
+ Effect.logDebug("No stored seq, using default", {
42
+ collection,
43
+ }),
44
+ );
45
+ return 0;
46
+ }),
47
+
48
+ save: (collection: string, seq: Seq) =>
49
+ Effect.gen(function* (_) {
50
+ const key = `cursor:${collection}`;
51
+ yield* _(
52
+ Effect.tryPromise({
53
+ try: () => kv.set(key, seq),
54
+ catch: cause => new IDBWriteError({ key, value: seq, cause }),
55
+ }),
56
+ );
57
+ yield* _(
58
+ Effect.logDebug("Seq saved", {
59
+ collection,
60
+ seq,
61
+ }),
62
+ );
63
+ }),
64
+
65
+ clear: (collection: string) =>
66
+ Effect.gen(function* (_) {
67
+ const key = `cursor:${collection}`;
68
+ yield* _(
69
+ Effect.tryPromise({
70
+ try: () => kv.del(key),
71
+ catch: cause => new IDBError({ operation: "delete", key, cause }),
72
+ }),
73
+ );
74
+ yield* _(Effect.logDebug("Seq cleared", { collection }));
75
+ }),
76
+ }),
77
+ );
78
+ }
@@ -0,0 +1,20 @@
1
+ function generateClientId(): number {
2
+ return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
3
+ }
4
+
5
+ export function getClientId(collection: string): string {
6
+ const key = `replicate:clientId:${collection}`;
7
+
8
+ if (typeof localStorage === "undefined") {
9
+ return String(generateClientId());
10
+ }
11
+
12
+ const stored = localStorage.getItem(key);
13
+ if (stored) {
14
+ return stored;
15
+ }
16
+
17
+ const clientId = String(generateClientId());
18
+ localStorage.setItem(key, clientId);
19
+ return clientId;
20
+ }
@@ -0,0 +1,122 @@
1
+ import { Effect, Context, Layer, Schedule, Duration } from "effect";
2
+ import type { ConvexClient } from "convex/browser";
3
+ import type { FunctionReference } from "convex/server";
4
+ import { NetworkError } from "$/client/errors";
5
+
6
+ interface SyncApi {
7
+ stream: FunctionReference<"query">;
8
+ recovery: FunctionReference<"query">;
9
+ compact: FunctionReference<"mutation">;
10
+ mark: FunctionReference<"mutation">;
11
+ }
12
+
13
+ export interface SyncConfig {
14
+ collection: string;
15
+ convexClient: ConvexClient;
16
+ api: SyncApi;
17
+ }
18
+
19
+ export interface CompactResult {
20
+ success: boolean;
21
+ removed: number;
22
+ retained: number;
23
+ size: number;
24
+ }
25
+
26
+ export interface RecoveryResult {
27
+ diff?: ArrayBuffer;
28
+ vector: ArrayBuffer;
29
+ cursor: number;
30
+ }
31
+
32
+ export interface StreamChange {
33
+ document: string;
34
+ bytes: ArrayBuffer;
35
+ seq: number;
36
+ type: string;
37
+ }
38
+
39
+ export interface StreamResponse {
40
+ changes: StreamChange[];
41
+ cursor: number;
42
+ more: boolean;
43
+ compact?: string;
44
+ }
45
+
46
+ const retryPolicy = Schedule.exponential("1 second").pipe(
47
+ Schedule.jittered,
48
+ Schedule.compose(Schedule.elapsed),
49
+ Schedule.whileOutput(duration => Duration.lessThan(duration, Duration.seconds(30))),
50
+ );
51
+
52
+ export class Sync extends Context.Tag("Sync")<
53
+ Sync,
54
+ {
55
+ readonly subscribe: (
56
+ cursor: number,
57
+ limit: number,
58
+ onUpdate: (response: StreamResponse) => void,
59
+ ) => Effect.Effect<() => void, NetworkError>;
60
+ readonly recover: (vector: ArrayBuffer) => Effect.Effect<RecoveryResult, NetworkError>;
61
+ readonly compact: (document: string) => Effect.Effect<CompactResult, NetworkError>;
62
+ readonly mark: (
63
+ document: string,
64
+ client: string,
65
+ seq: number,
66
+ ) => Effect.Effect<void, NetworkError>;
67
+ }
68
+ >() {}
69
+
70
+ export function createSyncLayer(config: SyncConfig) {
71
+ const { convexClient, api } = config;
72
+
73
+ return Layer.succeed(
74
+ Sync,
75
+ Sync.of({
76
+ subscribe: (cursor, limit, onUpdate) =>
77
+ Effect.gen(function* () {
78
+ const unsubscribe = yield* Effect.try({
79
+ try: () =>
80
+ convexClient.onUpdate(
81
+ api.stream,
82
+ { cursor, limit },
83
+ (response: StreamResponse) => {
84
+ onUpdate(response);
85
+ },
86
+ ),
87
+ catch: cause => new NetworkError({ operation: "subscribe", cause, retryable: true }),
88
+ });
89
+
90
+ return unsubscribe;
91
+ }),
92
+
93
+ recover: vector =>
94
+ Effect.gen(function* () {
95
+ const response = yield* Effect.tryPromise({
96
+ try: () => convexClient.query(api.recovery, { vector }),
97
+ catch: cause => new NetworkError({ operation: "recovery", cause, retryable: true }),
98
+ });
99
+
100
+ return response as RecoveryResult;
101
+ }).pipe(Effect.retry(retryPolicy)),
102
+
103
+ compact: document =>
104
+ Effect.gen(function* () {
105
+ const result = yield* Effect.tryPromise({
106
+ try: () => convexClient.mutation(api.compact, { document }),
107
+ catch: cause => new NetworkError({ operation: "compact", cause, retryable: true }),
108
+ });
109
+
110
+ return result as CompactResult;
111
+ }).pipe(Effect.retry(retryPolicy)),
112
+
113
+ mark: (document, client, seq) =>
114
+ Effect.gen(function* () {
115
+ yield* Effect.tryPromise({
116
+ try: () => convexClient.mutation(api.mark, { document, client, seq }),
117
+ catch: cause => new NetworkError({ operation: "mark", cause, retryable: true }),
118
+ });
119
+ }),
120
+ }),
121
+ );
122
+ }