@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.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);
1471
+ state() {
1472
+ return this.log.state(this.liveSyncTask !== void 0);
1442
1473
  }
1443
1474
  /**
1444
- * Streams tail events one at a time via callback.
1475
+ * Returns the retained canonical event list.
1445
1476
  */
1446
- async tail(onEvent, options = {}) {
1447
- await this.tailBatches(async (batch) => {
1448
- for (const event of batch) {
1449
- await onEvent(event);
1450
- }
1451
- }, options);
1477
+ events() {
1478
+ return this.log.events;
1452
1479
  }
1453
1480
  /**
1454
- * Streams tail event batches grouped by incoming frame via callback.
1455
- */
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);
1464
- }
1465
- /**
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),
@@ -1697,7 +1873,20 @@ var Starcite = class {
1697
1873
  async sessionFromIdentity(input) {
1698
1874
  let sessionId = input.id;
1699
1875
  let record;
1700
- if (!sessionId) {
1876
+ if (sessionId) {
1877
+ try {
1878
+ record = await this.createSession({
1879
+ id: sessionId,
1880
+ creator_principal: input.identity.toCreatorPrincipal(),
1881
+ title: input.title,
1882
+ metadata: input.metadata
1883
+ });
1884
+ } catch (error) {
1885
+ if (!(error instanceof StarciteApiError && error.status === 409)) {
1886
+ throw error;
1887
+ }
1888
+ }
1889
+ } else {
1701
1890
  record = await this.createSession({
1702
1891
  creator_principal: input.identity.toCreatorPrincipal(),
1703
1892
  title: input.title,
@@ -1788,45 +1977,83 @@ var Starcite = class {
1788
1977
  }
1789
1978
  };
1790
1979
 
1791
- // src/cursor-store.ts
1980
+ // src/session-store.ts
1981
+ var import_zod5 = require("zod");
1792
1982
  var DEFAULT_KEY_PREFIX = "starcite";
1793
- var InMemoryCursorStore = class {
1794
- cursors;
1795
- constructor(initial = {}) {
1796
- this.cursors = new Map(Object.entries(initial));
1797
- }
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();
1798
2004
  load(sessionId) {
1799
- return this.cursors.get(sessionId);
2005
+ const stored = this.sessions.get(sessionId);
2006
+ return stored ? cloneState(stored) : void 0;
1800
2007
  }
1801
- save(sessionId, cursor) {
1802
- 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);
1803
2013
  }
1804
2014
  };
1805
- var WebStorageCursorStore = class {
2015
+ var WebStorageSessionStore = class {
1806
2016
  storage;
1807
2017
  keyForSession;
2018
+ stateSchema;
1808
2019
  constructor(storage, options = {}) {
1809
2020
  this.storage = storage;
1810
2021
  const prefix = options.keyPrefix?.trim() || DEFAULT_KEY_PREFIX;
1811
- this.keyForSession = options.keyForSession ?? ((sessionId) => `${prefix}:${sessionId}:lastSeq`);
2022
+ this.keyForSession = options.keyForSession ?? ((sessionId) => `${prefix}:${sessionId}:sessionStore`);
2023
+ this.stateSchema = options.stateSchema ?? SessionStoreStateSchema;
1812
2024
  }
1813
2025
  load(sessionId) {
1814
2026
  const raw = this.storage.getItem(this.keyForSession(sessionId));
1815
2027
  if (raw === null) {
1816
2028
  return void 0;
1817
2029
  }
1818
- const parsed = Number.parseInt(raw, 10);
1819
- 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);
1820
2041
  }
1821
- save(sessionId, cursor) {
1822
- 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));
1823
2050
  }
1824
2051
  };
1825
- var LocalStorageCursorStore = class extends WebStorageCursorStore {
2052
+ var LocalStorageSessionStore = class extends WebStorageSessionStore {
1826
2053
  constructor(options = {}) {
1827
2054
  if (typeof localStorage === "undefined") {
1828
2055
  throw new StarciteError(
1829
- "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."
1830
2057
  );
1831
2058
  }
1832
2059
  super(localStorage, options);
@@ -1834,8 +2061,7 @@ var LocalStorageCursorStore = class extends WebStorageCursorStore {
1834
2061
  };
1835
2062
  // Annotate the CommonJS export names for ESM import in node:
1836
2063
  0 && (module.exports = {
1837
- InMemoryCursorStore,
1838
- LocalStorageCursorStore,
2064
+ LocalStorageSessionStore,
1839
2065
  MemoryStore,
1840
2066
  SessionLogConflictError,
1841
2067
  SessionLogGapError,
@@ -1849,6 +2075,6 @@ var LocalStorageCursorStore = class extends WebStorageCursorStore {
1849
2075
  StarciteSession,
1850
2076
  StarciteTailError,
1851
2077
  StarciteTokenExpiredError,
1852
- WebStorageCursorStore
2078
+ WebStorageSessionStore
1853
2079
  });
1854
2080
  //# sourceMappingURL=index.cjs.map