@outfitter/state 0.2.2 → 0.2.4
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 +38 -34
- package/dist/index.d.ts +32 -79
- package/dist/index.js +7 -7
- package/package.json +28 -27
package/README.md
CHANGED
|
@@ -42,14 +42,14 @@ if (cursorResult.isOk()) {
|
|
|
42
42
|
|
|
43
43
|
Cursors are intentionally **opaque** to consumers. They are immutable, frozen objects that encapsulate pagination state.
|
|
44
44
|
|
|
45
|
-
| Property
|
|
46
|
-
|
|
47
|
-
| `id`
|
|
48
|
-
| `position`
|
|
49
|
-
| `metadata`
|
|
50
|
-
| `ttl`
|
|
51
|
-
| `expiresAt` | `number`
|
|
52
|
-
| `createdAt` | `number`
|
|
45
|
+
| Property | Type | Description |
|
|
46
|
+
| ----------- | ------------------------- | ----------------------------------------------- |
|
|
47
|
+
| `id` | `string` | Unique identifier for storage lookup |
|
|
48
|
+
| `position` | `number` | Current offset in the result set |
|
|
49
|
+
| `metadata` | `Record<string, unknown>` | Optional user-defined context |
|
|
50
|
+
| `ttl` | `number` | Time-to-live in milliseconds (optional) |
|
|
51
|
+
| `expiresAt` | `number` | Computed Unix timestamp for expiry (if TTL set) |
|
|
52
|
+
| `createdAt` | `number` | Unix timestamp when cursor was created |
|
|
53
53
|
|
|
54
54
|
### Why Opaque?
|
|
55
55
|
|
|
@@ -95,7 +95,11 @@ if (result.isOk()) {
|
|
|
95
95
|
### Example: Paginated Handler
|
|
96
96
|
|
|
97
97
|
```typescript
|
|
98
|
-
import {
|
|
98
|
+
import {
|
|
99
|
+
createCursor,
|
|
100
|
+
createCursorStore,
|
|
101
|
+
advanceCursor,
|
|
102
|
+
} from "@outfitter/state";
|
|
99
103
|
import { Result } from "@outfitter/contracts";
|
|
100
104
|
|
|
101
105
|
const store = createCursorStore();
|
|
@@ -150,11 +154,11 @@ const issuesStore = createScopedStore(store, "linear:issues");
|
|
|
150
154
|
const prsStore = createScopedStore(store, "github:prs");
|
|
151
155
|
|
|
152
156
|
// Cursors are isolated - same ID won't conflict
|
|
153
|
-
issuesStore.set(issueCursor);
|
|
154
|
-
prsStore.set(prCursor);
|
|
157
|
+
issuesStore.set(issueCursor); // Stored as "linear:issues:cursor-id"
|
|
158
|
+
prsStore.set(prCursor); // Stored as "github:prs:cursor-id"
|
|
155
159
|
|
|
156
160
|
// Each scope manages its own cursors
|
|
157
|
-
issuesStore.clear();
|
|
161
|
+
issuesStore.clear(); // Only clears issue cursors
|
|
158
162
|
```
|
|
159
163
|
|
|
160
164
|
### Nested Scopes
|
|
@@ -167,8 +171,8 @@ const githubStore = createScopedStore(store, "github");
|
|
|
167
171
|
const issuesStore = createScopedStore(githubStore, "issues");
|
|
168
172
|
const prsStore = createScopedStore(githubStore, "prs");
|
|
169
173
|
|
|
170
|
-
issuesStore.getScope();
|
|
171
|
-
prsStore.getScope();
|
|
174
|
+
issuesStore.getScope(); // "github:issues"
|
|
175
|
+
prsStore.getScope(); // "github:prs"
|
|
172
176
|
```
|
|
173
177
|
|
|
174
178
|
### Scoped Store Behavior
|
|
@@ -183,15 +187,15 @@ if (cursor.isOk()) {
|
|
|
183
187
|
scoped.set(cursor.value);
|
|
184
188
|
|
|
185
189
|
// Underlying store has prefixed ID
|
|
186
|
-
store.list();
|
|
190
|
+
store.list(); // ["my-scope:abc123"]
|
|
187
191
|
|
|
188
192
|
// Scoped store returns clean ID
|
|
189
|
-
scoped.list();
|
|
193
|
+
scoped.list(); // ["abc123"]
|
|
190
194
|
|
|
191
195
|
// Get returns cursor with clean ID
|
|
192
196
|
const result = scoped.get("abc123");
|
|
193
197
|
if (result.isOk()) {
|
|
194
|
-
result.value.id;
|
|
198
|
+
result.value.id; // "abc123" (not "my-scope:abc123")
|
|
195
199
|
}
|
|
196
200
|
}
|
|
197
201
|
```
|
|
@@ -257,7 +261,7 @@ const result = createCursor({
|
|
|
257
261
|
|
|
258
262
|
if (result.isOk()) {
|
|
259
263
|
const cursor = result.value;
|
|
260
|
-
cursor.ttl;
|
|
264
|
+
cursor.ttl; // 3600000
|
|
261
265
|
cursor.expiresAt; // Unix timestamp (e.g., 1706000000000)
|
|
262
266
|
}
|
|
263
267
|
```
|
|
@@ -289,7 +293,7 @@ Cursors created without a TTL never expire:
|
|
|
289
293
|
```typescript
|
|
290
294
|
const result = createCursor({ position: 0 });
|
|
291
295
|
if (result.isOk()) {
|
|
292
|
-
result.value.ttl;
|
|
296
|
+
result.value.ttl; // undefined
|
|
293
297
|
result.value.expiresAt; // undefined
|
|
294
298
|
isExpired(result.value); // always false
|
|
295
299
|
}
|
|
@@ -299,24 +303,24 @@ if (result.isOk()) {
|
|
|
299
303
|
|
|
300
304
|
### Functions
|
|
301
305
|
|
|
302
|
-
| Function
|
|
303
|
-
|
|
304
|
-
| `createCursor(options)`
|
|
306
|
+
| Function | Description |
|
|
307
|
+
| --------------------------------- | ----------------------------------------- |
|
|
308
|
+
| `createCursor(options)` | Create a new immutable pagination cursor |
|
|
305
309
|
| `advanceCursor(cursor, position)` | Create a new cursor with updated position |
|
|
306
|
-
| `isExpired(cursor)`
|
|
307
|
-
| `createCursorStore()`
|
|
308
|
-
| `createPersistentStore(options)`
|
|
309
|
-
| `createScopedStore(store, scope)` | Create a namespace-isolated cursor store
|
|
310
|
+
| `isExpired(cursor)` | Check if a cursor has expired |
|
|
311
|
+
| `createCursorStore()` | Create an in-memory cursor store |
|
|
312
|
+
| `createPersistentStore(options)` | Create a disk-backed cursor store |
|
|
313
|
+
| `createScopedStore(store, scope)` | Create a namespace-isolated cursor store |
|
|
310
314
|
|
|
311
315
|
### Interfaces
|
|
312
316
|
|
|
313
|
-
| Interface
|
|
314
|
-
|
|
315
|
-
| `Cursor`
|
|
316
|
-
| `CreateCursorOptions`
|
|
317
|
-
| `CursorStore`
|
|
318
|
-
| `ScopedStore`
|
|
319
|
-
| `PersistentStore`
|
|
317
|
+
| Interface | Description |
|
|
318
|
+
| ------------------------ | ------------------------------------- |
|
|
319
|
+
| `Cursor` | Immutable pagination cursor |
|
|
320
|
+
| `CreateCursorOptions` | Options for `createCursor()` |
|
|
321
|
+
| `CursorStore` | Base interface for cursor stores |
|
|
322
|
+
| `ScopedStore` | Cursor store with namespace isolation |
|
|
323
|
+
| `PersistentStore` | Cursor store with disk persistence |
|
|
320
324
|
| `PersistentStoreOptions` | Options for `createPersistentStore()` |
|
|
321
325
|
|
|
322
326
|
## Error Handling
|
|
@@ -339,7 +343,7 @@ if (getResult.isErr()) {
|
|
|
339
343
|
// NotFoundError: Cursor not found: nonexistent
|
|
340
344
|
console.error(getResult.error.message);
|
|
341
345
|
console.log(getResult.error.resourceType); // "cursor"
|
|
342
|
-
console.log(getResult.error.resourceId);
|
|
346
|
+
console.log(getResult.error.resourceId); // "nonexistent"
|
|
343
347
|
}
|
|
344
348
|
```
|
|
345
349
|
|
package/dist/index.d.ts
CHANGED
|
@@ -23,18 +23,18 @@ import { NotFoundError, Result, StorageError, ValidationError } from "@outfitter
|
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
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;
|
|
26
30
|
/** Unique identifier for this cursor (UUID format) */
|
|
27
31
|
readonly id: string;
|
|
28
|
-
/** Current position/offset in the result set (zero-based) */
|
|
29
|
-
readonly position: number;
|
|
30
32
|
/** Optional user-defined metadata associated with this cursor */
|
|
31
33
|
readonly metadata?: Record<string, unknown>;
|
|
34
|
+
/** Current position/offset in the result set (zero-based) */
|
|
35
|
+
readonly position: number;
|
|
32
36
|
/** Time-to-live in milliseconds (optional, omitted if cursor never expires) */
|
|
33
37
|
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
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Options for creating a pagination cursor.
|
|
@@ -56,10 +56,10 @@ interface Cursor {
|
|
|
56
56
|
interface CreateCursorOptions {
|
|
57
57
|
/** Custom cursor ID (UUID generated if not provided) */
|
|
58
58
|
id?: string;
|
|
59
|
-
/** Starting position in the result set (must be non-negative) */
|
|
60
|
-
position: number;
|
|
61
59
|
/** User-defined metadata to associate with the cursor */
|
|
62
60
|
metadata?: Record<string, unknown>;
|
|
61
|
+
/** Starting position in the result set (must be non-negative) */
|
|
62
|
+
position: number;
|
|
63
63
|
/** Time-to-live in milliseconds (cursor never expires if omitted) */
|
|
64
64
|
ttl?: number;
|
|
65
65
|
}
|
|
@@ -92,10 +92,14 @@ interface CreateCursorOptions {
|
|
|
92
92
|
*/
|
|
93
93
|
interface CursorStore {
|
|
94
94
|
/**
|
|
95
|
-
*
|
|
96
|
-
* @param cursor - The cursor to store (replaces existing if same ID)
|
|
95
|
+
* Remove all cursors from the store.
|
|
97
96
|
*/
|
|
98
|
-
|
|
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;
|
|
99
103
|
/**
|
|
100
104
|
* Retrieve a cursor by ID.
|
|
101
105
|
* @param id - The cursor ID to look up
|
|
@@ -109,15 +113,6 @@ interface CursorStore {
|
|
|
109
113
|
*/
|
|
110
114
|
has(id: string): boolean;
|
|
111
115
|
/**
|
|
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
116
|
* List all cursor IDs in the store (including expired).
|
|
122
117
|
* @returns Array of cursor IDs
|
|
123
118
|
*/
|
|
@@ -127,6 +122,11 @@ interface CursorStore {
|
|
|
127
122
|
* @returns Number of cursors that were pruned
|
|
128
123
|
*/
|
|
129
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
130
|
}
|
|
131
131
|
/**
|
|
132
132
|
* A cursor store with namespace isolation.
|
|
@@ -198,17 +198,17 @@ interface PersistentStoreOptions {
|
|
|
198
198
|
* ```
|
|
199
199
|
*/
|
|
200
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;
|
|
201
206
|
/**
|
|
202
207
|
* Flush all in-memory cursors to disk.
|
|
203
208
|
* Uses atomic write (temp file + rename) to prevent corruption.
|
|
204
209
|
* @returns Promise that resolves when write is complete
|
|
205
210
|
*/
|
|
206
211
|
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
212
|
}
|
|
213
213
|
/**
|
|
214
214
|
* Create a new pagination cursor.
|
|
@@ -378,53 +378,6 @@ declare function decodeCursor(encoded: string): Result<Cursor, InstanceType<type
|
|
|
378
378
|
* ```
|
|
379
379
|
*/
|
|
380
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
381
|
declare function createPersistentStore(options: PersistentStoreOptions): Promise<PersistentStore>;
|
|
429
382
|
/**
|
|
430
383
|
* Create a scoped cursor store with namespace isolation.
|
|
@@ -503,6 +456,11 @@ declare const DEFAULT_PAGE_LIMIT = 25;
|
|
|
503
456
|
* ```
|
|
504
457
|
*/
|
|
505
458
|
interface PaginationStore {
|
|
459
|
+
/**
|
|
460
|
+
* Delete a cursor by ID.
|
|
461
|
+
* @param id - The cursor ID to delete
|
|
462
|
+
*/
|
|
463
|
+
delete(id: string): void;
|
|
506
464
|
/**
|
|
507
465
|
* Get a cursor by ID.
|
|
508
466
|
* @param id - The cursor ID to look up
|
|
@@ -515,11 +473,6 @@ interface PaginationStore {
|
|
|
515
473
|
* @param cursor - The cursor to store
|
|
516
474
|
*/
|
|
517
475
|
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
476
|
}
|
|
524
477
|
/**
|
|
525
478
|
* Get the default pagination store (module-level singleton).
|
|
@@ -568,10 +521,10 @@ declare function createPaginationStore(): PaginationStore;
|
|
|
568
521
|
* @typeParam T - The type of items being paginated
|
|
569
522
|
*/
|
|
570
523
|
interface PaginationResult<T> {
|
|
571
|
-
/** The items in the current page */
|
|
572
|
-
page: T[];
|
|
573
524
|
/** The cursor for the next page, or null if this is the last page */
|
|
574
525
|
nextCursor: Cursor | null;
|
|
526
|
+
/** The items in the current page */
|
|
527
|
+
page: T[];
|
|
575
528
|
}
|
|
576
529
|
/**
|
|
577
530
|
* Extract a page of items based on cursor position.
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
var __require =
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
3
|
|
|
4
|
-
// src/index.ts
|
|
5
|
-
import { existsSync, mkdirSync, renameSync, writeFileSync } from "
|
|
6
|
-
import { dirname } from "
|
|
4
|
+
// packages/state/src/index.ts
|
|
5
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from "fs";
|
|
6
|
+
import { dirname } from "path";
|
|
7
7
|
import {
|
|
8
8
|
NotFoundError,
|
|
9
9
|
Result,
|
|
@@ -213,6 +213,7 @@ function createCursorStore() {
|
|
|
213
213
|
}
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
|
+
var dispose = () => {};
|
|
216
217
|
async function createPersistentStore(options) {
|
|
217
218
|
const { path: storagePath } = options;
|
|
218
219
|
const cursors = new Map;
|
|
@@ -242,13 +243,12 @@ async function createPersistentStore(options) {
|
|
|
242
243
|
renameSync(tempPath, storagePath);
|
|
243
244
|
} catch (error) {
|
|
244
245
|
try {
|
|
245
|
-
const { unlinkSync } = await import("
|
|
246
|
+
const { unlinkSync } = await import("fs");
|
|
246
247
|
unlinkSync(tempPath);
|
|
247
248
|
} catch {}
|
|
248
249
|
throw error;
|
|
249
250
|
}
|
|
250
251
|
};
|
|
251
|
-
const dispose = () => {};
|
|
252
252
|
return {
|
|
253
253
|
set(cursor) {
|
|
254
254
|
cursors.set(cursor.id, cursor);
|
package/package.json
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outfitter/state",
|
|
3
|
+
"version": "0.2.4",
|
|
3
4
|
"description": "Pagination cursor persistence and state management for Outfitter",
|
|
4
|
-
"
|
|
5
|
-
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cursor",
|
|
7
|
+
"outfitter",
|
|
8
|
+
"pagination",
|
|
9
|
+
"state",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/outfitter-dev/outfitter.git",
|
|
16
|
+
"directory": "packages/state"
|
|
17
|
+
},
|
|
6
18
|
"files": [
|
|
7
19
|
"dist"
|
|
8
20
|
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"sideEffects": false,
|
|
9
23
|
"module": "./dist/index.js",
|
|
10
24
|
"types": "./dist/index.d.ts",
|
|
11
25
|
"exports": {
|
|
@@ -17,37 +31,24 @@
|
|
|
17
31
|
},
|
|
18
32
|
"./package.json": "./package.json"
|
|
19
33
|
},
|
|
20
|
-
"
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
21
37
|
"scripts": {
|
|
22
|
-
"build": "bunup --filter @outfitter/state",
|
|
23
|
-
"lint": "
|
|
24
|
-
"lint:fix": "
|
|
38
|
+
"build": "cd ../.. && bunup --filter @outfitter/state",
|
|
39
|
+
"lint": "oxlint ./src",
|
|
40
|
+
"lint:fix": "oxlint --fix ./src",
|
|
25
41
|
"test": "bun test",
|
|
26
42
|
"typecheck": "tsc --noEmit",
|
|
27
|
-
"clean": "rm -rf dist"
|
|
43
|
+
"clean": "rm -rf dist",
|
|
44
|
+
"prepublishOnly": "bun ../../scripts/check-publish-manifest.ts"
|
|
28
45
|
},
|
|
29
46
|
"dependencies": {
|
|
30
|
-
"@outfitter/contracts": "0.4.
|
|
31
|
-
"@outfitter/types": "0.2.
|
|
47
|
+
"@outfitter/contracts": "0.4.2",
|
|
48
|
+
"@outfitter/types": "0.2.4"
|
|
32
49
|
},
|
|
33
50
|
"devDependencies": {
|
|
34
|
-
"@types/bun": "
|
|
35
|
-
"typescript": "^5.
|
|
36
|
-
},
|
|
37
|
-
"keywords": [
|
|
38
|
-
"outfitter",
|
|
39
|
-
"state",
|
|
40
|
-
"pagination",
|
|
41
|
-
"cursor",
|
|
42
|
-
"typescript"
|
|
43
|
-
],
|
|
44
|
-
"license": "MIT",
|
|
45
|
-
"repository": {
|
|
46
|
-
"type": "git",
|
|
47
|
-
"url": "https://github.com/outfitter-dev/outfitter.git",
|
|
48
|
-
"directory": "packages/state"
|
|
49
|
-
},
|
|
50
|
-
"publishConfig": {
|
|
51
|
-
"access": "public"
|
|
51
|
+
"@types/bun": "^1.3.9",
|
|
52
|
+
"typescript": "^5.9.3"
|
|
52
53
|
}
|
|
53
54
|
}
|