@starcite/sdk 0.0.5 → 0.0.7

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.js CHANGED
@@ -78,13 +78,14 @@ function inferIdentityFromApiKey(apiKey) {
78
78
  const claims = ApiKeyClaimsSchema.parse(decodeJwt(apiKey));
79
79
  const id = claims.principal_id ?? claims.sub;
80
80
  const tenantId = claims.tenant_id;
81
- if (!(tenantId && id && claims.principal_type)) {
81
+ const type = claims.principal_type ?? "user";
82
+ if (!(tenantId && id)) {
82
83
  return void 0;
83
84
  }
84
85
  return new StarciteIdentity({
85
86
  tenantId,
86
87
  id,
87
- type: claims.principal_type
88
+ type
88
89
  });
89
90
  }
90
91
  function decodeSessionToken(token) {
@@ -285,7 +286,7 @@ var SessionLog = class {
285
286
  this.enforceRetention();
286
287
  return true;
287
288
  }
288
- getSnapshot(syncing) {
289
+ state(syncing) {
289
290
  return {
290
291
  events: this.history.slice(),
291
292
  lastSeq: this.appliedSeq,
@@ -888,7 +889,7 @@ var TailStream = class {
888
889
  this.follow = follow;
889
890
  this.shouldReconnect = follow ? opts.reconnect ?? true : false;
890
891
  this.catchUpIdleMs = opts.catchUpIdleMs ?? 1e3;
891
- this.connectionTimeoutMs = opts.connectionTimeoutMs ?? 4e3;
892
+ this.connectionTimeoutMs = opts.connectionTimeoutMs ?? 12e3;
892
893
  this.inactivityTimeoutMs = opts.inactivityTimeoutMs;
893
894
  this.maxBufferedBatches = opts.maxBufferedBatches ?? 1024;
894
895
  this.signal = opts.signal;
@@ -1259,8 +1260,10 @@ var StarciteSession = class {
1259
1260
  store;
1260
1261
  lifecycle = new EventEmitter3();
1261
1262
  eventSubscriptions = /* @__PURE__ */ new Map();
1263
+ appendTask = Promise.resolve();
1262
1264
  liveSyncController;
1263
1265
  liveSyncTask;
1266
+ liveSyncCatchUpActive = false;
1264
1267
  constructor(options) {
1265
1268
  this.id = options.id;
1266
1269
  this.token = options.token;
@@ -1270,8 +1273,8 @@ var StarciteSession = class {
1270
1273
  this.producerId = crypto.randomUUID();
1271
1274
  this.store = options.store;
1272
1275
  this.log = new SessionLog(options.logOptions);
1273
- const storedState = this.store.load(this.id);
1274
- if (storedState) {
1276
+ const storedState = this.store?.load(this.id);
1277
+ if (storedState !== void 0) {
1275
1278
  this.log.hydrate(storedState);
1276
1279
  }
1277
1280
  }
@@ -1280,28 +1283,35 @@ var StarciteSession = class {
1280
1283
  *
1281
1284
  * The SDK manages `actor`, `producer_id`, and `producer_seq` automatically.
1282
1285
  */
1283
- async append(input, options) {
1286
+ append(input, options) {
1284
1287
  const parsed = SessionAppendInputSchema.parse(input);
1285
- this.producerSeq += 1;
1286
- const result = await this.appendRaw(
1287
- {
1288
- type: parsed.type ?? "content",
1289
- payload: parsed.payload ?? { text: parsed.text },
1290
- actor: parsed.actor ?? this.identity.toActor(),
1291
- producer_id: this.producerId,
1292
- producer_seq: this.producerSeq,
1293
- source: parsed.source ?? "agent",
1294
- metadata: parsed.metadata,
1295
- refs: parsed.refs,
1296
- idempotency_key: parsed.idempotencyKey,
1297
- expected_seq: parsed.expectedSeq
1298
- },
1299
- options
1288
+ const runAppend = this.appendTask.then(async () => {
1289
+ this.producerSeq += 1;
1290
+ const result = await this.appendRaw(
1291
+ {
1292
+ type: parsed.type ?? "content",
1293
+ payload: parsed.payload ?? { text: parsed.text },
1294
+ actor: parsed.actor ?? this.identity.toActor(),
1295
+ producer_id: this.producerId,
1296
+ producer_seq: this.producerSeq,
1297
+ source: parsed.source ?? "agent",
1298
+ metadata: parsed.metadata,
1299
+ refs: parsed.refs,
1300
+ idempotency_key: parsed.idempotencyKey,
1301
+ expected_seq: parsed.expectedSeq
1302
+ },
1303
+ options
1304
+ );
1305
+ return {
1306
+ seq: result.seq,
1307
+ deduped: result.deduped
1308
+ };
1309
+ });
1310
+ this.appendTask = runAppend.then(
1311
+ () => void 0,
1312
+ () => void 0
1300
1313
  );
1301
- return {
1302
- seq: result.seq,
1303
- deduped: result.deduped
1304
- };
1314
+ return runAppend;
1305
1315
  }
1306
1316
  /**
1307
1317
  * Appends a raw event payload as-is. Caller manages all fields.
@@ -1318,11 +1328,33 @@ var StarciteSession = class {
1318
1328
  AppendEventResponseSchema
1319
1329
  );
1320
1330
  }
1321
- on(eventName, listener) {
1331
+ on(eventName, listener, options) {
1322
1332
  if (eventName === "event") {
1323
1333
  const eventListener = listener;
1324
1334
  if (!this.eventSubscriptions.has(eventListener)) {
1325
- const unsubscribe = this.log.subscribe(eventListener, { replay: true });
1335
+ const eventOptions = options;
1336
+ const replay = eventOptions?.replay ?? true;
1337
+ const replayCutoffSeq = replay ? this.log.lastSeq : -1;
1338
+ const schema = eventOptions?.schema;
1339
+ const dispatch = (event) => {
1340
+ const parsedEvent = this.parseOnEvent(event, schema);
1341
+ if (!parsedEvent) {
1342
+ return;
1343
+ }
1344
+ const classifiedContext = this.resolveEventContext(
1345
+ event.seq,
1346
+ replayCutoffSeq,
1347
+ this.liveSyncCatchUpActive
1348
+ );
1349
+ try {
1350
+ this.observeEventListenerResult(
1351
+ eventListener(parsedEvent, classifiedContext)
1352
+ );
1353
+ } catch (error) {
1354
+ this.emitStreamError(error);
1355
+ }
1356
+ };
1357
+ const unsubscribe = this.log.subscribe(dispatch, { replay });
1326
1358
  this.eventSubscriptions.set(eventListener, unsubscribe);
1327
1359
  }
1328
1360
  this.ensureLiveSync();
@@ -1384,77 +1416,195 @@ var StarciteSession = class {
1384
1416
  this.persistLogState();
1385
1417
  }
1386
1418
  /**
1387
- * Returns a stable snapshot of the current canonical in-memory log.
1419
+ * Returns a stable view of the current canonical in-memory log state.
1388
1420
  */
1389
- getSnapshot() {
1390
- return this.log.getSnapshot(this.liveSyncTask !== void 0);
1421
+ state() {
1422
+ return this.log.state(this.liveSyncTask !== void 0);
1391
1423
  }
1392
1424
  /**
1393
- * Streams tail events one at a time via callback.
1425
+ * Returns the retained canonical event list.
1394
1426
  */
1395
- async tail(onEvent, options = {}) {
1396
- await this.tailBatches(async (batch) => {
1397
- for (const event of batch) {
1398
- await onEvent(event);
1399
- }
1400
- }, options);
1427
+ events() {
1428
+ return this.log.events;
1401
1429
  }
1402
1430
  /**
1403
- * Streams tail event batches grouped by incoming frame via callback.
1404
- */
1405
- async tailBatches(onBatch, options = {}) {
1406
- await new TailStream({
1407
- sessionId: this.id,
1408
- token: this.token,
1409
- websocketBaseUrl: this.transport.websocketBaseUrl,
1410
- websocketFactory: this.transport.websocketFactory,
1411
- options
1412
- }).subscribe(onBatch);
1413
- }
1414
- /**
1415
- * Durably consumes events and checkpoints `event.seq` after each successful handler invocation.
1431
+ * Streams canonical events as an async iterator.
1432
+ *
1433
+ * Replay semantics and schema validation mirror `session.on("event", ...)`.
1416
1434
  */
1417
- async consume(options) {
1418
- const {
1419
- cursorStore,
1420
- handler,
1421
- cursor: requestedCursor,
1422
- ...tailOptions
1423
- } = options;
1424
- let cursor;
1425
- if (requestedCursor !== void 0) {
1426
- cursor = requestedCursor;
1427
- } else {
1428
- try {
1429
- cursor = await cursorStore.load(this.id) ?? 0;
1430
- } catch (error) {
1431
- throw new StarciteError(
1432
- `consume() failed to load cursor for session '${this.id}': ${error instanceof Error ? error.message : String(error)}`
1433
- );
1435
+ tail(options = {}) {
1436
+ const { replay = true, schema, ...tailOptions } = options;
1437
+ const replayCutoffSeq = replay ? this.log.lastSeq : -1;
1438
+ const startCursor = tailOptions.cursor ?? this.log.lastSeq;
1439
+ const session = this;
1440
+ const parseEvent = (event) => session.parseTailEvent(event, schema);
1441
+ return {
1442
+ async *[Symbol.asyncIterator]() {
1443
+ if (replay) {
1444
+ for (const replayEvent of session.log.events) {
1445
+ yield {
1446
+ event: parseEvent(replayEvent),
1447
+ context: { phase: "replay", replayed: true }
1448
+ };
1449
+ }
1450
+ }
1451
+ yield* session.iterateLiveTail({
1452
+ parseEvent,
1453
+ replayCutoffSeq,
1454
+ startCursor,
1455
+ tailOptions
1456
+ });
1434
1457
  }
1458
+ };
1459
+ }
1460
+ parseOnEvent(event, schema) {
1461
+ if (!schema) {
1462
+ return event;
1435
1463
  }
1436
- const stream = new TailStream({
1464
+ try {
1465
+ return schema.parse(event);
1466
+ } catch (error) {
1467
+ this.emitStreamError(
1468
+ new StarciteError(
1469
+ `session.on("event") schema validation failed for session '${this.id}': ${error instanceof Error ? error.message : String(error)}`
1470
+ )
1471
+ );
1472
+ return void 0;
1473
+ }
1474
+ }
1475
+ parseTailEvent(event, schema) {
1476
+ if (!schema) {
1477
+ return event;
1478
+ }
1479
+ try {
1480
+ return schema.parse(event);
1481
+ } catch (error) {
1482
+ throw new StarciteError(
1483
+ `session.tail() schema validation failed for session '${this.id}': ${error instanceof Error ? error.message : String(error)}`
1484
+ );
1485
+ }
1486
+ }
1487
+ resolveEventContext(eventSeq, replayCutoffSeq, forceReplay = false) {
1488
+ const replayed = forceReplay || eventSeq <= replayCutoffSeq;
1489
+ return replayed ? { phase: "replay", replayed: true } : { phase: "live", replayed: false };
1490
+ }
1491
+ observeEventListenerResult(result) {
1492
+ Promise.resolve(result).catch((error) => {
1493
+ this.emitStreamError(error);
1494
+ });
1495
+ }
1496
+ createTailAbortController(outerSignal) {
1497
+ const controller = new AbortController();
1498
+ if (!outerSignal) {
1499
+ return { controller, detach: () => void 0 };
1500
+ }
1501
+ const abortFromOuterSignal = () => {
1502
+ controller.abort(outerSignal.reason);
1503
+ };
1504
+ if (outerSignal.aborted) {
1505
+ controller.abort(outerSignal.reason);
1506
+ return { controller, detach: () => void 0 };
1507
+ }
1508
+ outerSignal.addEventListener("abort", abortFromOuterSignal, { once: true });
1509
+ return {
1510
+ controller,
1511
+ detach: () => {
1512
+ outerSignal.removeEventListener("abort", abortFromOuterSignal);
1513
+ }
1514
+ };
1515
+ }
1516
+ createTailRuntime({
1517
+ parseEvent,
1518
+ replayCutoffSeq,
1519
+ startCursor,
1520
+ tailOptions
1521
+ }) {
1522
+ const queue = [];
1523
+ let notify;
1524
+ let done = false;
1525
+ let failure;
1526
+ const shouldApplyToLog = tailOptions.agent === void 0;
1527
+ const { controller, detach } = this.createTailAbortController(
1528
+ tailOptions.signal
1529
+ );
1530
+ const wake = () => {
1531
+ notify?.();
1532
+ };
1533
+ const streamTask = new TailStream({
1437
1534
  sessionId: this.id,
1438
1535
  token: this.token,
1439
1536
  websocketBaseUrl: this.transport.websocketBaseUrl,
1440
1537
  websocketFactory: this.transport.websocketFactory,
1441
1538
  options: {
1442
1539
  ...tailOptions,
1443
- cursor
1540
+ cursor: startCursor,
1541
+ signal: controller.signal
1542
+ }
1543
+ }).subscribe((batch) => {
1544
+ const queuedEvents = shouldApplyToLog ? this.log.applyBatch(batch) : batch;
1545
+ if (shouldApplyToLog && queuedEvents.length > 0) {
1546
+ this.persistLogState();
1444
1547
  }
1548
+ for (const event of queuedEvents) {
1549
+ queue.push({
1550
+ event: parseEvent(event),
1551
+ context: this.resolveEventContext(event.seq, replayCutoffSeq)
1552
+ });
1553
+ }
1554
+ wake();
1555
+ }).catch((error) => {
1556
+ failure = error;
1557
+ }).finally(() => {
1558
+ done = true;
1559
+ wake();
1445
1560
  });
1446
- await stream.subscribe(async (batch) => {
1447
- for (const event of batch) {
1448
- await handler(event);
1449
- try {
1450
- await cursorStore.save(this.id, event.seq);
1451
- } catch (error) {
1452
- throw new StarciteError(
1453
- `consume() failed to save cursor for session '${this.id}': ${error instanceof Error ? error.message : String(error)}`
1454
- );
1561
+ return {
1562
+ next: async () => {
1563
+ while (queue.length === 0 && !done && !failure) {
1564
+ await new Promise((resolve) => {
1565
+ notify = resolve;
1566
+ });
1567
+ notify = void 0;
1455
1568
  }
1569
+ const next = queue.shift();
1570
+ return next;
1571
+ },
1572
+ getFailure: () => failure,
1573
+ dispose: async () => {
1574
+ controller.abort();
1575
+ detach();
1576
+ await streamTask;
1456
1577
  }
1578
+ };
1579
+ }
1580
+ async *iterateLiveTail({
1581
+ parseEvent,
1582
+ replayCutoffSeq,
1583
+ startCursor,
1584
+ tailOptions
1585
+ }) {
1586
+ const runtime = this.createTailRuntime({
1587
+ parseEvent,
1588
+ replayCutoffSeq,
1589
+ startCursor,
1590
+ tailOptions
1457
1591
  });
1592
+ try {
1593
+ while (true) {
1594
+ const next = await runtime.next();
1595
+ if (next) {
1596
+ yield next;
1597
+ continue;
1598
+ }
1599
+ const failure = runtime.getFailure();
1600
+ if (failure) {
1601
+ throw failure;
1602
+ }
1603
+ return;
1604
+ }
1605
+ } finally {
1606
+ await runtime.dispose();
1607
+ }
1458
1608
  }
1459
1609
  emitStreamError(error) {
1460
1610
  const streamError = error instanceof Error ? error : new StarciteError(`Session stream failed: ${String(error)}`);
@@ -1479,67 +1629,94 @@ var StarciteSession = class {
1479
1629
  }).finally(() => {
1480
1630
  this.liveSyncTask = void 0;
1481
1631
  this.liveSyncController = void 0;
1632
+ if (this.eventSubscriptions.size > 0) {
1633
+ this.ensureLiveSync();
1634
+ }
1482
1635
  });
1483
1636
  }
1484
1637
  async runLiveSync(signal) {
1638
+ let shouldRunCatchUpPass = this.log.lastSeq === 0;
1639
+ let retryDelayMs = 250;
1485
1640
  while (!signal.aborted && this.eventSubscriptions.size > 0) {
1486
- const stream = new TailStream({
1487
- sessionId: this.id,
1488
- token: this.token,
1489
- websocketBaseUrl: this.transport.websocketBaseUrl,
1490
- websocketFactory: this.transport.websocketFactory,
1491
- options: {
1492
- cursor: this.log.lastSeq,
1493
- signal
1494
- }
1495
- });
1641
+ this.liveSyncCatchUpActive = shouldRunCatchUpPass;
1496
1642
  try {
1497
- await stream.subscribe((batch) => {
1498
- const appliedEvents = this.log.applyBatch(batch);
1499
- if (appliedEvents.length > 0) {
1500
- this.persistLogState();
1501
- }
1502
- });
1643
+ await this.subscribeLiveSyncPass(signal, !shouldRunCatchUpPass);
1644
+ shouldRunCatchUpPass = false;
1645
+ retryDelayMs = 250;
1503
1646
  } catch (error) {
1504
1647
  if (signal.aborted) {
1505
1648
  return;
1506
1649
  }
1507
1650
  if (error instanceof SessionLogGapError) {
1651
+ shouldRunCatchUpPass = true;
1508
1652
  continue;
1509
1653
  }
1510
- throw error;
1654
+ this.emitStreamError(error);
1655
+ shouldRunCatchUpPass = true;
1656
+ await this.waitForLiveSyncRetry(retryDelayMs, signal);
1657
+ retryDelayMs = Math.min(retryDelayMs * 2, 5e3);
1658
+ } finally {
1659
+ this.liveSyncCatchUpActive = false;
1511
1660
  }
1512
1661
  }
1513
1662
  }
1663
+ async subscribeLiveSyncPass(signal, follow) {
1664
+ const stream = new TailStream({
1665
+ sessionId: this.id,
1666
+ token: this.token,
1667
+ websocketBaseUrl: this.transport.websocketBaseUrl,
1668
+ websocketFactory: this.transport.websocketFactory,
1669
+ options: {
1670
+ cursor: this.log.lastSeq,
1671
+ follow,
1672
+ signal
1673
+ }
1674
+ });
1675
+ await stream.subscribe((batch) => {
1676
+ const appliedEvents = this.log.applyBatch(batch);
1677
+ if (appliedEvents.length > 0) {
1678
+ this.persistLogState();
1679
+ }
1680
+ });
1681
+ }
1514
1682
  persistLogState() {
1683
+ if (!this.store) {
1684
+ return;
1685
+ }
1515
1686
  this.store.save(this.id, {
1516
1687
  cursor: this.log.cursor,
1517
- events: [...this.log.events]
1688
+ events: [...this.log.events],
1689
+ metadata: {
1690
+ schemaVersion: 1,
1691
+ updatedAtMs: Date.now()
1692
+ }
1518
1693
  });
1519
1694
  }
1520
- };
1521
-
1522
- // src/session-store.ts
1523
- function cloneEvents(events) {
1524
- return events.map((event) => structuredClone(event));
1525
- }
1526
- function cloneState(state) {
1527
- return {
1528
- cursor: state.cursor,
1529
- events: cloneEvents(state.events)
1530
- };
1531
- }
1532
- var MemoryStore = class {
1533
- sessions = /* @__PURE__ */ new Map();
1534
- load(sessionId) {
1535
- const stored = this.sessions.get(sessionId);
1536
- return stored ? cloneState(stored) : void 0;
1537
- }
1538
- save(sessionId, state) {
1539
- this.sessions.set(sessionId, cloneState(state));
1540
- }
1541
- clear(sessionId) {
1542
- this.sessions.delete(sessionId);
1695
+ waitForLiveSyncRetry(delayMs, signal) {
1696
+ if (delayMs <= 0 || signal.aborted) {
1697
+ return Promise.resolve();
1698
+ }
1699
+ return new Promise((resolve) => {
1700
+ let settled = false;
1701
+ const timer = setTimeout(() => {
1702
+ if (settled) {
1703
+ return;
1704
+ }
1705
+ settled = true;
1706
+ signal.removeEventListener("abort", onAbort);
1707
+ resolve();
1708
+ }, delayMs);
1709
+ const onAbort = () => {
1710
+ if (settled) {
1711
+ return;
1712
+ }
1713
+ settled = true;
1714
+ clearTimeout(timer);
1715
+ signal.removeEventListener("abort", onAbort);
1716
+ resolve();
1717
+ };
1718
+ signal.addEventListener("abort", onAbort, { once: true });
1719
+ });
1543
1720
  }
1544
1721
  };
1545
1722
 
@@ -1579,7 +1756,7 @@ var Starcite = class {
1579
1756
  }
1580
1757
  this.authBaseUrl = resolveAuthBaseUrl(options.authUrl, apiKey);
1581
1758
  const websocketFactory = options.websocketFactory ?? defaultWebSocketFactory;
1582
- this.store = options.store ?? new MemoryStore();
1759
+ this.store = options.store;
1583
1760
  this.transport = {
1584
1761
  baseUrl,
1585
1762
  websocketBaseUrl: toWebSocketBaseUrl(baseUrl),
@@ -1646,7 +1823,20 @@ var Starcite = class {
1646
1823
  async sessionFromIdentity(input) {
1647
1824
  let sessionId = input.id;
1648
1825
  let record;
1649
- if (!sessionId) {
1826
+ if (sessionId) {
1827
+ try {
1828
+ record = await this.createSession({
1829
+ id: sessionId,
1830
+ creator_principal: input.identity.toCreatorPrincipal(),
1831
+ title: input.title,
1832
+ metadata: input.metadata
1833
+ });
1834
+ } catch (error) {
1835
+ if (!(error instanceof StarciteApiError && error.status === 409)) {
1836
+ throw error;
1837
+ }
1838
+ }
1839
+ } else {
1650
1840
  record = await this.createSession({
1651
1841
  creator_principal: input.identity.toCreatorPrincipal(),
1652
1842
  title: input.title,
@@ -1737,53 +1927,90 @@ var Starcite = class {
1737
1927
  }
1738
1928
  };
1739
1929
 
1740
- // src/cursor-store.ts
1930
+ // src/session-store.ts
1931
+ import { z as z5 } from "zod";
1741
1932
  var DEFAULT_KEY_PREFIX = "starcite";
1742
- var InMemoryCursorStore = class {
1743
- cursors;
1744
- constructor(initial = {}) {
1745
- this.cursors = new Map(Object.entries(initial));
1746
- }
1933
+ var SessionStoreMetadataSchema = z5.object({
1934
+ schemaVersion: z5.literal(1),
1935
+ updatedAtMs: z5.number().int().nonnegative()
1936
+ });
1937
+ var SessionStoreStateSchema = z5.object({
1938
+ cursor: z5.number().int().nonnegative(),
1939
+ events: z5.array(TailEventSchema),
1940
+ metadata: SessionStoreMetadataSchema.optional()
1941
+ });
1942
+ function cloneEvents(events) {
1943
+ return events.map((event) => structuredClone(event));
1944
+ }
1945
+ function cloneState(state) {
1946
+ return {
1947
+ cursor: state.cursor,
1948
+ events: cloneEvents(state.events),
1949
+ metadata: state.metadata ? { ...state.metadata } : void 0
1950
+ };
1951
+ }
1952
+ var MemoryStore = class {
1953
+ sessions = /* @__PURE__ */ new Map();
1747
1954
  load(sessionId) {
1748
- return this.cursors.get(sessionId);
1955
+ const stored = this.sessions.get(sessionId);
1956
+ return stored ? cloneState(stored) : void 0;
1749
1957
  }
1750
- save(sessionId, cursor) {
1751
- this.cursors.set(sessionId, cursor);
1958
+ save(sessionId, state) {
1959
+ this.sessions.set(sessionId, cloneState(state));
1960
+ }
1961
+ clear(sessionId) {
1962
+ this.sessions.delete(sessionId);
1752
1963
  }
1753
1964
  };
1754
- var WebStorageCursorStore = class {
1965
+ var WebStorageSessionStore = class {
1755
1966
  storage;
1756
1967
  keyForSession;
1968
+ stateSchema;
1757
1969
  constructor(storage, options = {}) {
1758
1970
  this.storage = storage;
1759
1971
  const prefix = options.keyPrefix?.trim() || DEFAULT_KEY_PREFIX;
1760
- this.keyForSession = options.keyForSession ?? ((sessionId) => `${prefix}:${sessionId}:lastSeq`);
1972
+ this.keyForSession = options.keyForSession ?? ((sessionId) => `${prefix}:${sessionId}:sessionStore`);
1973
+ this.stateSchema = options.stateSchema ?? SessionStoreStateSchema;
1761
1974
  }
1762
1975
  load(sessionId) {
1763
1976
  const raw = this.storage.getItem(this.keyForSession(sessionId));
1764
1977
  if (raw === null) {
1765
1978
  return void 0;
1766
1979
  }
1767
- const parsed = Number.parseInt(raw, 10);
1768
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : void 0;
1980
+ let decoded;
1981
+ try {
1982
+ decoded = JSON.parse(raw);
1983
+ } catch {
1984
+ return void 0;
1985
+ }
1986
+ const parsed = this.stateSchema.safeParse(decoded);
1987
+ if (!parsed.success) {
1988
+ return void 0;
1989
+ }
1990
+ return cloneState(parsed.data);
1769
1991
  }
1770
- save(sessionId, cursor) {
1771
- this.storage.setItem(this.keyForSession(sessionId), `${cursor}`);
1992
+ save(sessionId, state) {
1993
+ this.storage.setItem(
1994
+ this.keyForSession(sessionId),
1995
+ JSON.stringify(cloneState(state))
1996
+ );
1997
+ }
1998
+ clear(sessionId) {
1999
+ this.storage.removeItem?.(this.keyForSession(sessionId));
1772
2000
  }
1773
2001
  };
1774
- var LocalStorageCursorStore = class extends WebStorageCursorStore {
2002
+ var LocalStorageSessionStore = class extends WebStorageSessionStore {
1775
2003
  constructor(options = {}) {
1776
2004
  if (typeof localStorage === "undefined") {
1777
2005
  throw new StarciteError(
1778
- "localStorage is not available in this runtime. Use WebStorageCursorStore with a custom storage adapter."
2006
+ "localStorage is not available in this runtime. Use WebStorageSessionStore with a custom storage adapter."
1779
2007
  );
1780
2008
  }
1781
2009
  super(localStorage, options);
1782
2010
  }
1783
2011
  };
1784
2012
  export {
1785
- InMemoryCursorStore,
1786
- LocalStorageCursorStore,
2013
+ LocalStorageSessionStore,
1787
2014
  MemoryStore,
1788
2015
  SessionLogConflictError,
1789
2016
  SessionLogGapError,
@@ -1797,6 +2024,6 @@ export {
1797
2024
  StarciteSession,
1798
2025
  StarciteTailError,
1799
2026
  StarciteTokenExpiredError,
1800
- WebStorageCursorStore
2027
+ WebStorageSessionStore
1801
2028
  };
1802
2029
  //# sourceMappingURL=index.js.map