@outfitter/state 0.1.0-rc.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/README.md +348 -0
- package/dist/index.d.ts +667 -0
- package/dist/index.js +425 -0
- package/package.json +53 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import { NotFoundError, Result, StorageError, ValidationError } from "@outfitter/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* A pagination cursor representing a position in a result set.
|
|
4
|
+
*
|
|
5
|
+
* Cursors are immutable (frozen) objects that encapsulate pagination state.
|
|
6
|
+
* They are intentionally opaque to prevent direct manipulation - use
|
|
7
|
+
* {@link advanceCursor} to create a new cursor with an updated position.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const result = createCursor({
|
|
12
|
+
* position: 0,
|
|
13
|
+
* metadata: { query: "status:open" },
|
|
14
|
+
* ttl: 3600000, // 1 hour
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* if (result.isOk()) {
|
|
18
|
+
* const cursor = result.value;
|
|
19
|
+
* console.log(cursor.id); // UUID
|
|
20
|
+
* console.log(cursor.position); // 0
|
|
21
|
+
* console.log(cursor.expiresAt); // Unix timestamp
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
interface Cursor {
|
|
26
|
+
/** Unique identifier for this cursor (UUID format) */
|
|
27
|
+
readonly id: string;
|
|
28
|
+
/** Current position/offset in the result set (zero-based) */
|
|
29
|
+
readonly position: number;
|
|
30
|
+
/** Optional user-defined metadata associated with this cursor */
|
|
31
|
+
readonly metadata?: Record<string, unknown>;
|
|
32
|
+
/** Time-to-live in milliseconds (optional, omitted if cursor never expires) */
|
|
33
|
+
readonly ttl?: number;
|
|
34
|
+
/** Unix timestamp (ms) when this cursor expires (computed from createdAt + ttl) */
|
|
35
|
+
readonly expiresAt?: number;
|
|
36
|
+
/** Unix timestamp (ms) when this cursor was created */
|
|
37
|
+
readonly createdAt: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Options for creating a pagination cursor.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* // Minimal options (ID auto-generated, no TTL)
|
|
45
|
+
* const opts1: CreateCursorOptions = { position: 0 };
|
|
46
|
+
*
|
|
47
|
+
* // Full options with custom ID, metadata, and TTL
|
|
48
|
+
* const opts2: CreateCursorOptions = {
|
|
49
|
+
* id: "my-cursor-id",
|
|
50
|
+
* position: 50,
|
|
51
|
+
* metadata: { query: "status:open", pageSize: 25 },
|
|
52
|
+
* ttl: 30 * 60 * 1000, // 30 minutes
|
|
53
|
+
* };
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
interface CreateCursorOptions {
|
|
57
|
+
/** Custom cursor ID (UUID generated if not provided) */
|
|
58
|
+
id?: string;
|
|
59
|
+
/** Starting position in the result set (must be non-negative) */
|
|
60
|
+
position: number;
|
|
61
|
+
/** User-defined metadata to associate with the cursor */
|
|
62
|
+
metadata?: Record<string, unknown>;
|
|
63
|
+
/** Time-to-live in milliseconds (cursor never expires if omitted) */
|
|
64
|
+
ttl?: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A store for managing pagination cursors.
|
|
68
|
+
*
|
|
69
|
+
* Cursor stores handle storage, retrieval, and expiration of cursors.
|
|
70
|
+
* Expired cursors are automatically excluded from `get()` and `has()` operations.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* const store = createCursorStore();
|
|
75
|
+
*
|
|
76
|
+
* // Store a cursor
|
|
77
|
+
* const cursor = createCursor({ position: 0 });
|
|
78
|
+
* if (cursor.isOk()) {
|
|
79
|
+
* store.set(cursor.value);
|
|
80
|
+
* }
|
|
81
|
+
*
|
|
82
|
+
* // Retrieve by ID
|
|
83
|
+
* const result = store.get("cursor-id");
|
|
84
|
+
* if (result.isOk()) {
|
|
85
|
+
* console.log(result.value.position);
|
|
86
|
+
* }
|
|
87
|
+
*
|
|
88
|
+
* // Cleanup expired cursors
|
|
89
|
+
* const pruned = store.prune();
|
|
90
|
+
* console.log(`Removed ${pruned} expired cursors`);
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
interface CursorStore {
|
|
94
|
+
/**
|
|
95
|
+
* Save or update a cursor in the store.
|
|
96
|
+
* @param cursor - The cursor to store (replaces existing if same ID)
|
|
97
|
+
*/
|
|
98
|
+
set(cursor: Cursor): void;
|
|
99
|
+
/**
|
|
100
|
+
* Retrieve a cursor by ID.
|
|
101
|
+
* @param id - The cursor ID to look up
|
|
102
|
+
* @returns Result with cursor or NotFoundError (also returned for expired cursors)
|
|
103
|
+
*/
|
|
104
|
+
get(id: string): Result<Cursor, InstanceType<typeof NotFoundError>>;
|
|
105
|
+
/**
|
|
106
|
+
* Check if a cursor exists and is not expired.
|
|
107
|
+
* @param id - The cursor ID to check
|
|
108
|
+
* @returns True if cursor exists and is valid, false otherwise
|
|
109
|
+
*/
|
|
110
|
+
has(id: string): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Delete a cursor by ID.
|
|
113
|
+
* @param id - The cursor ID to delete (no-op if not found)
|
|
114
|
+
*/
|
|
115
|
+
delete(id: string): void;
|
|
116
|
+
/**
|
|
117
|
+
* Remove all cursors from the store.
|
|
118
|
+
*/
|
|
119
|
+
clear(): void;
|
|
120
|
+
/**
|
|
121
|
+
* List all cursor IDs in the store (including expired).
|
|
122
|
+
* @returns Array of cursor IDs
|
|
123
|
+
*/
|
|
124
|
+
list(): string[];
|
|
125
|
+
/**
|
|
126
|
+
* Remove all expired cursors from the store.
|
|
127
|
+
* @returns Number of cursors that were pruned
|
|
128
|
+
*/
|
|
129
|
+
prune(): number;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* A cursor store with namespace isolation.
|
|
133
|
+
*
|
|
134
|
+
* Scoped stores prefix all cursor IDs with the scope name, preventing
|
|
135
|
+
* collisions between different contexts (e.g., "issues" vs "pull-requests").
|
|
136
|
+
*
|
|
137
|
+
* Scopes can be nested: creating a scoped store from another scoped store
|
|
138
|
+
* produces IDs like "parent:child:cursor-id".
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* const store = createCursorStore();
|
|
143
|
+
* const issueStore = createScopedStore(store, "issues");
|
|
144
|
+
* const prStore = createScopedStore(store, "prs");
|
|
145
|
+
*
|
|
146
|
+
* // These don't conflict - different namespaces
|
|
147
|
+
* issueStore.set(cursor1); // Stored as "issues:abc123"
|
|
148
|
+
* prStore.set(cursor2); // Stored as "prs:abc123"
|
|
149
|
+
*
|
|
150
|
+
* // Clear only affects the scope
|
|
151
|
+
* issueStore.clear(); // Only clears issue cursors
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
interface ScopedStore extends CursorStore {
|
|
155
|
+
/**
|
|
156
|
+
* Get the full scope path for this store.
|
|
157
|
+
* @returns Scope string (e.g., "parent:child" for nested scopes)
|
|
158
|
+
*/
|
|
159
|
+
getScope(): string;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Options for creating a persistent cursor store.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* const options: PersistentStoreOptions = {
|
|
167
|
+
* path: "/home/user/.config/myapp/cursors.json",
|
|
168
|
+
* };
|
|
169
|
+
*
|
|
170
|
+
* const store = await createPersistentStore(options);
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
interface PersistentStoreOptions {
|
|
174
|
+
/** Absolute file path for cursor persistence (JSON format) */
|
|
175
|
+
path: string;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* A cursor store that persists to disk and survives process restarts.
|
|
179
|
+
*
|
|
180
|
+
* Persistent stores use atomic writes (temp file + rename) to prevent
|
|
181
|
+
* corruption. They automatically load existing data on initialization
|
|
182
|
+
* and handle corrupted files gracefully by starting empty.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* const store = await createPersistentStore({
|
|
187
|
+
* path: "~/.config/myapp/cursors.json",
|
|
188
|
+
* });
|
|
189
|
+
*
|
|
190
|
+
* // Use like any cursor store
|
|
191
|
+
* store.set(cursor);
|
|
192
|
+
*
|
|
193
|
+
* // Flush to disk before exit
|
|
194
|
+
* await store.flush();
|
|
195
|
+
*
|
|
196
|
+
* // Cleanup resources
|
|
197
|
+
* store.dispose();
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
interface PersistentStore extends CursorStore {
|
|
201
|
+
/**
|
|
202
|
+
* Flush all in-memory cursors to disk.
|
|
203
|
+
* Uses atomic write (temp file + rename) to prevent corruption.
|
|
204
|
+
* @returns Promise that resolves when write is complete
|
|
205
|
+
*/
|
|
206
|
+
flush(): Promise<void>;
|
|
207
|
+
/**
|
|
208
|
+
* Dispose of the store and cleanup resources.
|
|
209
|
+
* Call this when the store is no longer needed.
|
|
210
|
+
*/
|
|
211
|
+
dispose(): void;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Create a new pagination cursor.
|
|
215
|
+
*
|
|
216
|
+
* Cursors are immutable and frozen. To update position, use {@link advanceCursor}.
|
|
217
|
+
* If no ID is provided, a UUID is generated automatically.
|
|
218
|
+
*
|
|
219
|
+
* @param options - Cursor creation options including position, optional ID, metadata, and TTL
|
|
220
|
+
* @returns Result containing frozen Cursor or ValidationError if position is negative
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* // Basic cursor at position 0
|
|
225
|
+
* const result = createCursor({ position: 0 });
|
|
226
|
+
*
|
|
227
|
+
* // Cursor with metadata and TTL
|
|
228
|
+
* const result = createCursor({
|
|
229
|
+
* position: 0,
|
|
230
|
+
* metadata: { filter: "active" },
|
|
231
|
+
* ttl: 3600000, // 1 hour
|
|
232
|
+
* });
|
|
233
|
+
*
|
|
234
|
+
* if (result.isOk()) {
|
|
235
|
+
* store.set(result.value);
|
|
236
|
+
* }
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
declare function createCursor(options: CreateCursorOptions): Result<Cursor, InstanceType<typeof ValidationError>>;
|
|
240
|
+
/**
|
|
241
|
+
* Advance a cursor to a new position, returning a new immutable cursor.
|
|
242
|
+
*
|
|
243
|
+
* The original cursor is not modified. All properties (ID, metadata, TTL,
|
|
244
|
+
* expiresAt, createdAt) are preserved in the new cursor.
|
|
245
|
+
*
|
|
246
|
+
* @param cursor - The cursor to advance (not modified)
|
|
247
|
+
* @param newPosition - The new position value (typically cursor.position + pageSize)
|
|
248
|
+
* @returns A new frozen Cursor with the updated position
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```typescript
|
|
252
|
+
* const cursor = createCursor({ position: 0 });
|
|
253
|
+
* if (cursor.isOk()) {
|
|
254
|
+
* // Advance by page size of 25
|
|
255
|
+
* const nextPage = advanceCursor(cursor.value, cursor.value.position + 25);
|
|
256
|
+
*
|
|
257
|
+
* console.log(cursor.value.position); // 0 (unchanged)
|
|
258
|
+
* console.log(nextPage.position); // 25
|
|
259
|
+
*
|
|
260
|
+
* // Store the advanced cursor
|
|
261
|
+
* store.set(nextPage);
|
|
262
|
+
* }
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
declare function advanceCursor(cursor: Cursor, newPosition: number): Cursor;
|
|
266
|
+
/**
|
|
267
|
+
* Check if a cursor has expired based on its TTL.
|
|
268
|
+
*
|
|
269
|
+
* Cursors without a TTL (no `expiresAt` property) never expire and
|
|
270
|
+
* this function will always return `false` for them.
|
|
271
|
+
*
|
|
272
|
+
* @param cursor - The cursor to check for expiration
|
|
273
|
+
* @returns `true` if cursor has expired, `false` if still valid or has no TTL
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* const cursor = createCursor({ position: 0, ttl: 1000 }); // 1 second TTL
|
|
278
|
+
*
|
|
279
|
+
* if (cursor.isOk()) {
|
|
280
|
+
* console.log(isExpired(cursor.value)); // false (just created)
|
|
281
|
+
*
|
|
282
|
+
* // Wait 2 seconds...
|
|
283
|
+
* await new Promise(resolve => setTimeout(resolve, 2000));
|
|
284
|
+
*
|
|
285
|
+
* console.log(isExpired(cursor.value)); // true (expired)
|
|
286
|
+
* }
|
|
287
|
+
*
|
|
288
|
+
* // Cursors without TTL never expire
|
|
289
|
+
* const eternal = createCursor({ position: 0 });
|
|
290
|
+
* if (eternal.isOk()) {
|
|
291
|
+
* console.log(isExpired(eternal.value)); // always false
|
|
292
|
+
* }
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
declare function isExpired(cursor: Cursor): boolean;
|
|
296
|
+
/**
|
|
297
|
+
* Encodes a cursor to an opaque URL-safe string.
|
|
298
|
+
*
|
|
299
|
+
* The internal structure is hidden from consumers. The cursor is serialized
|
|
300
|
+
* to JSON and then encoded using URL-safe base64 (RFC 4648 Section 5).
|
|
301
|
+
*
|
|
302
|
+
* @param cursor - Cursor to encode
|
|
303
|
+
* @returns URL-safe base64 encoded string (no +, /, or = characters)
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```typescript
|
|
307
|
+
* const cursor = createCursor({ position: 0, metadata: { query: "status:open" } });
|
|
308
|
+
* if (cursor.isOk()) {
|
|
309
|
+
* const encoded = encodeCursor(cursor.value);
|
|
310
|
+
* // encoded is a URL-safe string like "eyJpZCI6IjEyMzQ..."
|
|
311
|
+
*
|
|
312
|
+
* // Can be safely used in URLs, query params, headers
|
|
313
|
+
* const url = `/api/items?cursor=${encoded}`;
|
|
314
|
+
* }
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
declare function encodeCursor(cursor: Cursor): string;
|
|
318
|
+
/**
|
|
319
|
+
* Decodes an opaque cursor string back to a Cursor.
|
|
320
|
+
*
|
|
321
|
+
* Validates the structure after decoding, ensuring all required fields
|
|
322
|
+
* are present and have correct types.
|
|
323
|
+
*
|
|
324
|
+
* @param encoded - URL-safe base64 encoded cursor
|
|
325
|
+
* @returns Result with decoded Cursor or ValidationError if invalid
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```typescript
|
|
329
|
+
* // Successful decode
|
|
330
|
+
* const result = decodeCursor(encodedString);
|
|
331
|
+
* if (result.isOk()) {
|
|
332
|
+
* console.log(result.value.position);
|
|
333
|
+
* }
|
|
334
|
+
*
|
|
335
|
+
* // Handle invalid input
|
|
336
|
+
* const invalid = decodeCursor("not-a-valid-cursor");
|
|
337
|
+
* if (invalid.isErr()) {
|
|
338
|
+
* console.log(invalid.error.message); // "Invalid cursor: ..."
|
|
339
|
+
* }
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
declare function decodeCursor(encoded: string): Result<Cursor, InstanceType<typeof ValidationError>>;
|
|
343
|
+
/**
|
|
344
|
+
* Create an in-memory cursor store.
|
|
345
|
+
*
|
|
346
|
+
* The store automatically handles expiration: `get()` and `has()` return
|
|
347
|
+
* not-found/false for expired cursors. Use `prune()` to remove expired
|
|
348
|
+
* cursors from memory.
|
|
349
|
+
*
|
|
350
|
+
* @returns A new cursor store implementing both CursorStore and ScopedStore interfaces
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```typescript
|
|
354
|
+
* const store = createCursorStore();
|
|
355
|
+
*
|
|
356
|
+
* // Create and store a cursor
|
|
357
|
+
* const cursor = createCursor({
|
|
358
|
+
* position: 0,
|
|
359
|
+
* metadata: { query: "status:open" },
|
|
360
|
+
* ttl: 3600000, // 1 hour
|
|
361
|
+
* });
|
|
362
|
+
*
|
|
363
|
+
* if (cursor.isOk()) {
|
|
364
|
+
* store.set(cursor.value);
|
|
365
|
+
*
|
|
366
|
+
* // Retrieve later
|
|
367
|
+
* const result = store.get(cursor.value.id);
|
|
368
|
+
* if (result.isOk()) {
|
|
369
|
+
* console.log(result.value.position);
|
|
370
|
+
* }
|
|
371
|
+
* }
|
|
372
|
+
*
|
|
373
|
+
* // List all cursors
|
|
374
|
+
* console.log(store.list()); // ["cursor-id", ...]
|
|
375
|
+
*
|
|
376
|
+
* // Cleanup expired
|
|
377
|
+
* const pruned = store.prune();
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
declare function createCursorStore(): CursorStore & ScopedStore;
|
|
381
|
+
/**
|
|
382
|
+
* Create a persistent cursor store that saves to disk.
|
|
383
|
+
*
|
|
384
|
+
* The store loads existing cursors from the file on initialization.
|
|
385
|
+
* Changes are kept in memory until `flush()` is called. Uses atomic
|
|
386
|
+
* writes (temp file + rename) to prevent corruption.
|
|
387
|
+
*
|
|
388
|
+
* If the file is corrupted or invalid JSON, the store starts empty
|
|
389
|
+
* rather than throwing an error.
|
|
390
|
+
*
|
|
391
|
+
* @param options - Persistence options including the file path
|
|
392
|
+
* @returns Promise resolving to a PersistentStore
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* ```typescript
|
|
396
|
+
* // Create persistent store
|
|
397
|
+
* const store = await createPersistentStore({
|
|
398
|
+
* path: "/home/user/.config/myapp/cursors.json",
|
|
399
|
+
* });
|
|
400
|
+
*
|
|
401
|
+
* // Use like any cursor store
|
|
402
|
+
* const cursor = createCursor({ position: 0 });
|
|
403
|
+
* if (cursor.isOk()) {
|
|
404
|
+
* store.set(cursor.value);
|
|
405
|
+
* }
|
|
406
|
+
*
|
|
407
|
+
* // Flush to disk (call before process exit)
|
|
408
|
+
* await store.flush();
|
|
409
|
+
*
|
|
410
|
+
* // Cleanup when done
|
|
411
|
+
* store.dispose();
|
|
412
|
+
* ```
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* ```typescript
|
|
416
|
+
* // Combine with scoped stores for organized persistence
|
|
417
|
+
* const persistent = await createPersistentStore({
|
|
418
|
+
* path: "~/.config/myapp/cursors.json",
|
|
419
|
+
* });
|
|
420
|
+
*
|
|
421
|
+
* const issuesCursors = createScopedStore(persistent, "issues");
|
|
422
|
+
* const prsCursors = createScopedStore(persistent, "prs");
|
|
423
|
+
*
|
|
424
|
+
* // All scopes share the same persistence file
|
|
425
|
+
* await persistent.flush();
|
|
426
|
+
* ```
|
|
427
|
+
*/
|
|
428
|
+
declare function createPersistentStore(options: PersistentStoreOptions): Promise<PersistentStore>;
|
|
429
|
+
/**
|
|
430
|
+
* Create a scoped cursor store with namespace isolation.
|
|
431
|
+
*
|
|
432
|
+
* Scoped stores prefix all cursor IDs with the scope name, preventing
|
|
433
|
+
* collisions between different contexts (e.g., "issues" vs "pull-requests").
|
|
434
|
+
*
|
|
435
|
+
* Scopes can be nested: `createScopedStore(scopedStore, "child")` creates
|
|
436
|
+
* IDs like "parent:child:cursor-id".
|
|
437
|
+
*
|
|
438
|
+
* When retrieving cursors, the scope prefix is automatically stripped,
|
|
439
|
+
* so consumers see clean IDs without the namespace prefix.
|
|
440
|
+
*
|
|
441
|
+
* @param store - Parent store to scope (CursorStore or another ScopedStore for nesting)
|
|
442
|
+
* @param scope - Namespace for this scope (will be prefixed to all cursor IDs)
|
|
443
|
+
* @returns ScopedStore with isolated cursor management
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```typescript
|
|
447
|
+
* const store = createCursorStore();
|
|
448
|
+
* const issueStore = createScopedStore(store, "issues");
|
|
449
|
+
* const prStore = createScopedStore(store, "prs");
|
|
450
|
+
*
|
|
451
|
+
* // These don't conflict - different namespaces
|
|
452
|
+
* issueStore.set(cursor1); // Stored as "issues:abc123"
|
|
453
|
+
* prStore.set(cursor2); // Stored as "prs:abc123"
|
|
454
|
+
*
|
|
455
|
+
* // Retrieved cursors have clean IDs
|
|
456
|
+
* const result = issueStore.get("abc123");
|
|
457
|
+
* if (result.isOk()) {
|
|
458
|
+
* result.value.id; // "abc123" (not "issues:abc123")
|
|
459
|
+
* }
|
|
460
|
+
*
|
|
461
|
+
* // Clear only affects the scope
|
|
462
|
+
* issueStore.clear(); // Only clears issue cursors
|
|
463
|
+
* prStore.list(); // PR cursors still exist
|
|
464
|
+
* ```
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* ```typescript
|
|
468
|
+
* // Nested scopes for hierarchical organization
|
|
469
|
+
* const store = createCursorStore();
|
|
470
|
+
* const githubStore = createScopedStore(store, "github");
|
|
471
|
+
* const issuesStore = createScopedStore(githubStore, "issues");
|
|
472
|
+
*
|
|
473
|
+
* issuesStore.getScope(); // "github:issues"
|
|
474
|
+
*
|
|
475
|
+
* // Cursor stored as "github:issues:cursor-id"
|
|
476
|
+
* issuesStore.set(cursor);
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
declare function createScopedStore(store: CursorStore | ScopedStore, scope: string): ScopedStore;
|
|
480
|
+
/**
|
|
481
|
+
* Default page size when limit is not specified in cursor metadata.
|
|
482
|
+
*/
|
|
483
|
+
declare const DEFAULT_PAGE_LIMIT = 25;
|
|
484
|
+
/**
|
|
485
|
+
* A simple cursor store for pagination operations.
|
|
486
|
+
*
|
|
487
|
+
* This is a simplified interface compared to {@link CursorStore}, designed
|
|
488
|
+
* specifically for pagination helpers. It returns `null` instead of errors
|
|
489
|
+
* for missing cursors, making pagination code more straightforward.
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* ```typescript
|
|
493
|
+
* const store = createPaginationStore();
|
|
494
|
+
*
|
|
495
|
+
* // Store a cursor
|
|
496
|
+
* store.set("my-cursor", cursor);
|
|
497
|
+
*
|
|
498
|
+
* // Retrieve (returns null if not found)
|
|
499
|
+
* const cursor = store.get("my-cursor");
|
|
500
|
+
* if (cursor) {
|
|
501
|
+
* console.log(cursor.position);
|
|
502
|
+
* }
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
interface PaginationStore {
|
|
506
|
+
/**
|
|
507
|
+
* Get a cursor by ID.
|
|
508
|
+
* @param id - The cursor ID to look up
|
|
509
|
+
* @returns The cursor if found, null otherwise
|
|
510
|
+
*/
|
|
511
|
+
get(id: string): Cursor | null;
|
|
512
|
+
/**
|
|
513
|
+
* Store a cursor by ID.
|
|
514
|
+
* @param id - The ID to store under
|
|
515
|
+
* @param cursor - The cursor to store
|
|
516
|
+
*/
|
|
517
|
+
set(id: string, cursor: Cursor): void;
|
|
518
|
+
/**
|
|
519
|
+
* Delete a cursor by ID.
|
|
520
|
+
* @param id - The cursor ID to delete
|
|
521
|
+
*/
|
|
522
|
+
delete(id: string): void;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Get the default pagination store (module-level singleton).
|
|
526
|
+
*
|
|
527
|
+
* The default store is lazily initialized on first access.
|
|
528
|
+
* Use this when you want cursors to persist across multiple
|
|
529
|
+
* load/save calls within the same process.
|
|
530
|
+
*
|
|
531
|
+
* @returns The default pagination store instance
|
|
532
|
+
*
|
|
533
|
+
* @example
|
|
534
|
+
* ```typescript
|
|
535
|
+
* const store = getDefaultPaginationStore();
|
|
536
|
+
* store.set("cursor-1", cursor);
|
|
537
|
+
*
|
|
538
|
+
* // Later, same store is returned
|
|
539
|
+
* const sameStore = getDefaultPaginationStore();
|
|
540
|
+
* sameStore.get("cursor-1"); // Returns the cursor
|
|
541
|
+
* ```
|
|
542
|
+
*/
|
|
543
|
+
declare function getDefaultPaginationStore(): PaginationStore;
|
|
544
|
+
/**
|
|
545
|
+
* Create an in-memory pagination store.
|
|
546
|
+
*
|
|
547
|
+
* This is a simple Map-backed store for cursor persistence.
|
|
548
|
+
* Unlike {@link createCursorStore}, this store does not handle
|
|
549
|
+
* TTL/expiration - it's designed for simple pagination use cases.
|
|
550
|
+
*
|
|
551
|
+
* @returns A new pagination store instance
|
|
552
|
+
*
|
|
553
|
+
* @example
|
|
554
|
+
* ```typescript
|
|
555
|
+
* const store = createPaginationStore();
|
|
556
|
+
*
|
|
557
|
+
* const cursor = createCursor({ position: 0, metadata: { limit: 25 } });
|
|
558
|
+
* if (cursor.isOk()) {
|
|
559
|
+
* store.set(cursor.value.id, cursor.value);
|
|
560
|
+
* const retrieved = store.get(cursor.value.id);
|
|
561
|
+
* }
|
|
562
|
+
* ```
|
|
563
|
+
*/
|
|
564
|
+
declare function createPaginationStore(): PaginationStore;
|
|
565
|
+
/**
|
|
566
|
+
* Result of a pagination operation.
|
|
567
|
+
*
|
|
568
|
+
* @typeParam T - The type of items being paginated
|
|
569
|
+
*/
|
|
570
|
+
interface PaginationResult<T> {
|
|
571
|
+
/** The items in the current page */
|
|
572
|
+
page: T[];
|
|
573
|
+
/** The cursor for the next page, or null if this is the last page */
|
|
574
|
+
nextCursor: Cursor | null;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Extract a page of items based on cursor position.
|
|
578
|
+
*
|
|
579
|
+
* Uses `cursor.position` as the offset and `cursor.metadata.limit` as
|
|
580
|
+
* the page size (defaults to {@link DEFAULT_PAGE_LIMIT} if not specified).
|
|
581
|
+
*
|
|
582
|
+
* Returns a `nextCursor` for fetching the next page, or `null` if there
|
|
583
|
+
* are no more items (i.e., this is the last page).
|
|
584
|
+
*
|
|
585
|
+
* @typeParam T - The type of items being paginated
|
|
586
|
+
* @param items - The full array of items to paginate
|
|
587
|
+
* @param cursor - Cursor containing position (offset) and optionally limit in metadata
|
|
588
|
+
* @returns Object containing the page slice and next cursor (or null)
|
|
589
|
+
*
|
|
590
|
+
* @example
|
|
591
|
+
* ```typescript
|
|
592
|
+
* const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
593
|
+
* const cursor = createCursor({
|
|
594
|
+
* position: 0,
|
|
595
|
+
* metadata: { limit: 3 },
|
|
596
|
+
* });
|
|
597
|
+
*
|
|
598
|
+
* if (cursor.isOk()) {
|
|
599
|
+
* const { page, nextCursor } = paginate(items, cursor.value);
|
|
600
|
+
* console.log(page); // [1, 2, 3]
|
|
601
|
+
* console.log(nextCursor?.position); // 3
|
|
602
|
+
*
|
|
603
|
+
* if (nextCursor) {
|
|
604
|
+
* const { page: page2 } = paginate(items, nextCursor);
|
|
605
|
+
* console.log(page2); // [4, 5, 6]
|
|
606
|
+
* }
|
|
607
|
+
* }
|
|
608
|
+
* ```
|
|
609
|
+
*/
|
|
610
|
+
declare function paginate<T>(items: T[], cursor: Cursor): PaginationResult<T>;
|
|
611
|
+
/**
|
|
612
|
+
* Load a cursor from a pagination store.
|
|
613
|
+
*
|
|
614
|
+
* Returns `Ok(null)` if the cursor is not found (not an error).
|
|
615
|
+
* This differs from {@link CursorStore.get} which returns a `NotFoundError`.
|
|
616
|
+
*
|
|
617
|
+
* @param id - The cursor ID to load
|
|
618
|
+
* @param store - Optional store to load from (defaults to module-level store)
|
|
619
|
+
* @returns Result containing the cursor or null if not found
|
|
620
|
+
*
|
|
621
|
+
* @example
|
|
622
|
+
* ```typescript
|
|
623
|
+
* // Using default store
|
|
624
|
+
* const result = loadCursor("my-cursor");
|
|
625
|
+
* if (result.isOk()) {
|
|
626
|
+
* if (result.value) {
|
|
627
|
+
* console.log(`Found cursor at position ${result.value.position}`);
|
|
628
|
+
* } else {
|
|
629
|
+
* console.log("Cursor not found, starting fresh");
|
|
630
|
+
* }
|
|
631
|
+
* }
|
|
632
|
+
*
|
|
633
|
+
* // Using custom store
|
|
634
|
+
* const store = createPaginationStore();
|
|
635
|
+
* const result = loadCursor("my-cursor", store);
|
|
636
|
+
* ```
|
|
637
|
+
*/
|
|
638
|
+
declare function loadCursor(id: string, store?: PaginationStore): Result<Cursor | null, StorageError>;
|
|
639
|
+
/**
|
|
640
|
+
* Save a cursor to a pagination store.
|
|
641
|
+
*
|
|
642
|
+
* The cursor is stored by its `id` property.
|
|
643
|
+
*
|
|
644
|
+
* @param cursor - The cursor to save
|
|
645
|
+
* @param store - Optional store to save to (defaults to module-level store)
|
|
646
|
+
* @returns Result indicating success or storage error
|
|
647
|
+
*
|
|
648
|
+
* @example
|
|
649
|
+
* ```typescript
|
|
650
|
+
* const cursor = createCursor({
|
|
651
|
+
* id: "search-results",
|
|
652
|
+
* position: 50,
|
|
653
|
+
* metadata: { limit: 25, query: "status:open" },
|
|
654
|
+
* });
|
|
655
|
+
*
|
|
656
|
+
* if (cursor.isOk()) {
|
|
657
|
+
* // Save to default store
|
|
658
|
+
* saveCursor(cursor.value);
|
|
659
|
+
*
|
|
660
|
+
* // Or save to custom store
|
|
661
|
+
* const store = createPaginationStore();
|
|
662
|
+
* saveCursor(cursor.value, store);
|
|
663
|
+
* }
|
|
664
|
+
* ```
|
|
665
|
+
*/
|
|
666
|
+
declare function saveCursor(cursor: Cursor, store?: PaginationStore): Result<void, StorageError>;
|
|
667
|
+
export { saveCursor, paginate, loadCursor, isExpired, getDefaultPaginationStore, encodeCursor, decodeCursor, createScopedStore, createPersistentStore, createPaginationStore, createCursorStore, createCursor, advanceCursor, ScopedStore, PersistentStoreOptions, PersistentStore, PaginationStore, PaginationResult, DEFAULT_PAGE_LIMIT, CursorStore, Cursor, CreateCursorOptions };
|