experimental-a2 0.21.1 → 0.23.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.
- package/CHANGELOG.md +32 -0
- package/dist/{actor-HnGmrOZZ.d.ts → actor-CqibLgjQ.d.ts} +13 -5
- package/dist/actor-CqibLgjQ.d.ts.map +1 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/actor-react.js +1 -1
- package/dist/actor-shared-DFET5q16.js.map +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +3 -3
- package/dist/{ai-CvEnqVfo.d.ts → ai-DAu77CDr.d.ts} +2 -2
- package/dist/{ai-CvEnqVfo.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
- package/dist/{ai-CzE672Q3.js → ai-EEfKyc-z.js} +2 -2
- package/dist/{ai-CzE672Q3.js.map → ai-EEfKyc-z.js.map} +1 -1
- package/dist/ai-client.d.ts +1 -1
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +4 -2
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +234 -62
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-Bfk-Lycn.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
- package/dist/{ai-harness-Bfk-Lycn.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
- package/dist/ai-harness-dQeJaR1t.js.map +1 -1
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +3 -3
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +10 -4
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/{client-9IeTsiAs.js → client-BQS4i_V9.js} +27 -4
- package/dist/client-BQS4i_V9.js.map +1 -0
- package/dist/client-CUMbS_38.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/{internal-Bc4PvfHk.js → internal-CvsrprnU.js} +32 -15
- package/dist/{internal-Bc4PvfHk.js.map → internal-CvsrprnU.js.map} +1 -1
- package/dist/react.js +1 -1
- package/dist/{runtime-consumer-BWmht60z.js → runtime-consumer-CIeL33wj.js} +2 -2
- package/dist/{runtime-consumer-BWmht60z.js.map → runtime-consumer-CIeL33wj.js.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +2 -2
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +2 -2
- package/dist/{server-DvIB8kj5.js → server-DtFrWxi3.js} +30 -8
- package/dist/server-DtFrWxi3.js.map +1 -0
- package/dist/{server-BshFZ0eL.d.ts → server-qgez0Xny.d.ts} +3 -3
- package/dist/{server-BshFZ0eL.d.ts.map → server-qgez0Xny.d.ts.map} +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{session-clear-Bk_29eEw.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
- package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
- package/dist/{store-Z2qjw50n.d.ts → store-D0MhBvg6.d.ts} +15 -6
- package/dist/store-D0MhBvg6.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +15 -17
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-DYNyxCev.js → store-polling-BZt2Rf3D.js} +2 -2
- package/dist/{store-polling-DYNyxCev.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +31 -40
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
- package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-IiVdlYHc.js → store-redis-notify-CBDVJxA_.js} +545 -399
- package/dist/store-redis-notify-CBDVJxA_.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +3 -4
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +16 -18
- package/dist/store-sqlite.js.map +1 -1
- package/dist/store-valkey.d.ts +33 -0
- package/dist/store-valkey.d.ts.map +1 -0
- package/dist/store-valkey.js +218 -0
- package/dist/store-valkey.js.map +1 -0
- package/dist/stream-buffer-DrTyiIuo.js +57 -0
- package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
- package/dist/{wire-CZGqNZRx.js → wire-CV-Z7wz-.js} +3 -3
- package/dist/{wire-CZGqNZRx.js.map → wire-CV-Z7wz-.js.map} +1 -1
- package/docs/actors/01-introduction.mdx +5 -2
- package/docs/agents/03-conversations.mdx +9 -8
- package/docs/agents/04-harnesses.mdx +7 -4
- package/docs/client/03-presence.mdx +1 -0
- package/docs/reference/01-core.mdx +4 -2
- package/docs/reference/02-server.mdx +5 -1
- package/docs/reference/04-ai.mdx +13 -4
- package/docs/reference/06-errors.mdx +1 -1
- package/docs/server/01-consumers.mdx +4 -1
- package/docs/server/06-guarantees.mdx +7 -4
- package/docs/stores/01-overview.mdx +1 -0
- package/docs/stores/02-redis.mdx +18 -10
- package/docs/stores/03-redis-http.mdx +6 -4
- package/docs/stores/04-postgres.mdx +2 -2
- package/docs/stores/07-valkey.mdx +97 -0
- package/docs/upgrades/01-overview.mdx +1 -0
- package/docs/upgrades/78-0-22.mdx +115 -0
- package/examples/playground/app/a0/sandbox.ts +58 -13
- package/examples/playground/app/a2slack/db/a2.ts +7 -4
- package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +8 -1
- package/examples/playground/app/chat/server.ts +2 -2
- package/examples/playground/lib/store.ts +1 -1
- package/examples/playground/package.json +1 -1
- package/package.json +8 -2
- package/sql/postgres/drizzle-schema.ts +7 -4
- package/sql/postgres/schema.sql +24 -3
- package/src/actor-shared.ts +10 -2
- package/src/ai-agent.ts +13 -0
- package/src/ai-fx.ts +365 -78
- package/src/ai-harness.ts +7 -0
- package/src/client.ts +19 -2
- package/src/deterministic-id.ts +1 -1
- package/src/internal.ts +42 -11
- package/src/libfx.d.ts +2 -0
- package/src/presence-recovery.ts +18 -11
- package/src/presence-tombstones.ts +52 -0
- package/src/push-envelope.ts +2 -2
- package/src/server.ts +49 -15
- package/src/store-memory.ts +24 -32
- package/src/store-postgres.ts +43 -53
- package/src/store-presence-polling.ts +59 -34
- package/src/store-redis-core.ts +595 -439
- package/src/store-redis-notify.ts +82 -28
- package/src/store-redis.ts +2 -3
- package/src/store-sqlite.ts +34 -40
- package/src/store-valkey.ts +376 -0
- package/src/store.ts +14 -5
- package/dist/actor-HnGmrOZZ.d.ts.map +0 -1
- package/dist/client-9IeTsiAs.js.map +0 -1
- package/dist/server-DvIB8kj5.js.map +0 -1
- package/dist/session-clear-Bk_29eEw.d.ts.map +0 -1
- package/dist/store-Z2qjw50n.d.ts.map +0 -1
- package/dist/store-presence-polling-CzQ0wdtM.js +0 -94
- package/dist/store-presence-polling-CzQ0wdtM.js.map +0 -1
- package/dist/store-redis-notify-IiVdlYHc.js.map +0 -1
- package/dist/stream-buffer-Cx7AAPGq.js +0 -23
- package/dist/stream-buffer-Cx7AAPGq.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
|
@@ -300,6 +300,8 @@ export interface A2Store {
|
|
|
300
300
|
* lost — return the existing rows as success. If only *some* ids
|
|
301
301
|
* exist, the caller mixed an already-sent batch with fresh events —
|
|
302
302
|
* always a caller bug — so throw `A2Error('PARTIAL_DUPLICATE_BATCH')`.
|
|
303
|
+
* Ids are unique per session: the same id in another session names a
|
|
304
|
+
* different event and plays no part in these checks.
|
|
303
305
|
*
|
|
304
306
|
* In the same atomic operation, each event's `targets` start cursor work:
|
|
305
307
|
* a lane target queues the event in that consumer's lane, a null lane
|
|
@@ -538,11 +540,15 @@ export interface A2Store {
|
|
|
538
540
|
presence?: {
|
|
539
541
|
/**
|
|
540
542
|
* Field-wise last-writer-wins merge of one participant's values:
|
|
541
|
-
* a field whose
|
|
542
|
-
* sender's stamp) is left untouched
|
|
543
|
-
*
|
|
543
|
+
* a field whose unexpired entry has a strictly newer `at` (the
|
|
544
|
+
* sender's stamp) is left untouched. `null` deletes the value but
|
|
545
|
+
* keeps a tombstone with its `at`, which takes part in the merge
|
|
546
|
+
* like a value until it expires, so a set older than the delete
|
|
547
|
+
* still loses when it arrives later. Every applied write, value or
|
|
548
|
+
* tombstone, refreshes that field's expiry to the storage's own
|
|
544
549
|
* clock plus `ttlMs` — the sender stamp orders writes but never
|
|
545
|
-
* anchors their lifetime.
|
|
550
|
+
* anchors their lifetime. `subscribe` delivers applied writes only,
|
|
551
|
+
* a delete as `null`.
|
|
546
552
|
*/
|
|
547
553
|
set(
|
|
548
554
|
ns: string,
|
|
@@ -551,7 +557,10 @@ export interface A2Store {
|
|
|
551
557
|
meta: { seen: number; at: Date; ttlMs: number },
|
|
552
558
|
): Promise<void>
|
|
553
559
|
|
|
554
|
-
/**
|
|
560
|
+
/**
|
|
561
|
+
* The current map, pruned of rows at or past their `expiresAt`.
|
|
562
|
+
* Tombstones never appear.
|
|
563
|
+
*/
|
|
555
564
|
read(ns: string): Promise<PresenceRow[]>
|
|
556
565
|
|
|
557
566
|
/**
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"actor-HnGmrOZZ.d.ts","names":[],"sources":["../src/actor-shared.ts","../src/actor.ts"],"mappings":";;;;;;;;;;;cAiCa,0BAA0B;;WAE5B;;WAEA;EACT,YAAY,eAAe,mBAAmB;;;;;;;;;KAepC;EACV;EACA;;;;;;;EAOA;;;KAIU,gBAAgB,UAAU,iBAAiB;EACrD,gBAAgB;IAEd;;KAIQ,oBAAoB,QAC7B,WAAW,cAAc,EAAE;;;;;;KAQlB,iBAAiB;GAExB,yBAAA,WAAW;IAAgB,OAAO,EAAE;IAAI;IAAc,IAAI;;;;;;;;;;;;KAanD,UAAU,iBACV,WAAW,eAAe,EAAE,MACjC,QAAQ,EAAE,eACV,OAAO,EAAE;;;;;;;;;KAWJ,uBAAuB;EAAmB;;;;;;;;;;;;KAY1C,kBAAkB,iBAClB,WAAW,eAAe,EAAE,MACjC,OAAO,EAAE,gBAAgB,SAAS,yBAAyB,iBAC3D,OAAO,EAAE,IAAI,SAAS,yBAAyB;;KAI1C,aAAa,UAAU;;EAEjC,OAAO;;EAEP;;EAEA;;EAEA,QAAQ;;EAER,MAAM,UAAU;;EAEhB,UAAU,kBAAkB;;;;;;;;KASlB,uBAAuB,UAAU;;EAE3C;;EAEA;;EAEA,QAAQ;;EAER,SAAS;IAAU,OAAO;IAAY;;;EAEtC,MAAM,UAAU;;EAEhB,UAAU,kBAAkB;;KAGlB,kBAAkB;EAC5B,OAAO;;EAEP;;EAEA;;EAEA;;;KAWU,qBAAqB;EAC/B,kBAAkB,iBAAiB,kBAAkB;;;;;;;;;;;;;;KClE3C,aAAa,UAAU,eAAe,gBAAgB,iBAC5D,KAAK,aAAa,IAAI,OAAO,YAAY,cAAc;EAEvD;EACA,SACE,KAAK,uBAAuB,IAC5B,OAAO,YAAY,cACT;;;;;;;;;;;;;;;;iBAiBF,WAAW,KAAK,GAC9B,SAAS,KAAK,KAAK,OAAO,aAAa;EACpC;EAAkB,SAAS,KAAK,KAAK,OAAO,aAAa;;KAIlD,aAAa,UAAU;;EAEjC;;EAEA,OAAO;;;;;EAKP,aAAa,WAAW,cAAc,aAAa,GAAG;EACtD,QAAQ;EACR,YAAY;EACZ,YAAY;MACR,gBAAgB;EAChB;;;;;;;;;;;;EAYA;;;;;;;KAQM;WACG;WAAwB;;WAExB;WACA;WACA;;WAGA;WACA;WACA;WACA;WACA;;WAGA;WACA;WACA;WACA,QAAQ,SAAS;;KAGpB,kBAAkB,UAAU;;EAEtC,aAAa,WAAW,6BAA6B;;;;;;;;;;;;EAYrD,QAAQ,OAAO;;KAGL;;EAEV;;KAGU,mBAAmB;;EAE7B;;KAGU,gBAAgB;;EAE1B,OAAO;;EAEP;;;;;EAKA;;KAGG,WACH,UAAU,eACV,gBAAgB,0BACH,YAAY,MAErB,QAAQ,YAAY,IACpB,UAAU,qBACP,QAAQ,gBAAgB,gBAE3B,OAAO,YAAY,IACnB,UAAU,qBACP,QAAQ,gBAAgB;KAE5B,WACH,UAAU,eACV,gBAAgB,0BACH,YAAY,MAErB,QAAQ,YAAY,IACpB,UAAU,qBACP;EAAU;EAAY;MAEzB,OAAO,YAAY,IACnB,UAAU,qBACP;EAAU;EAAY;;;KAGnB,YAAY,UAAU;WACvB;;;;;;;;WAQA,kBAAkB,WAAW,cAAc,WAAW,GAAG;;WAEzD,kBAAkB,WAAW,cAAc,WAAW,GAAG;;WAEzD,aAAa,QAAQ,gBAAgB;;;;;;;;;;;WAWrC,QACP,SAAS,SACT,UAAU,kBAAkB,OACzB,QAAQ;;WAEJ,SAAS,QAAQ;;KAGhB,gBAAgB,UAAU;WAC3B;;;;;;WAMA,WAAW;;WAEX,UAAU,SAAS;;WAEnB,QAAQ,SAAS;;WAEjB,SAAS,QAAQ,qBAAqB,aAAa;EAC5D,MAAM,aAAa,YAAY;;;;;;;iBAsDjB,MAAM,UAAU,eAC9B,SAAS,aAAa,KACrB,gBAAgB"}
|