@sylphx/lens-server 1.11.2 → 2.0.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/dist/index.d.ts +1244 -260
- package/dist/index.js +1700 -1158
- package/package.json +2 -2
- package/src/context/index.test.ts +425 -0
- package/src/context/index.ts +90 -0
- package/src/e2e/server.test.ts +215 -433
- package/src/handlers/framework.ts +294 -0
- package/src/handlers/http.test.ts +215 -0
- package/src/handlers/http.ts +189 -0
- package/src/handlers/index.ts +55 -0
- package/src/handlers/unified.ts +114 -0
- package/src/handlers/ws-types.ts +126 -0
- package/src/handlers/ws.ts +669 -0
- package/src/index.ts +127 -24
- package/src/plugin/index.ts +41 -0
- package/src/plugin/op-log.ts +286 -0
- package/src/plugin/optimistic.ts +375 -0
- package/src/plugin/types.ts +551 -0
- package/src/reconnect/index.ts +9 -0
- package/src/reconnect/operation-log.test.ts +480 -0
- package/src/reconnect/operation-log.ts +450 -0
- package/src/server/create.test.ts +256 -2193
- package/src/server/create.ts +285 -1481
- package/src/server/dataloader.ts +60 -0
- package/src/server/selection.ts +44 -0
- package/src/server/types.ts +289 -0
- package/src/sse/handler.ts +123 -56
- package/src/state/index.ts +9 -11
- package/src/storage/index.ts +26 -0
- package/src/storage/memory.ts +279 -0
- package/src/storage/types.ts +205 -0
- package/src/state/graph-state-manager.test.ts +0 -1105
- package/src/state/graph-state-manager.ts +0 -890
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sylphx/lens-server - Storage
|
|
3
|
+
*
|
|
4
|
+
* Storage adapters for opLog plugin.
|
|
5
|
+
*
|
|
6
|
+
* Built-in:
|
|
7
|
+
* - `memoryStorage()` - In-memory (default, for long-running servers)
|
|
8
|
+
*
|
|
9
|
+
* External packages (install separately):
|
|
10
|
+
* - `@sylphx/lens-storage-redis` - Redis via ioredis
|
|
11
|
+
* - `@sylphx/lens-storage-upstash` - Upstash Redis HTTP (serverless/edge)
|
|
12
|
+
* - `@sylphx/lens-storage-vercel-kv` - Vercel KV (Next.js/Vercel)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// In-memory (default)
|
|
16
|
+
export { memoryStorage } from "./memory.js";
|
|
17
|
+
|
|
18
|
+
// Types (for implementing custom storage adapters)
|
|
19
|
+
export {
|
|
20
|
+
DEFAULT_STORAGE_CONFIG,
|
|
21
|
+
type EmitResult,
|
|
22
|
+
type OpLogStorage,
|
|
23
|
+
type OpLogStorageConfig,
|
|
24
|
+
type StoredEntityState,
|
|
25
|
+
type StoredPatchEntry,
|
|
26
|
+
} from "./types.js";
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sylphx/lens-server - Memory Storage
|
|
3
|
+
*
|
|
4
|
+
* In-memory storage adapter for opLog plugin.
|
|
5
|
+
* Default storage for long-running servers.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - O(1) state and version lookups
|
|
9
|
+
* - Bounded patch history per entity
|
|
10
|
+
* - Automatic cleanup of old patches
|
|
11
|
+
*
|
|
12
|
+
* Memory: O(entities × maxPatchesPerEntity)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { PatchOperation } from "@sylphx/lens-core";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_STORAGE_CONFIG,
|
|
18
|
+
type EmitResult,
|
|
19
|
+
type OpLogStorage,
|
|
20
|
+
type OpLogStorageConfig,
|
|
21
|
+
type StoredPatchEntry,
|
|
22
|
+
} from "./types.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Entity key for internal storage.
|
|
26
|
+
*/
|
|
27
|
+
type EntityKey = string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Internal entity state.
|
|
31
|
+
*/
|
|
32
|
+
interface EntityState {
|
|
33
|
+
data: Record<string, unknown>;
|
|
34
|
+
version: number;
|
|
35
|
+
patches: StoredPatchEntry[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create entity key from entity type and ID.
|
|
40
|
+
*/
|
|
41
|
+
function makeKey(entity: string, entityId: string): EntityKey {
|
|
42
|
+
return `${entity}:${entityId}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compute JSON Patch operations between two states.
|
|
47
|
+
*/
|
|
48
|
+
function computePatch(
|
|
49
|
+
oldState: Record<string, unknown>,
|
|
50
|
+
newState: Record<string, unknown>,
|
|
51
|
+
): PatchOperation[] {
|
|
52
|
+
const patch: PatchOperation[] = [];
|
|
53
|
+
const oldKeys = new Set(Object.keys(oldState));
|
|
54
|
+
const newKeys = new Set(Object.keys(newState));
|
|
55
|
+
|
|
56
|
+
// Additions and replacements
|
|
57
|
+
for (const key of newKeys) {
|
|
58
|
+
const oldValue = oldState[key];
|
|
59
|
+
const newValue = newState[key];
|
|
60
|
+
|
|
61
|
+
if (!oldKeys.has(key)) {
|
|
62
|
+
// New field
|
|
63
|
+
patch.push({ op: "add", path: `/${key}`, value: newValue });
|
|
64
|
+
} else if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
|
65
|
+
// Changed field
|
|
66
|
+
patch.push({ op: "replace", path: `/${key}`, value: newValue });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Deletions
|
|
71
|
+
for (const key of oldKeys) {
|
|
72
|
+
if (!newKeys.has(key)) {
|
|
73
|
+
patch.push({ op: "remove", path: `/${key}` });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return patch;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Hash entity state for change detection.
|
|
82
|
+
*/
|
|
83
|
+
function hashState(state: Record<string, unknown>): string {
|
|
84
|
+
return JSON.stringify(state);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create an in-memory storage adapter.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const storage = memoryStorage();
|
|
93
|
+
*
|
|
94
|
+
* // Or with custom config
|
|
95
|
+
* const storage = memoryStorage({
|
|
96
|
+
* maxPatchesPerEntity: 500,
|
|
97
|
+
* maxPatchAge: 60000,
|
|
98
|
+
* });
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export function memoryStorage(config: OpLogStorageConfig = {}): OpLogStorage {
|
|
102
|
+
const cfg = { ...DEFAULT_STORAGE_CONFIG, ...config };
|
|
103
|
+
const entities = new Map<EntityKey, EntityState>();
|
|
104
|
+
let cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
105
|
+
|
|
106
|
+
// Start cleanup timer
|
|
107
|
+
if (cfg.cleanupInterval > 0) {
|
|
108
|
+
cleanupTimer = setInterval(() => cleanup(), cfg.cleanupInterval);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Cleanup old patches based on age.
|
|
113
|
+
*/
|
|
114
|
+
function cleanup(): void {
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
const minTimestamp = now - cfg.maxPatchAge;
|
|
117
|
+
|
|
118
|
+
for (const state of entities.values()) {
|
|
119
|
+
// Remove patches older than maxPatchAge
|
|
120
|
+
state.patches = state.patches.filter((p) => p.timestamp >= minTimestamp);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Trim patches to maxPatchesPerEntity.
|
|
126
|
+
*/
|
|
127
|
+
function trimPatches(state: EntityState): void {
|
|
128
|
+
if (state.patches.length > cfg.maxPatchesPerEntity) {
|
|
129
|
+
// Remove oldest patches
|
|
130
|
+
state.patches = state.patches.slice(-cfg.maxPatchesPerEntity);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
async emit(entity, entityId, data): Promise<EmitResult> {
|
|
136
|
+
const key = makeKey(entity, entityId);
|
|
137
|
+
const existing = entities.get(key);
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
|
|
140
|
+
if (!existing) {
|
|
141
|
+
// First emit - no previous state
|
|
142
|
+
const newState: EntityState = {
|
|
143
|
+
data: { ...data },
|
|
144
|
+
version: 1,
|
|
145
|
+
patches: [],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// No patch for first emit (full state is sent instead)
|
|
149
|
+
entities.set(key, newState);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
version: 1,
|
|
153
|
+
patch: null,
|
|
154
|
+
changed: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check if state actually changed
|
|
159
|
+
const oldHash = hashState(existing.data);
|
|
160
|
+
const newHash = hashState(data);
|
|
161
|
+
|
|
162
|
+
if (oldHash === newHash) {
|
|
163
|
+
return {
|
|
164
|
+
version: existing.version,
|
|
165
|
+
patch: null,
|
|
166
|
+
changed: false,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Compute patch
|
|
171
|
+
const patch = computePatch(existing.data, data);
|
|
172
|
+
|
|
173
|
+
// Update state
|
|
174
|
+
const newVersion = existing.version + 1;
|
|
175
|
+
existing.data = { ...data };
|
|
176
|
+
existing.version = newVersion;
|
|
177
|
+
|
|
178
|
+
// Append patch to log
|
|
179
|
+
if (patch.length > 0) {
|
|
180
|
+
existing.patches.push({
|
|
181
|
+
version: newVersion,
|
|
182
|
+
patch,
|
|
183
|
+
timestamp: now,
|
|
184
|
+
});
|
|
185
|
+
trimPatches(existing);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
version: newVersion,
|
|
190
|
+
patch: patch.length > 0 ? patch : null,
|
|
191
|
+
changed: true,
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
async getState(entity, entityId): Promise<Record<string, unknown> | null> {
|
|
196
|
+
const key = makeKey(entity, entityId);
|
|
197
|
+
const state = entities.get(key);
|
|
198
|
+
return state ? { ...state.data } : null;
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
async getVersion(entity, entityId): Promise<number> {
|
|
202
|
+
const key = makeKey(entity, entityId);
|
|
203
|
+
const state = entities.get(key);
|
|
204
|
+
return state?.version ?? 0;
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
async getLatestPatch(entity, entityId): Promise<PatchOperation[] | null> {
|
|
208
|
+
const key = makeKey(entity, entityId);
|
|
209
|
+
const state = entities.get(key);
|
|
210
|
+
|
|
211
|
+
if (!state || state.patches.length === 0) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return state.patches[state.patches.length - 1].patch;
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
async getPatchesSince(entity, entityId, sinceVersion): Promise<PatchOperation[][] | null> {
|
|
219
|
+
const key = makeKey(entity, entityId);
|
|
220
|
+
const state = entities.get(key);
|
|
221
|
+
|
|
222
|
+
if (!state) {
|
|
223
|
+
return sinceVersion === 0 ? [] : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Already up to date
|
|
227
|
+
if (sinceVersion >= state.version) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Find patches since the given version
|
|
232
|
+
const relevantPatches = state.patches.filter((p) => p.version > sinceVersion);
|
|
233
|
+
|
|
234
|
+
if (relevantPatches.length === 0) {
|
|
235
|
+
// No patches in log - version too old
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Verify continuity
|
|
240
|
+
relevantPatches.sort((a, b) => a.version - b.version);
|
|
241
|
+
|
|
242
|
+
// First patch must be sinceVersion + 1
|
|
243
|
+
if (relevantPatches[0].version !== sinceVersion + 1) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check for gaps
|
|
248
|
+
for (let i = 1; i < relevantPatches.length; i++) {
|
|
249
|
+
if (relevantPatches[i].version !== relevantPatches[i - 1].version + 1) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return relevantPatches.map((p) => p.patch);
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
async has(entity, entityId): Promise<boolean> {
|
|
258
|
+
const key = makeKey(entity, entityId);
|
|
259
|
+
return entities.has(key);
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
async delete(entity, entityId): Promise<void> {
|
|
263
|
+
const key = makeKey(entity, entityId);
|
|
264
|
+
entities.delete(key);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
async clear(): Promise<void> {
|
|
268
|
+
entities.clear();
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
async dispose(): Promise<void> {
|
|
272
|
+
if (cleanupTimer) {
|
|
273
|
+
clearInterval(cleanupTimer);
|
|
274
|
+
cleanupTimer = null;
|
|
275
|
+
}
|
|
276
|
+
entities.clear();
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sylphx/lens-server - Storage Types
|
|
3
|
+
*
|
|
4
|
+
* Storage adapter interface for opLog plugin.
|
|
5
|
+
* Enables serverless support by abstracting state/version/patch storage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PatchOperation } from "@sylphx/lens-core";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Entity state stored in the operation log.
|
|
12
|
+
*/
|
|
13
|
+
export interface StoredEntityState {
|
|
14
|
+
/** Canonical state data */
|
|
15
|
+
data: Record<string, unknown>;
|
|
16
|
+
/** Current version */
|
|
17
|
+
version: number;
|
|
18
|
+
/** Timestamp of last update */
|
|
19
|
+
updatedAt: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Operation log entry stored in storage.
|
|
24
|
+
*/
|
|
25
|
+
export interface StoredPatchEntry {
|
|
26
|
+
/** Version this patch creates */
|
|
27
|
+
version: number;
|
|
28
|
+
/** Patch operations */
|
|
29
|
+
patch: PatchOperation[];
|
|
30
|
+
/** Timestamp when patch was created */
|
|
31
|
+
timestamp: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Result from emit operation.
|
|
36
|
+
*/
|
|
37
|
+
export interface EmitResult {
|
|
38
|
+
/** New version after emit */
|
|
39
|
+
version: number;
|
|
40
|
+
/** Computed patch (null if first emit) */
|
|
41
|
+
patch: PatchOperation[] | null;
|
|
42
|
+
/** Whether state actually changed */
|
|
43
|
+
changed: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Storage adapter interface for opLog.
|
|
48
|
+
*
|
|
49
|
+
* Implementations:
|
|
50
|
+
* - `memoryStorage()` - In-memory (default, for long-running servers)
|
|
51
|
+
* - `redisStorage()` - Redis/Upstash (for serverless)
|
|
52
|
+
* - `kvStorage()` - Cloudflare KV, Vercel KV
|
|
53
|
+
*
|
|
54
|
+
* All methods are async to support external storage.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* // Default (in-memory)
|
|
59
|
+
* const app = createApp({
|
|
60
|
+
* router,
|
|
61
|
+
* plugins: [opLog()],
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* // With Redis for serverless
|
|
65
|
+
* const app = createApp({
|
|
66
|
+
* router,
|
|
67
|
+
* plugins: [opLog({
|
|
68
|
+
* storage: redisStorage({ url: process.env.REDIS_URL }),
|
|
69
|
+
* })],
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export interface OpLogStorage {
|
|
74
|
+
/**
|
|
75
|
+
* Emit new state for an entity.
|
|
76
|
+
* This is an atomic operation that:
|
|
77
|
+
* 1. Computes patch from previous state (if exists)
|
|
78
|
+
* 2. Stores new state with incremented version
|
|
79
|
+
* 3. Appends patch to operation log
|
|
80
|
+
*
|
|
81
|
+
* @param entity - Entity type name
|
|
82
|
+
* @param entityId - Entity ID
|
|
83
|
+
* @param data - New state data
|
|
84
|
+
* @returns Emit result with version, patch, and changed flag
|
|
85
|
+
*/
|
|
86
|
+
emit(entity: string, entityId: string, data: Record<string, unknown>): Promise<EmitResult>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get current canonical state for an entity.
|
|
90
|
+
*
|
|
91
|
+
* @param entity - Entity type name
|
|
92
|
+
* @param entityId - Entity ID
|
|
93
|
+
* @returns State data or null if not found
|
|
94
|
+
*/
|
|
95
|
+
getState(entity: string, entityId: string): Promise<Record<string, unknown> | null>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get current version for an entity.
|
|
99
|
+
* Returns 0 if entity doesn't exist.
|
|
100
|
+
*
|
|
101
|
+
* @param entity - Entity type name
|
|
102
|
+
* @param entityId - Entity ID
|
|
103
|
+
* @returns Current version (0 if not found)
|
|
104
|
+
*/
|
|
105
|
+
getVersion(entity: string, entityId: string): Promise<number>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the latest patch for an entity.
|
|
109
|
+
* Returns null if no patches available.
|
|
110
|
+
*
|
|
111
|
+
* @param entity - Entity type name
|
|
112
|
+
* @param entityId - Entity ID
|
|
113
|
+
* @returns Latest patch or null
|
|
114
|
+
*/
|
|
115
|
+
getLatestPatch(entity: string, entityId: string): Promise<PatchOperation[] | null>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get all patches since a given version.
|
|
119
|
+
* Used for reconnection to bring client up to date.
|
|
120
|
+
*
|
|
121
|
+
* @param entity - Entity type name
|
|
122
|
+
* @param entityId - Entity ID
|
|
123
|
+
* @param sinceVersion - Client's current version
|
|
124
|
+
* @returns Array of patches (one per version), or null if too old
|
|
125
|
+
*/
|
|
126
|
+
getPatchesSince(
|
|
127
|
+
entity: string,
|
|
128
|
+
entityId: string,
|
|
129
|
+
sinceVersion: number,
|
|
130
|
+
): Promise<PatchOperation[][] | null>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if entity exists in storage.
|
|
134
|
+
*
|
|
135
|
+
* @param entity - Entity type name
|
|
136
|
+
* @param entityId - Entity ID
|
|
137
|
+
* @returns True if entity exists
|
|
138
|
+
*/
|
|
139
|
+
has(entity: string, entityId: string): Promise<boolean>;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Delete an entity from storage.
|
|
143
|
+
* Removes state, version, and all patches.
|
|
144
|
+
*
|
|
145
|
+
* @param entity - Entity type name
|
|
146
|
+
* @param entityId - Entity ID
|
|
147
|
+
*/
|
|
148
|
+
delete(entity: string, entityId: string): Promise<void>;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Clear all data from storage.
|
|
152
|
+
* Used for testing.
|
|
153
|
+
*/
|
|
154
|
+
clear(): Promise<void>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Dispose storage resources.
|
|
158
|
+
* Called when shutting down.
|
|
159
|
+
*/
|
|
160
|
+
dispose?(): Promise<void>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Configuration for operation log storage.
|
|
165
|
+
*/
|
|
166
|
+
export interface OpLogStorageConfig {
|
|
167
|
+
/**
|
|
168
|
+
* Maximum number of patches to keep per entity.
|
|
169
|
+
* Older patches are evicted when limit is reached.
|
|
170
|
+
* @default 1000
|
|
171
|
+
*/
|
|
172
|
+
maxPatchesPerEntity?: number;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Maximum age of patches in milliseconds.
|
|
176
|
+
* Patches older than this are evicted.
|
|
177
|
+
* @default 300000 (5 minutes)
|
|
178
|
+
*/
|
|
179
|
+
maxPatchAge?: number;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Cleanup interval in milliseconds.
|
|
183
|
+
* Set to 0 to disable automatic cleanup.
|
|
184
|
+
* @default 60000 (1 minute)
|
|
185
|
+
*/
|
|
186
|
+
cleanupInterval?: number;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Maximum retries for emit operations on version conflict.
|
|
190
|
+
* Only applies to external storage (Redis, Upstash, Vercel KV).
|
|
191
|
+
* Set to 0 to disable retries (fail immediately on conflict).
|
|
192
|
+
* @default 3
|
|
193
|
+
*/
|
|
194
|
+
maxRetries?: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Default storage configuration.
|
|
199
|
+
*/
|
|
200
|
+
export const DEFAULT_STORAGE_CONFIG: Required<OpLogStorageConfig> = {
|
|
201
|
+
maxPatchesPerEntity: 1000,
|
|
202
|
+
maxPatchAge: 5 * 60 * 1000, // 5 minutes
|
|
203
|
+
cleanupInterval: 60 * 1000, // 1 minute
|
|
204
|
+
maxRetries: 3,
|
|
205
|
+
};
|