experimental-a2 0.22.0 → 0.24.0

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.
Files changed (131) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/{actor-CqibLgjQ.d.ts → actor-Bqyb7gwf.d.ts} +3 -3
  3. package/dist/{actor-CqibLgjQ.d.ts.map → actor-Bqyb7gwf.d.ts.map} +1 -1
  4. package/dist/actor-client.d.ts +1 -1
  5. package/dist/actor-client.js +2 -2
  6. package/dist/actor-react.d.ts +2 -2
  7. package/dist/actor-react.js +5 -5
  8. package/dist/{actor-shared-DFET5q16.js → actor-shared-B4OB6Qo7.js} +3 -3
  9. package/dist/{actor-shared-DFET5q16.js.map → actor-shared-B4OB6Qo7.js.map} +1 -1
  10. package/dist/actor.d.ts +1 -1
  11. package/dist/actor.js +4 -4
  12. package/dist/{ai-DAu77CDr.d.ts → ai-BZUu5-b7.d.ts} +2 -2
  13. package/dist/{ai-DAu77CDr.d.ts.map → ai-BZUu5-b7.d.ts.map} +1 -1
  14. package/dist/{ai-EEfKyc-z.js → ai-CCCNxd3L.js} +3 -3
  15. package/dist/{ai-EEfKyc-z.js.map → ai-CCCNxd3L.js.map} +1 -1
  16. package/dist/ai-client.d.ts +2 -2
  17. package/dist/ai-client.js +2 -2
  18. package/dist/ai-fx.d.ts +1 -1
  19. package/dist/ai-fx.js +25 -3
  20. package/dist/ai-fx.js.map +1 -1
  21. package/dist/{ai-harness-DTjkAr2T.d.ts → ai-harness-C8aV869s.d.ts} +2 -2
  22. package/dist/{ai-harness-DTjkAr2T.d.ts.map → ai-harness-C8aV869s.d.ts.map} +1 -1
  23. package/dist/ai-pi.d.ts +1 -1
  24. package/dist/ai-react.d.ts +4 -4
  25. package/dist/ai-react.js +2 -2
  26. package/dist/ai-server.d.ts +3 -3
  27. package/dist/ai-server.js +4 -4
  28. package/dist/ai.d.ts +1 -1
  29. package/dist/ai.js +1 -1
  30. package/dist/{client-CUMbS_38.d.ts → client-CCJASjUM.d.ts} +2 -2
  31. package/dist/{client-CUMbS_38.d.ts.map → client-CCJASjUM.d.ts.map} +1 -1
  32. package/dist/{client-BQS4i_V9.js → client-sq-djxhD.js} +4 -4
  33. package/dist/{client-BQS4i_V9.js.map → client-sq-djxhD.js.map} +1 -1
  34. package/dist/client.d.ts +2 -2
  35. package/dist/client.js +1 -1
  36. package/dist/{contract-CWukt01d.js → contract-DqfBSIqJ.js} +2 -2
  37. package/dist/{contract-CWukt01d.js.map → contract-DqfBSIqJ.js.map} +1 -1
  38. package/dist/index.d.ts +3 -3
  39. package/dist/index.js +3 -3
  40. package/dist/{initial-snapshot-D3Lve43o.d.ts → initial-snapshot-BA6xwv10.d.ts} +2 -2
  41. package/dist/initial-snapshot-BA6xwv10.d.ts.map +1 -0
  42. package/dist/{react-BoHvC3v9.d.ts → react-KC4eW7yG.d.ts} +3 -3
  43. package/dist/{react-BoHvC3v9.d.ts.map → react-KC4eW7yG.d.ts.map} +1 -1
  44. package/dist/react.d.ts +1 -1
  45. package/dist/react.js +1 -1
  46. package/dist/{reducer-CjmvZSAb.js → reducer-Bswr11ar.js} +2 -2
  47. package/dist/{reducer-CjmvZSAb.js.map → reducer-Bswr11ar.js.map} +1 -1
  48. package/dist/scheduler-qstash.d.ts +2 -2
  49. package/dist/scheduler-qstash.js +2 -2
  50. package/dist/scheduler-vercel.d.ts +2 -2
  51. package/dist/scheduler-vercel.js +1 -1
  52. package/dist/{server-qgez0Xny.d.ts → server-B9gu0qi1.d.ts} +9 -3
  53. package/dist/server-B9gu0qi1.d.ts.map +1 -0
  54. package/dist/{server-DtFrWxi3.js → server-DX7mM6nm.js} +22 -14
  55. package/dist/server-DX7mM6nm.js.map +1 -0
  56. package/dist/server.d.ts +2 -2
  57. package/dist/server.js +1 -1
  58. package/dist/{session-clear-wHd5GCpZ.js → session-clear-XJYpmUpg.js} +14 -2
  59. package/dist/session-clear-XJYpmUpg.js.map +1 -0
  60. package/dist/{session-clear-D1PFe7xl.d.ts → session-clear-vfonou9S.d.ts} +2 -2
  61. package/dist/session-clear-vfonou9S.d.ts.map +1 -0
  62. package/dist/{store-D0MhBvg6.d.ts → store-DEzcHZOA.d.ts} +11 -1
  63. package/dist/store-DEzcHZOA.d.ts.map +1 -0
  64. package/dist/store-N8PXxDAS.js.map +1 -1
  65. package/dist/{store-codec-DwrWlgQx.js → store-codec-pAG2oPgQ.js} +4 -4
  66. package/dist/store-codec-pAG2oPgQ.js.map +1 -0
  67. package/dist/store-memory.d.ts +1 -1
  68. package/dist/store-memory.d.ts.map +1 -1
  69. package/dist/store-memory.js +11 -4
  70. package/dist/store-memory.js.map +1 -1
  71. package/dist/store-postgres.d.ts +1 -1
  72. package/dist/store-postgres.d.ts.map +1 -1
  73. package/dist/store-postgres.js +26 -7
  74. package/dist/store-postgres.js.map +1 -1
  75. package/dist/store-redis-http.d.ts +1 -1
  76. package/dist/store-redis-http.js +2 -2
  77. package/dist/{store-redis-notify-CsJw_hfD.js → store-redis-notify-ys_-VrSP.js} +34 -18
  78. package/dist/store-redis-notify-ys_-VrSP.js.map +1 -0
  79. package/dist/store-redis.d.ts +1 -1
  80. package/dist/store-redis.js +2 -2
  81. package/dist/store-sqlite.d.ts +1 -1
  82. package/dist/store-sqlite.d.ts.map +1 -1
  83. package/dist/store-sqlite.js +11 -4
  84. package/dist/store-sqlite.js.map +1 -1
  85. package/dist/store-valkey.d.ts +33 -0
  86. package/dist/store-valkey.d.ts.map +1 -0
  87. package/dist/store-valkey.js +218 -0
  88. package/dist/store-valkey.js.map +1 -0
  89. package/dist/{wire-CV-Z7wz-.js → wire-BFE9sIQf.js} +3 -2
  90. package/dist/wire-BFE9sIQf.js.map +1 -0
  91. package/docs/client/03-presence.mdx +1 -0
  92. package/docs/contract/03-sessions.mdx +51 -0
  93. package/docs/reference/01-core.mdx +1 -0
  94. package/docs/reference/02-server.mdx +44 -4
  95. package/docs/reference/06-errors.mdx +1 -0
  96. package/docs/stores/01-overview.mdx +1 -0
  97. package/docs/stores/02-redis.mdx +3 -1
  98. package/docs/stores/07-valkey.mdx +97 -0
  99. package/examples/playground/app/a2slack/consumers/search.ts +1 -1
  100. package/examples/playground/app/a2slack/db/migrations/0005_a2slack_feed_v2.sql +7 -0
  101. package/examples/playground/app/a2slack/db/migrations/meta/0005_snapshot.json +1165 -0
  102. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +8 -1
  103. package/examples/playground/app/a2slack/db/queries.ts +1 -1
  104. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +17 -14
  105. package/examples/playground/app/a2slack/search-index.ts +1 -1
  106. package/examples/playground/app/a2slack/server.ts +2 -2
  107. package/examples/playground/lib/store.ts +2 -0
  108. package/examples/playground/package.json +1 -1
  109. package/package.json +8 -2
  110. package/src/ai-fx.ts +46 -3
  111. package/src/errors.ts +39 -0
  112. package/src/idempotent-replay.ts +4 -3
  113. package/src/libfx.d.ts +2 -0
  114. package/src/server.ts +32 -9
  115. package/src/store-memory.ts +26 -4
  116. package/src/store-postgres.ts +36 -6
  117. package/src/store-redis-core.ts +40 -15
  118. package/src/store-redis-notify.ts +8 -2
  119. package/src/store-sqlite.ts +25 -4
  120. package/src/store-valkey.ts +376 -0
  121. package/src/store.ts +10 -0
  122. package/src/wire.ts +1 -0
  123. package/dist/initial-snapshot-D3Lve43o.d.ts.map +0 -1
  124. package/dist/server-DtFrWxi3.js.map +0 -1
  125. package/dist/server-qgez0Xny.d.ts.map +0 -1
  126. package/dist/session-clear-D1PFe7xl.d.ts.map +0 -1
  127. package/dist/session-clear-wHd5GCpZ.js.map +0 -1
  128. package/dist/store-D0MhBvg6.d.ts.map +0 -1
  129. package/dist/store-codec-DwrWlgQx.js.map +0 -1
  130. package/dist/store-redis-notify-CsJw_hfD.js.map +0 -1
  131. package/dist/wire-CV-Z7wz-.js.map +0 -1
