@outfitter/state 0.2.4 → 0.2.5

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.
@@ -0,0 +1,154 @@
1
+ // @bun
2
+ // packages/state/src/internal/cursor.ts
3
+ import { Result, ValidationError } from "@outfitter/contracts";
4
+ function createCursor(options) {
5
+ if (options.position < 0) {
6
+ return Result.err(new ValidationError({
7
+ message: "Position must be non-negative",
8
+ field: "position"
9
+ }));
10
+ }
11
+ const createdAt = Date.now();
12
+ const id = options.id ?? crypto.randomUUID();
13
+ const cursor = Object.freeze({
14
+ id,
15
+ position: options.position,
16
+ createdAt,
17
+ ...options.metadata !== undefined && { metadata: options.metadata },
18
+ ...options.ttl !== undefined && { ttl: options.ttl },
19
+ ...options.ttl !== undefined && { expiresAt: createdAt + options.ttl }
20
+ });
21
+ return Result.ok(cursor);
22
+ }
23
+ function advanceCursor(cursor, newPosition) {
24
+ const newCursor = Object.freeze({
25
+ id: cursor.id,
26
+ position: newPosition,
27
+ createdAt: cursor.createdAt,
28
+ ...cursor.metadata !== undefined && { metadata: cursor.metadata },
29
+ ...cursor.ttl !== undefined && { ttl: cursor.ttl },
30
+ ...cursor.expiresAt !== undefined && { expiresAt: cursor.expiresAt }
31
+ });
32
+ return newCursor;
33
+ }
34
+ function isExpired(cursor) {
35
+ if (cursor.expiresAt === undefined) {
36
+ return false;
37
+ }
38
+ return Date.now() > cursor.expiresAt;
39
+ }
40
+ function toBase64Url(base64) {
41
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
42
+ }
43
+ var base64Encoder = new TextEncoder;
44
+ var base64Decoder = new TextDecoder;
45
+ function toBase64(value) {
46
+ const bytes = base64Encoder.encode(value);
47
+ let binary = "";
48
+ for (const byte of bytes) {
49
+ binary += String.fromCharCode(byte);
50
+ }
51
+ return btoa(binary);
52
+ }
53
+ function fromBase64(base64) {
54
+ const binary = atob(base64);
55
+ const bytes = new Uint8Array(binary.length);
56
+ for (let i = 0;i < binary.length; i += 1) {
57
+ bytes[i] = binary.charCodeAt(i);
58
+ }
59
+ return base64Decoder.decode(bytes);
60
+ }
61
+ function fromBase64Url(base64Url) {
62
+ let base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
63
+ const padLength = (4 - base64.length % 4) % 4;
64
+ base64 += "=".repeat(padLength);
65
+ return base64;
66
+ }
67
+ function encodeCursor(cursor) {
68
+ const json = JSON.stringify(cursor);
69
+ const base64 = toBase64(json);
70
+ return toBase64Url(base64);
71
+ }
72
+ function decodeCursor(encoded) {
73
+ let json;
74
+ try {
75
+ const base64 = fromBase64Url(encoded);
76
+ json = fromBase64(base64);
77
+ } catch {
78
+ return Result.err(new ValidationError({
79
+ message: "Invalid cursor: failed to decode base64",
80
+ field: "cursor"
81
+ }));
82
+ }
83
+ let data;
84
+ try {
85
+ data = JSON.parse(json);
86
+ } catch {
87
+ return Result.err(new ValidationError({
88
+ message: "Invalid cursor: failed to parse JSON",
89
+ field: "cursor"
90
+ }));
91
+ }
92
+ if (typeof data !== "object" || data === null) {
93
+ return Result.err(new ValidationError({
94
+ message: "Invalid cursor: expected object",
95
+ field: "cursor"
96
+ }));
97
+ }
98
+ const obj = data;
99
+ if (typeof obj.id !== "string") {
100
+ return Result.err(new ValidationError({
101
+ message: "Invalid cursor: missing or invalid 'id' field",
102
+ field: "cursor.id"
103
+ }));
104
+ }
105
+ if (typeof obj.position !== "number") {
106
+ return Result.err(new ValidationError({
107
+ message: "Invalid cursor: missing or invalid 'position' field",
108
+ field: "cursor.position"
109
+ }));
110
+ }
111
+ if (obj.position < 0) {
112
+ return Result.err(new ValidationError({
113
+ message: "Invalid cursor: position must be non-negative",
114
+ field: "cursor.position"
115
+ }));
116
+ }
117
+ if (typeof obj.createdAt !== "number") {
118
+ return Result.err(new ValidationError({
119
+ message: "Invalid cursor: missing or invalid 'createdAt' field",
120
+ field: "cursor.createdAt"
121
+ }));
122
+ }
123
+ if (obj.metadata !== undefined && (typeof obj.metadata !== "object" || obj.metadata === null)) {
124
+ return Result.err(new ValidationError({
125
+ message: "Invalid cursor: 'metadata' must be an object",
126
+ field: "cursor.metadata"
127
+ }));
128
+ }
129
+ if (obj.ttl !== undefined && typeof obj.ttl !== "number") {
130
+ return Result.err(new ValidationError({
131
+ message: "Invalid cursor: 'ttl' must be a number",
132
+ field: "cursor.ttl"
133
+ }));
134
+ }
135
+ if (obj.expiresAt !== undefined && typeof obj.expiresAt !== "number") {
136
+ return Result.err(new ValidationError({
137
+ message: "Invalid cursor: 'expiresAt' must be a number",
138
+ field: "cursor.expiresAt"
139
+ }));
140
+ }
141
+ const cursor = Object.freeze({
142
+ id: obj.id,
143
+ position: obj.position,
144
+ createdAt: obj.createdAt,
145
+ ...obj.metadata !== undefined && {
146
+ metadata: obj.metadata
147
+ },
148
+ ...obj.ttl !== undefined && { ttl: obj.ttl },
149
+ ...obj.expiresAt !== undefined && { expiresAt: obj.expiresAt }
150
+ });
151
+ return Result.ok(cursor);
152
+ }
153
+
154
+ export { createCursor, advanceCursor, isExpired, encodeCursor, decodeCursor };
@@ -0,0 +1,264 @@
1
+ import { NotFoundError, Result } 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
+ /** Unix timestamp (ms) when this cursor was created */
27
+ readonly createdAt: number;
28
+ /** Unix timestamp (ms) when this cursor expires (computed from createdAt + ttl) */
29
+ readonly expiresAt?: number;
30
+ /** Unique identifier for this cursor (UUID format) */
31
+ readonly id: string;
32
+ /** Optional user-defined metadata associated with this cursor */
33
+ readonly metadata?: Record<string, unknown>;
34
+ /** Current position/offset in the result set (zero-based) */
35
+ readonly position: number;
36
+ /** Time-to-live in milliseconds (optional, omitted if cursor never expires) */
37
+ readonly ttl?: 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
+ /** User-defined metadata to associate with the cursor */
60
+ metadata?: Record<string, unknown>;
61
+ /** Starting position in the result set (must be non-negative) */
62
+ position: number;
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
+ * Remove all cursors from the store.
96
+ */
97
+ clear(): void;
98
+ /**
99
+ * Delete a cursor by ID.
100
+ * @param id - The cursor ID to delete (no-op if not found)
101
+ */
102
+ delete(id: string): void;
103
+ /**
104
+ * Retrieve a cursor by ID.
105
+ * @param id - The cursor ID to look up
106
+ * @returns Result with cursor or NotFoundError (also returned for expired cursors)
107
+ */
108
+ get(id: string): Result<Cursor, InstanceType<typeof NotFoundError>>;
109
+ /**
110
+ * Check if a cursor exists and is not expired.
111
+ * @param id - The cursor ID to check
112
+ * @returns True if cursor exists and is valid, false otherwise
113
+ */
114
+ has(id: string): boolean;
115
+ /**
116
+ * List all cursor IDs in the store (including expired).
117
+ * @returns Array of cursor IDs
118
+ */
119
+ list(): string[];
120
+ /**
121
+ * Remove all expired cursors from the store.
122
+ * @returns Number of cursors that were pruned
123
+ */
124
+ prune(): number;
125
+ /**
126
+ * Save or update a cursor in the store.
127
+ * @param cursor - The cursor to store (replaces existing if same ID)
128
+ */
129
+ set(cursor: Cursor): void;
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
+ * Dispose of the store and cleanup resources.
203
+ * Call this when the store is no longer needed.
204
+ */
205
+ dispose(): void;
206
+ /**
207
+ * Flush all in-memory cursors to disk.
208
+ * Uses atomic write (temp file + rename) to prevent corruption.
209
+ * @returns Promise that resolves when write is complete
210
+ */
211
+ flush(): Promise<void>;
212
+ }
213
+ /**
214
+ * A simple cursor store for pagination operations.
215
+ *
216
+ * This is a simplified interface compared to {@link CursorStore}, designed
217
+ * specifically for pagination helpers. It returns `null` instead of errors
218
+ * for missing cursors, making pagination code more straightforward.
219
+ *
220
+ * @example
221
+ * ```typescript
222
+ * const store = createPaginationStore();
223
+ *
224
+ * // Store a cursor
225
+ * store.set("my-cursor", cursor);
226
+ *
227
+ * // Retrieve (returns null if not found)
228
+ * const cursor = store.get("my-cursor");
229
+ * if (cursor) {
230
+ * console.log(cursor.position);
231
+ * }
232
+ * ```
233
+ */
234
+ interface PaginationStore {
235
+ /**
236
+ * Delete a cursor by ID.
237
+ * @param id - The cursor ID to delete
238
+ */
239
+ delete(id: string): void;
240
+ /**
241
+ * Get a cursor by ID.
242
+ * @param id - The cursor ID to look up
243
+ * @returns The cursor if found, null otherwise
244
+ */
245
+ get(id: string): Cursor | null;
246
+ /**
247
+ * Store a cursor by ID.
248
+ * @param id - The ID to store under
249
+ * @param cursor - The cursor to store
250
+ */
251
+ set(id: string, cursor: Cursor): void;
252
+ }
253
+ /**
254
+ * Result of a pagination operation.
255
+ *
256
+ * @typeParam T - The type of items being paginated
257
+ */
258
+ interface PaginationResult<T> {
259
+ /** The cursor for the next page, or null if this is the last page */
260
+ nextCursor: Cursor | null;
261
+ /** The items in the current page */
262
+ page: T[];
263
+ }
264
+ export { Cursor, CreateCursorOptions, CursorStore, ScopedStore, PersistentStoreOptions, PersistentStore, PaginationStore, PaginationResult };
@@ -0,0 +1,133 @@
1
+ import { CreateCursorOptions, Cursor } from "./state-6h22x3e6.js";
2
+ import { Result, ValidationError } from "@outfitter/contracts";
3
+ /**
4
+ * Create a new pagination cursor.
5
+ *
6
+ * Cursors are immutable and frozen. To update position, use {@link advanceCursor}.
7
+ * If no ID is provided, a UUID is generated automatically.
8
+ *
9
+ * @param options - Cursor creation options including position, optional ID, metadata, and TTL
10
+ * @returns Result containing frozen Cursor or ValidationError if position is negative
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * // Basic cursor at position 0
15
+ * const result = createCursor({ position: 0 });
16
+ *
17
+ * // Cursor with metadata and TTL
18
+ * const result = createCursor({
19
+ * position: 0,
20
+ * metadata: { filter: "active" },
21
+ * ttl: 3600000, // 1 hour
22
+ * });
23
+ *
24
+ * if (result.isOk()) {
25
+ * store.set(result.value);
26
+ * }
27
+ * ```
28
+ */
29
+ declare function createCursor(options: CreateCursorOptions): Result<Cursor, InstanceType<typeof ValidationError>>;
30
+ /**
31
+ * Advance a cursor to a new position, returning a new immutable cursor.
32
+ *
33
+ * The original cursor is not modified. All properties (ID, metadata, TTL,
34
+ * expiresAt, createdAt) are preserved in the new cursor.
35
+ *
36
+ * @param cursor - The cursor to advance (not modified)
37
+ * @param newPosition - The new position value (typically cursor.position + pageSize)
38
+ * @returns A new frozen Cursor with the updated position
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const cursor = createCursor({ position: 0 });
43
+ * if (cursor.isOk()) {
44
+ * // Advance by page size of 25
45
+ * const nextPage = advanceCursor(cursor.value, cursor.value.position + 25);
46
+ *
47
+ * console.log(cursor.value.position); // 0 (unchanged)
48
+ * console.log(nextPage.position); // 25
49
+ *
50
+ * // Store the advanced cursor
51
+ * store.set(nextPage);
52
+ * }
53
+ * ```
54
+ */
55
+ declare function advanceCursor(cursor: Cursor, newPosition: number): Cursor;
56
+ /**
57
+ * Check if a cursor has expired based on its TTL.
58
+ *
59
+ * Cursors without a TTL (no `expiresAt` property) never expire and
60
+ * this function will always return `false` for them.
61
+ *
62
+ * @param cursor - The cursor to check for expiration
63
+ * @returns `true` if cursor has expired, `false` if still valid or has no TTL
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const cursor = createCursor({ position: 0, ttl: 1000 }); // 1 second TTL
68
+ *
69
+ * if (cursor.isOk()) {
70
+ * console.log(isExpired(cursor.value)); // false (just created)
71
+ *
72
+ * // Wait 2 seconds...
73
+ * await new Promise(resolve => setTimeout(resolve, 2000));
74
+ *
75
+ * console.log(isExpired(cursor.value)); // true (expired)
76
+ * }
77
+ *
78
+ * // Cursors without TTL never expire
79
+ * const eternal = createCursor({ position: 0 });
80
+ * if (eternal.isOk()) {
81
+ * console.log(isExpired(eternal.value)); // always false
82
+ * }
83
+ * ```
84
+ */
85
+ declare function isExpired(cursor: Cursor): boolean;
86
+ /**
87
+ * Encodes a cursor to an opaque URL-safe string.
88
+ *
89
+ * The internal structure is hidden from consumers. The cursor is serialized
90
+ * to JSON and then encoded using URL-safe base64 (RFC 4648 Section 5).
91
+ *
92
+ * @param cursor - Cursor to encode
93
+ * @returns URL-safe base64 encoded string (no +, /, or = characters)
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * const cursor = createCursor({ position: 0, metadata: { query: "status:open" } });
98
+ * if (cursor.isOk()) {
99
+ * const encoded = encodeCursor(cursor.value);
100
+ * // encoded is a URL-safe string like "eyJpZCI6IjEyMzQ..."
101
+ *
102
+ * // Can be safely used in URLs, query params, headers
103
+ * const url = `/api/items?cursor=${encoded}`;
104
+ * }
105
+ * ```
106
+ */
107
+ declare function encodeCursor(cursor: Cursor): string;
108
+ /**
109
+ * Decodes an opaque cursor string back to a Cursor.
110
+ *
111
+ * Validates the structure after decoding, ensuring all required fields
112
+ * are present and have correct types.
113
+ *
114
+ * @param encoded - URL-safe base64 encoded cursor
115
+ * @returns Result with decoded Cursor or ValidationError if invalid
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * // Successful decode
120
+ * const result = decodeCursor(encodedString);
121
+ * if (result.isOk()) {
122
+ * console.log(result.value.position);
123
+ * }
124
+ *
125
+ * // Handle invalid input
126
+ * const invalid = decodeCursor("not-a-valid-cursor");
127
+ * if (invalid.isErr()) {
128
+ * console.log(invalid.error.message); // "Invalid cursor: ..."
129
+ * }
130
+ * ```
131
+ */
132
+ declare function decodeCursor(encoded: string): Result<Cursor, InstanceType<typeof ValidationError>>;
133
+ export { createCursor, advanceCursor, isExpired, encodeCursor, decodeCursor };
@@ -0,0 +1,139 @@
1
+ import { CursorStore, PersistentStore, PersistentStoreOptions, ScopedStore } from "./state-6h22x3e6.js";
2
+ /**
3
+ * Create an in-memory cursor store.
4
+ *
5
+ * The store automatically handles expiration: `get()` and `has()` return
6
+ * not-found/false for expired cursors. Use `prune()` to remove expired
7
+ * cursors from memory.
8
+ *
9
+ * @returns A new cursor store implementing both CursorStore and ScopedStore interfaces
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const store = createCursorStore();
14
+ *
15
+ * // Create and store a cursor
16
+ * const cursor = createCursor({
17
+ * position: 0,
18
+ * metadata: { query: "status:open" },
19
+ * ttl: 3600000, // 1 hour
20
+ * });
21
+ *
22
+ * if (cursor.isOk()) {
23
+ * store.set(cursor.value);
24
+ *
25
+ * // Retrieve later
26
+ * const result = store.get(cursor.value.id);
27
+ * if (result.isOk()) {
28
+ * console.log(result.value.position);
29
+ * }
30
+ * }
31
+ *
32
+ * // List all cursors
33
+ * console.log(store.list()); // ["cursor-id", ...]
34
+ *
35
+ * // Cleanup expired
36
+ * const pruned = store.prune();
37
+ * ```
38
+ */
39
+ declare function createCursorStore(): CursorStore & ScopedStore;
40
+ /**
41
+ * Create a persistent cursor store that saves to disk.
42
+ *
43
+ * The store loads existing cursors from the file on initialization.
44
+ * Changes are kept in memory until `flush()` is called. Uses atomic
45
+ * writes (temp file + rename) to prevent corruption.
46
+ *
47
+ * If the file is corrupted or invalid JSON, the store starts empty
48
+ * rather than throwing an error.
49
+ *
50
+ * @param options - Persistence options including the file path
51
+ * @returns Promise resolving to a PersistentStore
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * // Create persistent store
56
+ * const store = await createPersistentStore({
57
+ * path: "/home/user/.config/myapp/cursors.json",
58
+ * });
59
+ *
60
+ * // Use like any cursor store
61
+ * const cursor = createCursor({ position: 0 });
62
+ * if (cursor.isOk()) {
63
+ * store.set(cursor.value);
64
+ * }
65
+ *
66
+ * // Flush to disk (call before process exit)
67
+ * await store.flush();
68
+ *
69
+ * // Cleanup when done
70
+ * store.dispose();
71
+ * ```
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * // Combine with scoped stores for organized persistence
76
+ * const persistent = await createPersistentStore({
77
+ * path: "~/.config/myapp/cursors.json",
78
+ * });
79
+ *
80
+ * const issuesCursors = createScopedStore(persistent, "issues");
81
+ * const prsCursors = createScopedStore(persistent, "prs");
82
+ *
83
+ * // All scopes share the same persistence file
84
+ * await persistent.flush();
85
+ * ```
86
+ */
87
+ declare function createPersistentStore(options: PersistentStoreOptions): Promise<PersistentStore>;
88
+ /**
89
+ * Create a scoped cursor store with namespace isolation.
90
+ *
91
+ * Scoped stores prefix all cursor IDs with the scope name, preventing
92
+ * collisions between different contexts (e.g., "issues" vs "pull-requests").
93
+ *
94
+ * Scopes can be nested: `createScopedStore(scopedStore, "child")` creates
95
+ * IDs like "parent:child:cursor-id".
96
+ *
97
+ * When retrieving cursors, the scope prefix is automatically stripped,
98
+ * so consumers see clean IDs without the namespace prefix.
99
+ *
100
+ * @param store - Parent store to scope (CursorStore or another ScopedStore for nesting)
101
+ * @param scope - Namespace for this scope (will be prefixed to all cursor IDs)
102
+ * @returns ScopedStore with isolated cursor management
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const store = createCursorStore();
107
+ * const issueStore = createScopedStore(store, "issues");
108
+ * const prStore = createScopedStore(store, "prs");
109
+ *
110
+ * // These don't conflict - different namespaces
111
+ * issueStore.set(cursor1); // Stored as "issues:abc123"
112
+ * prStore.set(cursor2); // Stored as "prs:abc123"
113
+ *
114
+ * // Retrieved cursors have clean IDs
115
+ * const result = issueStore.get("abc123");
116
+ * if (result.isOk()) {
117
+ * result.value.id; // "abc123" (not "issues:abc123")
118
+ * }
119
+ *
120
+ * // Clear only affects the scope
121
+ * issueStore.clear(); // Only clears issue cursors
122
+ * prStore.list(); // PR cursors still exist
123
+ * ```
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * // Nested scopes for hierarchical organization
128
+ * const store = createCursorStore();
129
+ * const githubStore = createScopedStore(store, "github");
130
+ * const issuesStore = createScopedStore(githubStore, "issues");
131
+ *
132
+ * issuesStore.getScope(); // "github:issues"
133
+ *
134
+ * // Cursor stored as "github:issues:cursor-id"
135
+ * issuesStore.set(cursor);
136
+ * ```
137
+ */
138
+ declare function createScopedStore(store: CursorStore | ScopedStore, scope: string): ScopedStore;
139
+ export { createCursorStore, createPersistentStore, createScopedStore };