@starcite/sdk 0.0.6 → 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.cjs CHANGED
@@ -30,8 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- InMemoryCursorStore: () => InMemoryCursorStore,
34
- LocalStorageCursorStore: () => LocalStorageCursorStore,
33
+ LocalStorageSessionStore: () => LocalStorageSessionStore,
35
34
  MemoryStore: () => MemoryStore,
36
35
  SessionLogConflictError: () => SessionLogConflictError,
37
36
  SessionLogGapError: () => SessionLogGapError,
@@ -45,7 +44,7 @@ __export(index_exports, {
45
44
  StarciteSession: () => StarciteSession,
46
45
  StarciteTailError: () => StarciteTailError,
47
46
  StarciteTokenExpiredError: () => StarciteTokenExpiredError,
48
- WebStorageCursorStore: () => WebStorageCursorStore
47
+ WebStorageSessionStore: () => WebStorageSessionStore
49
48
  });
50
49
  module.exports = __toCommonJS(index_exports);
51
50
 
@@ -129,13 +128,14 @@ function inferIdentityFromApiKey(apiKey) {
129
128
  const claims = ApiKeyClaimsSchema.parse((0, import_jose.decodeJwt)(apiKey));
130
129
  const id = claims.principal_id ?? claims.sub;
131
130
  const tenantId = claims.tenant_id;
132
- if (!(tenantId && id && claims.principal_type)) {
131
+ const type = claims.principal_type ?? "user";
132
+ if (!(tenantId && id)) {
133
133
  return void 0;
134
134
  }
135
135
  return new StarciteIdentity({
136
136
  tenantId,
137
137
  id,
138
- type: claims.principal_type
138
+ type
139
139
  });
140
140
  }
141
141
  function decodeSessionToken(token) {
@@ -336,7 +336,7 @@ var SessionLog = class {
336
336
  this.enforceRetention();
337
337
  return true;
338
338
  }
339
- getSnapshot(syncing) {
339
+ state(syncing) {
340
340
  return {
341
341
  events: this.history.slice(),
342
342
  lastSeq: this.appliedSeq,
@@ -939,7 +939,7 @@ var TailStream = class {
939
939
  this.follow = follow;
940
940
  this.shouldReconnect = follow ? opts.reconnect ?? true : false;
941
941
  this.catchUpIdleMs = opts.catchUpIdleMs ?? 1e3;
942
- this.connectionTimeoutMs = opts.connectionTimeoutMs ?? 4e3;
942
+ this.connectionTimeoutMs = opts.connectionTimeoutMs ?? 12e3;
943
943
  this.inactivityTimeoutMs = opts.inactivityTimeoutMs;
944
944
  this.maxBufferedBatches = opts.maxBufferedBatches ?? 1024;
945
945
  this.signal = opts.signal;
@@ -1310,8 +1310,10 @@ var StarciteSession = class {
1310
1310
  store;
1311
1311
  lifecycle = new import_eventemitter33.default();
1312
1312
  eventSubscriptions = /* @__PURE__ */ new Map();
1313
+ appendTask = Promise.resolve();
1313
1314
  liveSyncController;
1314
1315
  liveSyncTask;
1316
+ liveSyncCatchUpActive = false;
1315
1317
  constructor(options) {
1316
1318
  this.id = options.id;
1317
1319
  this.token = options.token;
@@ -1321,8 +1323,8 @@ var StarciteSession = class {
1321
1323
  this.producerId = crypto.randomUUID();
1322
1324
  this.store = options.store;
1323
1325
  this.log = new SessionLog(options.logOptions);
1324
- const storedState = this.store.load(this.id);
1325
- if (storedState) {
1326
+ const storedState = this.store?.load(this.id);
1327
+ if (storedState !== void 0) {
1326
1328
  this.log.hydrate(storedState);
1327
1329
  }
1328
1330
  }
@@ -1331,28 +1333,35 @@ var StarciteSession = class {
1331
1333
  *
1332
1334
  * The SDK manages `actor`, `producer_id`, and `producer_seq` automatically.
1333
1335
  */
1334
- async append(input, options) {
1336
+ append(input, options) {
1335
1337
  const parsed = SessionAppendInputSchema.parse(input);
1336
- this.producerSeq += 1;
1337
- const result = await this.appendRaw(
1338
- {
1339
- type: parsed.type ?? "content",
1340
- payload: parsed.payload ?? { text: parsed.text },
1341
- actor: parsed.actor ?? this.identity.toActor(),
1342
- producer_id: this.producerId,
1343
- producer_seq: this.producerSeq,
1344
- source: parsed.source ?? "agent",
1345
- metadata: parsed.metadata,
1346
- refs: parsed.refs,
1347
- idempotency_key: parsed.idempotencyKey,
1348
- expected_seq: parsed.expectedSeq
1349
- },
1350
- options
1338
+ const runAppend = this.appendTask.then(async () => {
1339
+ this.producerSeq += 1;
1340
+ const result = await this.appendRaw(
1341
+ {
1342
+ type: parsed.type ?? "content",
1343
+ payload: parsed.payload ?? { text: parsed.text },
1344
+ actor: parsed.actor ?? this.identity.toActor(),
1345
+ producer_id: this.producerId,
1346
+ producer_seq: this.producerSeq,
1347
+ source: parsed.source ?? "agent",
1348
+ metadata: parsed.metadata,
1349
+ refs: parsed.refs,
1350
+ idempotency_key: parsed.idempotencyKey,
1351
+ expected_seq: parsed.expectedSeq
1352
+ },
1353
+ options
1354
+ );
1355
+ return {
1356
+ seq: result.seq,
1357
+ deduped: result.deduped
1358
+ };
1359
+ });
1360
+ this.appendTask = runAppend.then(
1361
+ () => void 0,
1362
+ () => void 0
1351
1363
  );
1352
- return {
1353
- seq: result.seq,
1354
- deduped: result.deduped
1355
- };
1364
+ return runAppend;
1356
1365
  }
1357
1366
  /**
1358
1367
  * Appends a raw event payload as-is. Caller manages all fields.
@@ -1369,11 +1378,33 @@ var StarciteSession = class {
1369
1378
  AppendEventResponseSchema
1370
1379
  );
1371
1380
  }
1372
- on(eventName, listener) {
1381
+ on(eventName, listener, options) {
1373
1382
  if (eventName === "event") {
1374
1383
  const eventListener = listener;
1375
1384
  if (!this.eventSubscriptions.has(eventListener)) {
1376
- const unsubscribe = this.log.subscribe(eventListener, { replay: true });
1385
+ const eventOptions = options;
1386
+ const replay = eventOptions?.replay ?? true;
1387
+ const replayCutoffSeq = replay ? this.log.lastSeq : -1;
1388
+ const schema = eventOptions?.schema;
1389
+ const dispatch = (event) => {
1390
+ const parsedEvent = this.parseOnEvent(event, schema);
1391
+ if (!parsedEvent) {
1392
+ return;
1393
+ }
1394
+ const classifiedContext = this.resolveEventContext(
1395
+ event.seq,
1396
+ replayCutoffSeq,
1397
+ this.liveSyncCatchUpActive
1398
+ );
1399
+ try {
1400
+ this.observeEventListenerResult(
1401
+ eventListener(parsedEvent, classifiedContext)
1402
+ );
1403
+ } catch (error) {
1404
+ this.emitStreamError(error);
1405
+ }
1406
+ };
1407
+ const unsubscribe = this.log.subscribe(dispatch, { replay });
1377
1408
  this.eventSubscriptions.set(eventListener, unsubscribe);
1378
1409
  }
1379
1410
  this.ensureLiveSync();
@@ -1435,77 +1466,195 @@ var StarciteSession = class {
1435
1466
  this.persistLogState();
1436
1467
  }
1437
1468
  /**
1438
- * Returns a stable snapshot of the current canonical in-memory log.
1469
+ * Returns a stable view of the current canonical in-memory log state.
1439
1470
  */
1440
- getSnapshot() {
1441
- return this.log.getSnapshot(this.liveSyncTask !== void 0);
1442
- }
1443
- /**
1444
- * Streams tail events one at a time via callback.
1445
- */
1446
- async tail(onEvent, options = {}) {
1447
- await this.tailBatches(async (batch) => {
1448
- for (const event of batch) {
1449
- await onEvent(event);
1450
- }
1451
- }, options);
1471
+ state() {
1472
+ return this.log.state(this.liveSyncTask !== void 0);
1452
1473
  }
1453
1474
  /**
1454
- * Streams tail event batches grouped by incoming frame via callback.
1475
+ * Returns the retained canonical event list.
1455
1476
  */
1456
- async tailBatches(onBatch, options = {}) {
1457
- await new TailStream({
1458
- sessionId: this.id,
1459
- token: this.token,
1460
- websocketBaseUrl: this.transport.websocketBaseUrl,
1461
- websocketFactory: this.transport.websocketFactory,
1462
- options
1463
- }).subscribe(onBatch);
1477
+ events() {
1478
+ return this.log.events;
1464
1479
  }
1465
1480
  /**
1466
- * Durably consumes events and checkpoints `event.seq` after each successful handler invocation.
1481
+ * Streams canonical events as an async iterator.
1482
+ *
1483
+ * Replay semantics and schema validation mirror `session.on("event", ...)`.
1467
1484
  */
1468
- async consume(options) {
1469
- const {
1470
- cursorStore,
1471
- handler,
1472
- cursor: requestedCursor,
1473
- ...tailOptions
1474
- } = options;
1475
- let cursor;
1476
- if (requestedCursor !== void 0) {
1477
- cursor = requestedCursor;
1478
- } else {
1479
- try {
1480
- cursor = await cursorStore.load(this.id) ?? 0;
1481
- } catch (error) {
1482
- throw new StarciteError(
1483
- `consume() failed to load cursor for session '${this.id}': ${error instanceof Error ? error.message : String(error)}`
1484
- );
1485
+ tail(options = {}) {
1486
+ const { replay = true, schema, ...tailOptions } = options;
1487
+ const replayCutoffSeq = replay ? this.log.lastSeq : -1;
1488
+ const startCursor = tailOptions.cursor ?? this.log.lastSeq;
1489
+ const session = this;
1490
+ const parseEvent = (event) => session.parseTailEvent(event, schema);
1491
+ return {
1492
+ async *[Symbol.asyncIterator]() {
1493
+ if (replay) {
1494
+ for (const replayEvent of session.log.events) {
1495
+ yield {
1496
+ event: parseEvent(replayEvent),
1497
+ context: { phase: "replay", replayed: true }
1498
+ };
1499
+ }
1500
+ }
1501
+ yield* session.iterateLiveTail({
1502
+ parseEvent,
1503
+ replayCutoffSeq,
1504
+ startCursor,
1505
+ tailOptions
1506
+ });
1485
1507
  }
1508
+ };
1509
+ }
1510
+ parseOnEvent(event, schema) {
1511
+ if (!schema) {
1512
+ return event;
1486
1513
  }
1487
- const stream = new TailStream({
1514
+ try {
1515
+ return schema.parse(event);
1516
+ } catch (error) {
1517
+ this.emitStreamError(
1518
+ new StarciteError(
1519
+ `session.on("event") schema validation failed for session '${this.id}': ${error instanceof Error ? error.message : String(error)}`
1520
+ )
1521
+ );
1522
+ return void 0;
1523
+ }
1524
+ }
1525
+ parseTailEvent(event, schema) {
1526
+ if (!schema) {
1527
+ return event;
1528
+ }
1529
+ try {
1530
+ return schema.parse(event);
1531
+ } catch (error) {
1532
+ throw new StarciteError(
1533
+ `session.tail() schema validation failed for session '${this.id}': ${error instanceof Error ? error.message : String(error)}`
1534
+ );
1535
+ }
1536
+ }
1537
+ resolveEventContext(eventSeq, replayCutoffSeq, forceReplay = false) {
1538
+ const replayed = forceReplay || eventSeq <= replayCutoffSeq;
1539
+ return replayed ? { phase: "replay", replayed: true } : { phase: "live", replayed: false };
1540
+ }
1541
+ observeEventListenerResult(result) {
1542
+ Promise.resolve(result).catch((error) => {
1543
+ this.emitStreamError(error);
1544
+ });
1545
+ }
1546
+ createTailAbortController(outerSignal) {
1547
+ const controller = new AbortController();
1548
+ if (!outerSignal) {
1549
+ return { controller, detach: () => void 0 };
1550
+ }
1551
+ const abortFromOuterSignal = () => {
1552
+ controller.abort(outerSignal.reason);
1553
+ };
1554
+ if (outerSignal.aborted) {
1555
+ controller.abort(outerSignal.reason);
1556
+ return { controller, detach: () => void 0 };
1557
+ }
1558
+ outerSignal.addEventListener("abort", abortFromOuterSignal, { once: true });
1559
+ return {
1560
+ controller,
1561
+ detach: () => {
1562
+ outerSignal.removeEventListener("abort", abortFromOuterSignal);
1563
+ }
1564
+ };
1565
+ }
1566
+ createTailRuntime({
1567
+ parseEvent,
1568
+ replayCutoffSeq,
1569
+ startCursor,
1570
+ tailOptions
1571
+ }) {
1572
+ const queue = [];
1573
+ let notify;
1574
+ let done = false;
1575
+ let failure;
1576
+ const shouldApplyToLog = tailOptions.agent === void 0;
1577
+ const { controller, detach } = this.createTailAbortController(
1578
+ tailOptions.signal
1579
+ );
1580
+ const wake = () => {
1581
+ notify?.();
1582
+ };
1583
+ const streamTask = new TailStream({
1488
1584
  sessionId: this.id,
1489
1585
  token: this.token,
1490
1586
  websocketBaseUrl: this.transport.websocketBaseUrl,
1491
1587
  websocketFactory: this.transport.websocketFactory,
1492
1588
  options: {
1493
1589
  ...tailOptions,
1494
- cursor
1590
+ cursor: startCursor,
1591
+ signal: controller.signal
1592
+ }
1593
+ }).subscribe((batch) => {
1594
+ const queuedEvents = shouldApplyToLog ? this.log.applyBatch(batch) : batch;
1595
+ if (shouldApplyToLog && queuedEvents.length > 0) {
1596
+ this.persistLogState();
1495
1597
  }
1598
+ for (const event of queuedEvents) {
1599
+ queue.push({
1600
+ event: parseEvent(event),
1601
+ context: this.resolveEventContext(event.seq, replayCutoffSeq)
1602
+ });
1603
+ }
1604
+ wake();
1605
+ }).catch((error) => {
1606
+ failure = error;
1607
+ }).finally(() => {
1608
+ done = true;
1609
+ wake();
1496
1610
  });
1497
- await stream.subscribe(async (batch) => {
1498
- for (const event of batch) {
1499
- await handler(event);
1500
- try {
1501
- await cursorStore.save(this.id, event.seq);
1502
- } catch (error) {
1503
- throw new StarciteError(
1504
- `consume() failed to save cursor for session '${this.id}': ${error instanceof Error ? error.message : String(error)}`
1505
- );
1611
+ return {
1612
+ next: async () => {
1613
+ while (queue.length === 0 && !done && !failure) {
1614
+ await new Promise((resolve) => {
1615
+ notify = resolve;
1616
+ });
1617
+ notify = void 0;
1506
1618
  }
1619
+ const next = queue.shift();
1620
+ return next;
1621
+ },
1622
+ getFailure: () => failure,
1623
+ dispose: async () => {
1624
+ controller.abort();
1625
+ detach();
1626
+ await streamTask;
1507
1627
  }
1628
+ };
1629
+ }
1630
+ async *iterateLiveTail({
1631
+ parseEvent,
1632
+ replayCutoffSeq,
1633
+ startCursor,
1634
+ tailOptions
1635
+ }) {
1636
+ const runtime = this.createTailRuntime({
1637
+ parseEvent,
1638
+ replayCutoffSeq,
1639
+ startCursor,
1640
+ tailOptions
1508
1641
  });
1642
+ try {
1643
+ while (true) {
1644
+ const next = await runtime.next();
1645
+ if (next) {
1646
+ yield next;
1647
+ continue;
1648
+ }
1649
+ const failure = runtime.getFailure();
1650
+ if (failure) {
1651
+ throw failure;
1652
+ }
1653
+ return;
1654
+ }
1655
+ } finally {
1656
+ await runtime.dispose();
1657
+ }
1509
1658
  }
1510
1659
  emitStreamError(error) {
1511
1660
  const streamError = error instanceof Error ? error : new StarciteError(`Session stream failed: ${String(error)}`);
@@ -1530,67 +1679,94 @@ var StarciteSession = class {
1530
1679
  }).finally(() => {
1531
1680
  this.liveSyncTask = void 0;
1532
1681
  this.liveSyncController = void 0;
1682
+ if (this.eventSubscriptions.size > 0) {
1683
+ this.ensureLiveSync();
1684
+ }
1533
1685
  });
1534
1686
  }
1535
1687
  async runLiveSync(signal) {
1688
+ let shouldRunCatchUpPass = this.log.lastSeq === 0;
1689
+ let retryDelayMs = 250;
1536
1690
  while (!signal.aborted && this.eventSubscriptions.size > 0) {
1537
- const stream = new TailStream({
1538
- sessionId: this.id,
1539
- token: this.token,
1540
- websocketBaseUrl: this.transport.websocketBaseUrl,
1541
- websocketFactory: this.transport.websocketFactory,
1542
- options: {
1543
- cursor: this.log.lastSeq,
1544
- signal
1545
- }
1546
- });
1691
+ this.liveSyncCatchUpActive = shouldRunCatchUpPass;
1547
1692
  try {
1548
- await stream.subscribe((batch) => {
1549
- const appliedEvents = this.log.applyBatch(batch);
1550
- if (appliedEvents.length > 0) {
1551
- this.persistLogState();
1552
- }
1553
- });
1693
+ await this.subscribeLiveSyncPass(signal, !shouldRunCatchUpPass);
1694
+ shouldRunCatchUpPass = false;
1695
+ retryDelayMs = 250;
1554
1696
  } catch (error) {
1555
1697
  if (signal.aborted) {
1556
1698
  return;
1557
1699
  }
1558
1700
  if (error instanceof SessionLogGapError) {
1701
+ shouldRunCatchUpPass = true;
1559
1702
  continue;
1560
1703
  }
1561
- throw error;
1704
+ this.emitStreamError(error);
1705
+ shouldRunCatchUpPass = true;
1706
+ await this.waitForLiveSyncRetry(retryDelayMs, signal);
1707
+ retryDelayMs = Math.min(retryDelayMs * 2, 5e3);
1708
+ } finally {
1709
+ this.liveSyncCatchUpActive = false;
1562
1710
  }
1563
1711
  }
1564
1712
  }
1713
+ async subscribeLiveSyncPass(signal, follow) {
1714
+ const stream = new TailStream({
1715
+ sessionId: this.id,
1716
+ token: this.token,
1717
+ websocketBaseUrl: this.transport.websocketBaseUrl,
1718
+ websocketFactory: this.transport.websocketFactory,
1719
+ options: {
1720
+ cursor: this.log.lastSeq,
1721
+ follow,
1722
+ signal
1723
+ }
1724
+ });
1725
+ await stream.subscribe((batch) => {
1726
+ const appliedEvents = this.log.applyBatch(batch);
1727
+ if (appliedEvents.length > 0) {
1728
+ this.persistLogState();
1729
+ }
1730
+ });
1731
+ }
1565
1732
  persistLogState() {
1733
+ if (!this.store) {
1734
+ return;
1735
+ }
1566
1736
  this.store.save(this.id, {
1567
1737
  cursor: this.log.cursor,
1568
- events: [...this.log.events]
1738
+ events: [...this.log.events],
1739
+ metadata: {
1740
+ schemaVersion: 1,
1741
+ updatedAtMs: Date.now()
1742
+ }
1569
1743
  });
1570
1744
  }
1571
- };
1572
-
1573
- // src/session-store.ts
1574
- function cloneEvents(events) {
1575
- return events.map((event) => structuredClone(event));
1576
- }
1577
- function cloneState(state) {
1578
- return {
1579
- cursor: state.cursor,
1580
- events: cloneEvents(state.events)
1581
- };
1582
- }
1583
- var MemoryStore = class {
1584
- sessions = /* @__PURE__ */ new Map();
1585
- load(sessionId) {
1586
- const stored = this.sessions.get(sessionId);
1587
- return stored ? cloneState(stored) : void 0;
1588
- }
1589
- save(sessionId, state) {
1590
- this.sessions.set(sessionId, cloneState(state));
1591
- }
1592
- clear(sessionId) {
1593
- this.sessions.delete(sessionId);
1745
+ waitForLiveSyncRetry(delayMs, signal) {
1746
+ if (delayMs <= 0 || signal.aborted) {
1747
+ return Promise.resolve();
1748
+ }
1749
+ return new Promise((resolve) => {
1750
+ let settled = false;
1751
+ const timer = setTimeout(() => {
1752
+ if (settled) {
1753
+ return;
1754
+ }
1755
+ settled = true;
1756
+ signal.removeEventListener("abort", onAbort);
1757
+ resolve();
1758
+ }, delayMs);
1759
+ const onAbort = () => {
1760
+ if (settled) {
1761
+ return;
1762
+ }
1763
+ settled = true;
1764
+ clearTimeout(timer);
1765
+ signal.removeEventListener("abort", onAbort);
1766
+ resolve();
1767
+ };
1768
+ signal.addEventListener("abort", onAbort, { once: true });
1769
+ });
1594
1770
  }
1595
1771
  };
1596
1772
 
@@ -1630,7 +1806,7 @@ var Starcite = class {
1630
1806
  }
1631
1807
  this.authBaseUrl = resolveAuthBaseUrl(options.authUrl, apiKey);
1632
1808
  const websocketFactory = options.websocketFactory ?? defaultWebSocketFactory;
1633
- this.store = options.store ?? new MemoryStore();
1809
+ this.store = options.store;
1634
1810
  this.transport = {
1635
1811
  baseUrl,
1636
1812
  websocketBaseUrl: toWebSocketBaseUrl(baseUrl),
@@ -1801,45 +1977,83 @@ var Starcite = class {
1801
1977
  }
1802
1978
  };
1803
1979
 
1804
- // src/cursor-store.ts
1980
+ // src/session-store.ts
1981
+ var import_zod5 = require("zod");
1805
1982
  var DEFAULT_KEY_PREFIX = "starcite";
1806
- var InMemoryCursorStore = class {
1807
- cursors;
1808
- constructor(initial = {}) {
1809
- this.cursors = new Map(Object.entries(initial));
1810
- }
1983
+ var SessionStoreMetadataSchema = import_zod5.z.object({
1984
+ schemaVersion: import_zod5.z.literal(1),
1985
+ updatedAtMs: import_zod5.z.number().int().nonnegative()
1986
+ });
1987
+ var SessionStoreStateSchema = import_zod5.z.object({
1988
+ cursor: import_zod5.z.number().int().nonnegative(),
1989
+ events: import_zod5.z.array(TailEventSchema),
1990
+ metadata: SessionStoreMetadataSchema.optional()
1991
+ });
1992
+ function cloneEvents(events) {
1993
+ return events.map((event) => structuredClone(event));
1994
+ }
1995
+ function cloneState(state) {
1996
+ return {
1997
+ cursor: state.cursor,
1998
+ events: cloneEvents(state.events),
1999
+ metadata: state.metadata ? { ...state.metadata } : void 0
2000
+ };
2001
+ }
2002
+ var MemoryStore = class {
2003
+ sessions = /* @__PURE__ */ new Map();
1811
2004
  load(sessionId) {
1812
- return this.cursors.get(sessionId);
2005
+ const stored = this.sessions.get(sessionId);
2006
+ return stored ? cloneState(stored) : void 0;
1813
2007
  }
1814
- save(sessionId, cursor) {
1815
- this.cursors.set(sessionId, cursor);
2008
+ save(sessionId, state) {
2009
+ this.sessions.set(sessionId, cloneState(state));
2010
+ }
2011
+ clear(sessionId) {
2012
+ this.sessions.delete(sessionId);
1816
2013
  }
1817
2014
  };
1818
- var WebStorageCursorStore = class {
2015
+ var WebStorageSessionStore = class {
1819
2016
  storage;
1820
2017
  keyForSession;
2018
+ stateSchema;
1821
2019
  constructor(storage, options = {}) {
1822
2020
  this.storage = storage;
1823
2021
  const prefix = options.keyPrefix?.trim() || DEFAULT_KEY_PREFIX;
1824
- this.keyForSession = options.keyForSession ?? ((sessionId) => `${prefix}:${sessionId}:lastSeq`);
2022
+ this.keyForSession = options.keyForSession ?? ((sessionId) => `${prefix}:${sessionId}:sessionStore`);
2023
+ this.stateSchema = options.stateSchema ?? SessionStoreStateSchema;
1825
2024
  }
1826
2025
  load(sessionId) {
1827
2026
  const raw = this.storage.getItem(this.keyForSession(sessionId));
1828
2027
  if (raw === null) {
1829
2028
  return void 0;
1830
2029
  }
1831
- const parsed = Number.parseInt(raw, 10);
1832
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : void 0;
2030
+ let decoded;
2031
+ try {
2032
+ decoded = JSON.parse(raw);
2033
+ } catch {
2034
+ return void 0;
2035
+ }
2036
+ const parsed = this.stateSchema.safeParse(decoded);
2037
+ if (!parsed.success) {
2038
+ return void 0;
2039
+ }
2040
+ return cloneState(parsed.data);
1833
2041
  }
1834
- save(sessionId, cursor) {
1835
- this.storage.setItem(this.keyForSession(sessionId), `${cursor}`);
2042
+ save(sessionId, state) {
2043
+ this.storage.setItem(
2044
+ this.keyForSession(sessionId),
2045
+ JSON.stringify(cloneState(state))
2046
+ );
2047
+ }
2048
+ clear(sessionId) {
2049
+ this.storage.removeItem?.(this.keyForSession(sessionId));
1836
2050
  }
1837
2051
  };
1838
- var LocalStorageCursorStore = class extends WebStorageCursorStore {
2052
+ var LocalStorageSessionStore = class extends WebStorageSessionStore {
1839
2053
  constructor(options = {}) {
1840
2054
  if (typeof localStorage === "undefined") {
1841
2055
  throw new StarciteError(
1842
- "localStorage is not available in this runtime. Use WebStorageCursorStore with a custom storage adapter."
2056
+ "localStorage is not available in this runtime. Use WebStorageSessionStore with a custom storage adapter."
1843
2057
  );
1844
2058
  }
1845
2059
  super(localStorage, options);
@@ -1847,8 +2061,7 @@ var LocalStorageCursorStore = class extends WebStorageCursorStore {
1847
2061
  };
1848
2062
  // Annotate the CommonJS export names for ESM import in node:
1849
2063
  0 && (module.exports = {
1850
- InMemoryCursorStore,
1851
- LocalStorageCursorStore,
2064
+ LocalStorageSessionStore,
1852
2065
  MemoryStore,
1853
2066
  SessionLogConflictError,
1854
2067
  SessionLogGapError,
@@ -1862,6 +2075,6 @@ var LocalStorageCursorStore = class extends WebStorageCursorStore {
1862
2075
  StarciteSession,
1863
2076
  StarciteTailError,
1864
2077
  StarciteTokenExpiredError,
1865
- WebStorageCursorStore
2078
+ WebStorageSessionStore
1866
2079
  });
1867
2080
  //# sourceMappingURL=index.cjs.map