@@ -0,0 +1,376 @@
1
+ /**
2
+ * experimental-a2/store-valkey — the Redis store on node-redis connections,
3
+ * for Valkey and Redis servers that need more than a URL: TLS settings,
4
+ * rotating credentials (such as ElastiCache IAM tokens), reconnect
5
+ * strategies, and timeouts. Storage and the live feed are the ones
6
+ * experimental-a2/store-redis uses (store-redis-notify.ts); this module owns
7
+ * the two connections.
8
+ *
9
+ * A2 creates both clients from the same options: one for commands, one
10
+ * dedicated subscriber. node-redis restores subscriptions before it reports
11
+ * a reconnect ready, so every feed and presence subscription on a channel
12
+ * catches up right after a reconnect instead of waiting for the safety read.
13
+ * A client whose reconnect strategy gives up is replaced on next use.
14
+ *
15
+ * Cluster mode is out: each script's keys share a session's hash tag, but
16
+ * the store runs on one non-cluster connection for commands and one for
17
+ * pub/sub. `redis` (node-redis) is an optional peer dependency.
18
+ */
19
+
20
+ import type { RedisClientOptions } from 'redis'
21
+ import { A2Error } from './errors.ts'
22
+ import { retryableLazy } from './retryable-lazy.ts'
23
+ import {
24
+ createRedisNotifyStore,
25
+ type RedisConnection,
26
+ } from './store-redis-notify.ts'
27
+ import {
28
+ RANDOM_IDS,
29
+ SYSTEM_CLOCK,
30
+ type A2Store,
31
+ type Clock,
32
+ type IdSource,
33
+ } from './store.ts'
34
+
35
+ /**
36
+ * node-redis `createClient` options. A2 reads replies in their RESP2
37
+ * shapes, so it always connects with `RESP: 2`, which rules out `RESP`
38
+ * and the options that need RESP3.
39
+ */
40
+ export type ValkeyClientOptions = Omit<
41
+ RedisClientOptions,
42
+ 'RESP' | 'clientSideCache' | 'emitInvalidate' | 'maintNotifications'
43
+ >
44
+
45
+ export type ValkeyStoreOptions = {
46
+ /** `redis://` or `rediss://` URL. Overrides `clientOptions.url`. */
47
+ url?: string | undefined
48
+ /**
49
+ * Options for both clients A2 creates: `socket` (TLS, connect timeout,
50
+ * `reconnectStrategy`), `credentialsProvider`, `commandOptions`,
51
+ * `disableOfflineQueue`, and so on.
52
+ */
53
+ clientOptions?: ValkeyClientOptions
54
+ /** Key prefix — isolates multiple apps on one server. Default `'a2'`. */
55
+ keyPrefix?: string
56
+ /** Injectable clock — every stored timestamp comes from here. */
57
+ clock?: Clock
58
+ /** Injectable id source for generated event ids. */
59
+ ids?: IdSource
60
+ }
61
+
62
+ export type ValkeyStore = A2Store & {
63
+ /** Destroy the command client and the subscriber. */
64
+ close(): Promise<void>
65
+ }
66
+
67
+ /** The node-redis client surface this module uses. */
68
+ type Client = {
69
+ readonly isOpen: boolean
70
+ readonly isReady: boolean
71
+ connect(): Promise<unknown>
72
+ destroy(): void
73
+ on(event: 'error' | 'ready', listener: () => void): unknown
74
+ sendCommand(
75
+ args: string[],
76
+ options: { typeMapping: Record<number, unknown> },
77
+ ): Promise<unknown>
78
+ subscribe(
79
+ channel: string,
80
+ listener: (message: string, channel: string) => void,
81
+ ): Promise<unknown>
82
+ unsubscribe(channel: string): Promise<unknown>
83
+ }
84
+
85
+ type Driver = {
86
+ create(): Client
87
+ bytes: { typeMapping: Record<number, unknown> }
88
+ }
89
+
90
+ /** Overrides any `commandOptions.typeMapping`: replies decode as strings. */
91
+ const STRING_REPLIES = { typeMapping: {} }
92
+
93
+ /**
94
+ * node-redis 6 needs `destroy()` to release a client whose reconnect
95
+ * strategy gave up; node-redis 5 throws there instead.
96
+ */
97
+ const dispose = (client: Client | null | undefined): void => {
98
+ try {
99
+ client?.destroy()
100
+ } catch {}
101
+ }
102
+
103
+ const closedError = (): A2Error =>
104
+ new A2Error('STORE_UNAVAILABLE', 'the store was closed')
105
+
106
+ /** node-redis 6's default `commandOptions.timeout`; node-redis 5 has none. */
107
+ const DEFAULT_TIMEOUT_MS = 5_000
108
+
109
+ type OwnedEntry = {
110
+ client: Client | null
111
+ created: Promise<Client>
112
+ /** Settles when the client first reports ready, or its connect fails. */
113
+ connected: Promise<unknown> | null
114
+ ready: boolean
115
+ }
116
+
117
+ /**
118
+ * One A2-owned client at a time. `get` hands a new client out once it
119
+ * first reports ready, waiting at most `timeoutMs` per call while it keeps
120
+ * connecting, so `disableOfflineQueue` cannot reject a cold start's first
121
+ * calls. Later reconnects follow node-redis's offline queue,
122
+ * `disableOfflineQueue`, and command timeouts. A client whose connect
123
+ * failed, or whose reconnect strategy gave up, is closed for good, so the
124
+ * next `get` replaces it.
125
+ */
126
+ const ownedClient = (options: {
127
+ driver: () => Promise<Driver>
128
+ opened: (client: Client) => void
129
+ timeoutMs: number
130
+ }) => {
131
+ let current: OwnedEntry | null = null
132
+ let closed = false
133
+
134
+ const firstReady = async (entry: OwnedEntry): Promise<Client> => {
135
+ const client = await entry.created
136
+ if (entry.ready) return client
137
+ let timer: ReturnType<typeof setTimeout> | undefined
138
+ try {
139
+ await Promise.race([
140
+ entry.connected,
141
+ new Promise<never>((_, reject) => {
142
+ timer = setTimeout(() => {
143
+ reject(
144
+ new A2Error(
145
+ 'STORE_UNAVAILABLE',
146
+ `connect timed out after ${options.timeoutMs}ms`,
147
+ ),
148
+ )
149
+ }, options.timeoutMs)
150
+ }),
151
+ ])
152
+ } catch (err) {
153
+ if (err instanceof A2Error) throw err
154
+ throw new A2Error('STORE_UNAVAILABLE', 'connect failed', { cause: err })
155
+ } finally {
156
+ clearTimeout(timer)
157
+ }
158
+ return client
159
+ }
160
+
161
+ return {
162
+ get(): Promise<Client> {
163
+ if (closed) return Promise.reject(closedError())
164
+ if (current && (current.client === null || current.client.isOpen))
165
+ return firstReady(current)
166
+ dispose(current?.client)
167
+ const entry: OwnedEntry = {
168
+ client: null,
169
+ connected: null,
170
+ ready: false,
171
+ created: options.driver().then((driver) => {
172
+ if (closed) throw closedError()
173
+ const client = driver.create()
174
+ entry.client = client
175
+ // Without a listener, node-redis's 'error' event crashes the
176
+ // process; failures still surface as command rejections.
177
+ client.on('error', () => {})
178
+ entry.connected = client.connect().then(
179
+ () => {
180
+ entry.ready = true
181
+ return undefined
182
+ },
183
+ (err: unknown) => {
184
+ if (current === entry) {
185
+ dispose(client)
186
+ current = null
187
+ }
188
+ throw err
189
+ },
190
+ )
191
+ void entry.connected.catch(() => {})
192
+ options.opened(client)
193
+ return client
194
+ }),
195
+ }
196
+ current = entry
197
+ void entry.created.catch(() => {
198
+ if (current === entry) current = null
199
+ })
200
+ return firstReady(entry)
201
+ },
202
+ close(): void {
203
+ closed = true
204
+ dispose(current?.client)
205
+ current = null
206
+ },
207
+ }
208
+ }
209
+
210
+ export function valkey(options: ValkeyStoreOptions = {}): ValkeyStore {
211
+ const clock = options.clock ?? SYSTEM_CLOCK
212
+ const ids = options.ids ?? RANDOM_IDS
213
+ const prefix = options.keyPrefix ?? 'a2'
214
+
215
+ if (!options.url && !options.clientOptions) {
216
+ throw new TypeError('valkey() needs a url or clientOptions')
217
+ }
218
+ const clientOptions = {
219
+ ...options.clientOptions,
220
+ ...(options.url ? { url: options.url } : {}),
221
+ RESP: 2 as const,
222
+ }
223
+
224
+ // Lazy init — constructing the backend does no I/O.
225
+ const driver = retryableLazy(async (): Promise<Driver> => {
226
+ const mod = await import('redis').catch(() => {
227
+ throw new A2Error(
228
+ 'STORE_NOT_CONFIGURED',
229
+ "experimental-a2/store-valkey needs the 'redis' package (optional peer dependency) — install it",
230
+ )
231
+ })
232
+ return {
233
+ create: () => mod.createClient(clientOptions) as unknown as Client,
234
+ bytes: { typeMapping: { [mod.RESP_TYPES.BLOB_STRING]: Buffer } },
235
+ }
236
+ })
237
+
238
+ const timeoutMs = clientOptions.commandOptions?.timeout ?? DEFAULT_TIMEOUT_MS
239
+ const commands = ownedClient({
240
+ driver: driver.get,
241
+ opened: () => {},
242
+ timeoutMs,
243
+ })
244
+
245
+ /**
246
+ * Channels the subscriber holds, restored on a replacement client. Each
247
+ * maps to the call that subscribed it, so a failed call only forgets its
248
+ * own subscription.
249
+ */
250
+ const channels = new Map<string, symbol>()
251
+ let deliver: ((channel: string, message: string) => void) | undefined
252
+ let reconnected: ((channel: string) => void) | undefined
253
+ const onMessage = (message: string, channel: string): void => {
254
+ deliver?.(channel, message)
255
+ }
256
+ const resubscribed = (): void => {
257
+ for (const channel of channels.keys()) reconnected?.(channel)
258
+ }
259
+ /**
260
+ * node-redis queues SUBSCRIBE outside `sendCommand`, so neither
261
+ * `disableOfflineQueue` nor `commandOptions.timeout` applies to it: a
262
+ * subscribe sent while the subscriber reconnects would wait for the
263
+ * server indefinitely. Apply both here. A timed-out SUBSCRIBE stays
264
+ * queued, so an UNSUBSCRIBE queued behind it cancels it.
265
+ */
266
+ const subscribeBounded = async (
267
+ client: Client,
268
+ channel: string,
269
+ ): Promise<unknown> => {
270
+ if (!client.isReady && clientOptions.disableOfflineQueue) {
271
+ throw new A2Error('STORE_UNAVAILABLE', 'the subscriber is offline')
272
+ }
273
+ let timer: ReturnType<typeof setTimeout> | undefined
274
+ const timedOut = new Promise<never>((_, reject) => {
275
+ timer = setTimeout(() => {
276
+ void client.unsubscribe(channel).catch(() => {})
277
+ reject(
278
+ new A2Error(
279
+ 'STORE_UNAVAILABLE',
280
+ `subscribe timed out after ${timeoutMs}ms`,
281
+ ),
282
+ )
283
+ }, timeoutMs)
284
+ })
285
+ try {
286
+ return await Promise.race([
287
+ client.subscribe(channel, onMessage),
288
+ timedOut,
289
+ ])
290
+ } finally {
291
+ clearTimeout(timer)
292
+ }
293
+ }
294
+ const subscriber = ownedClient({
295
+ driver: driver.get,
296
+ opened: (client) => {
297
+ // node-redis restores subscriptions before a reconnect's 'ready'.
298
+ let readies = 0
299
+ client.on('ready', () => {
300
+ readies += 1
301
+ if (readies > 1) resubscribed()
302
+ })
303
+ if (channels.size === 0) return
304
+ void Promise.all(
305
+ [...channels.keys()].map((channel) =>
306
+ client.subscribe(channel, onMessage),
307
+ ),
308
+ ).then(resubscribed, () => {})
309
+ },
310
+ timeoutMs,
311
+ })
312
+
313
+ const subscriberConnection: RedisConnection = {
314
+ async call(command, ...args) {
315
+ const channel = String(args[0])
316
+ const name = command.toLowerCase()
317
+ if (name === 'subscribe') {
318
+ const call = Symbol(channel)
319
+ try {
320
+ // Restore only held channels while a replacement connects; a new
321
+ // channel belongs to the bounded subscribe after first readiness.
322
+ const client = await subscriber.get()
323
+ channels.set(channel, call)
324
+ return await subscribeBounded(client, channel)
325
+ } catch (err) {
326
+ if (channels.get(channel) === call) channels.delete(channel)
327
+ throw err
328
+ }
329
+ }
330
+ if (name === 'unsubscribe') {
331
+ channels.delete(channel)
332
+ return (await subscriber.get()).unsubscribe(channel)
333
+ }
334
+ return (await subscriber.get()).sendCommand(
335
+ [command, ...args.map(String)],
336
+ STRING_REPLIES,
337
+ )
338
+ },
339
+ duplicate: () => subscriberConnection,
340
+ on(_event, listener) {
341
+ deliver = listener
342
+ },
343
+ disconnect: subscriber.close,
344
+ }
345
+
346
+ const commandConnection: RedisConnection = {
347
+ call: async (command, ...args) =>
348
+ (await commands.get()).sendCommand(
349
+ [command, ...args.map(String)],
350
+ STRING_REPLIES,
351
+ ),
352
+ duplicate: () => subscriberConnection,
353
+ on: () => undefined,
354
+ disconnect: commands.close,
355
+ }
356
+
357
+ return createRedisNotifyStore({
358
+ client: () => Promise.resolve(commandConnection),
359
+ callBytes: async (command, ...args) => {
360
+ const { bytes } = await driver.get()
361
+ return (await commands.get()).sendCommand(
362
+ [command, ...args.map(String)],
363
+ bytes,
364
+ )
365
+ },
366
+ clock,
367
+ ids,
368
+ keyPrefix: prefix,
369
+ onReconnect: (listener) => {
370
+ reconnected = listener
371
+ },
372
+ async closeClient() {
373
+ commands.close()
374
+ },
375
+ })
376
+ }
package/src/store.ts CHANGED
@@ -326,12 +326,22 @@ export interface A2Store {
326
326
  * a `cause` at or below the latest clear marker throws
327
327
  * `SUPERSEDED_ATTEMPT`, an `originIndex` there throws `SESSION_CLEARED`,
328
328
  * and ids whose rows lie below the marker are free to reuse.
329
+ *
330
+ * `ifLastIndex` conditions a non-empty batch on the session's head, the
331
+ * last index including any clear marker: when the head is anything else,
332
+ * throw `lastIndexMismatchError` carrying the head and write nothing. It is
333
+ * checked last, after the replay path and both fences, inside the same
334
+ * atomic operation, never as a read before it. A conditional batch whose
335
+ * ids conflict (mixed, or not matching their rows) reports that head
336
+ * instead of `PARTIAL_DUPLICATE_BATCH` when it isn't `ifLastIndex`
337
+ * (`duplicateBatchError`).
329
338
  */
330
339
  append(
331
340
  sessionId: string,
332
341
  events: AppendEvent[],
333
342
  options?: {
334
343
  originIndex?: number
344
+ ifLastIndex?: number
335
345
  consumers?: readonly StoreConsumer[]
336
346
  },
337
347
  ): Promise<StoreAppendResult>
package/src/wire.ts CHANGED
@@ -309,6 +309,7 @@ const ERROR_STATUS: Record<A2ServerErrorCode, number> = {
309
309
  PARTIAL_DUPLICATE_BATCH: 400,
310
310
  SUPERSEDED_ATTEMPT: 409,
311
311
  SESSION_CLEARED: 409,
312
+ LAST_INDEX_MISMATCH: 409,
312
313
  CLAIM_EXPIRED: 409,
313
314
  STORE_UNAVAILABLE: 503,
314
315
  STORE_NOT_CONFIGURED: 500,
@@ -1 +0,0 @@
1
- {"version":3,"file":"initial-snapshot-D3Lve43o.d.ts","names":[],"sources":["../src/errors.ts","../src/initial-snapshot.ts"],"mappings":";;KAAY;KAcA,cACV;cAEW,gBAAgB;WAClB,MAAM;;WAEN;EAET,YACE,MAAM,aACN,iBACA;IAAY;IAAmB;;;;;;;;;;;cA6CtB,0BAA0B;EACrC,YAAY,iBAAiB;IAAY;;;;;KCrE/B,gBAAgB,KACxB,cAAc,wBAGd,QAAQ,cAAc"}