@usearete/sdk 0.1.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/dist/index.js ADDED
@@ -0,0 +1,3074 @@
1
+ 'use strict';
2
+
3
+ var pako = require('pako');
4
+
5
+ const DEFAULT_MAX_ENTRIES_PER_VIEW = 10000;
6
+ const DEFAULT_CONFIG = {
7
+ reconnectIntervals: [1000, 2000, 4000, 8000, 16000],
8
+ maxReconnectAttempts: 5,
9
+ maxEntriesPerView: DEFAULT_MAX_ENTRIES_PER_VIEW,
10
+ };
11
+ /**
12
+ * Determines if the error indicates the client should fetch a new token
13
+ */
14
+ function shouldRefreshToken(code) {
15
+ return [
16
+ 'TOKEN_EXPIRED',
17
+ 'TOKEN_INVALID_SIGNATURE',
18
+ 'TOKEN_INVALID_FORMAT',
19
+ 'TOKEN_INVALID_ISSUER',
20
+ 'TOKEN_INVALID_AUDIENCE',
21
+ 'TOKEN_KEY_NOT_FOUND',
22
+ ].includes(code);
23
+ }
24
+ class AreteError extends Error {
25
+ constructor(message, code, details) {
26
+ super(message);
27
+ this.code = code;
28
+ this.details = details;
29
+ this.name = 'AreteError';
30
+ }
31
+ }
32
+ /**
33
+ * Parse a kebab-case error code string (from X-Error-Code header) to AuthErrorCode
34
+ */
35
+ function parseErrorCode(errorCode) {
36
+ const codeMap = {
37
+ 'token-missing': 'TOKEN_MISSING',
38
+ 'token-expired': 'TOKEN_EXPIRED',
39
+ 'token-invalid-signature': 'TOKEN_INVALID_SIGNATURE',
40
+ 'token-invalid-format': 'TOKEN_INVALID_FORMAT',
41
+ 'token-invalid-issuer': 'TOKEN_INVALID_ISSUER',
42
+ 'token-invalid-audience': 'TOKEN_INVALID_AUDIENCE',
43
+ 'token-missing-claim': 'TOKEN_MISSING_CLAIM',
44
+ 'token-key-not-found': 'TOKEN_KEY_NOT_FOUND',
45
+ 'origin-mismatch': 'ORIGIN_MISMATCH',
46
+ 'origin-required': 'ORIGIN_REQUIRED',
47
+ 'origin-not-allowed': 'ORIGIN_NOT_ALLOWED',
48
+ 'rate-limit-exceeded': 'RATE_LIMIT_EXCEEDED',
49
+ 'websocket-session-rate-limit-exceeded': 'WEBSOCKET_SESSION_RATE_LIMIT_EXCEEDED',
50
+ 'connection-limit-exceeded': 'CONNECTION_LIMIT_EXCEEDED',
51
+ 'subscription-limit-exceeded': 'SUBSCRIPTION_LIMIT_EXCEEDED',
52
+ 'snapshot-limit-exceeded': 'SNAPSHOT_LIMIT_EXCEEDED',
53
+ 'egress-limit-exceeded': 'EGRESS_LIMIT_EXCEEDED',
54
+ 'invalid-static-token': 'INVALID_STATIC_TOKEN',
55
+ 'internal-error': 'INTERNAL_ERROR',
56
+ 'auth-required': 'AUTH_REQUIRED',
57
+ 'missing-authorization-header': 'MISSING_AUTHORIZATION_HEADER',
58
+ 'invalid-authorization-format': 'INVALID_AUTHORIZATION_FORMAT',
59
+ 'invalid-api-key': 'INVALID_API_KEY',
60
+ 'expired-api-key': 'EXPIRED_API_KEY',
61
+ 'user-not-found': 'USER_NOT_FOUND',
62
+ 'secret-key-required': 'SECRET_KEY_REQUIRED',
63
+ 'deployment-access-denied': 'DEPLOYMENT_ACCESS_DENIED',
64
+ 'quota-exceeded': 'QUOTA_EXCEEDED',
65
+ };
66
+ return codeMap[errorCode.toLowerCase()] || 'INTERNAL_ERROR';
67
+ }
68
+
69
+ const GZIP_MAGIC_0 = 0x1f;
70
+ const GZIP_MAGIC_1 = 0x8b;
71
+ function isGzipData(data) {
72
+ return data.length >= 2 && data[0] === GZIP_MAGIC_0 && data[1] === GZIP_MAGIC_1;
73
+ }
74
+ function isSnapshotFrame(frame) {
75
+ return frame.op === 'snapshot';
76
+ }
77
+ function isSubscribedFrame(frame) {
78
+ return frame.op === 'subscribed';
79
+ }
80
+ function isEntityFrame(frame) {
81
+ return ['create', 'upsert', 'patch', 'delete'].includes(frame.op);
82
+ }
83
+ function parseFrame(data) {
84
+ if (typeof data === 'string') {
85
+ return JSON.parse(data);
86
+ }
87
+ const bytes = new Uint8Array(data);
88
+ if (isGzipData(bytes)) {
89
+ const decompressed = pako.inflate(bytes);
90
+ const jsonString = new TextDecoder().decode(decompressed);
91
+ return JSON.parse(jsonString);
92
+ }
93
+ const jsonString = new TextDecoder('utf-8').decode(data);
94
+ return JSON.parse(jsonString);
95
+ }
96
+ async function parseFrameFromBlob(blob) {
97
+ const arrayBuffer = await blob.arrayBuffer();
98
+ return parseFrame(arrayBuffer);
99
+ }
100
+ function isValidFrame(frame) {
101
+ if (typeof frame !== 'object' || frame === null) {
102
+ return false;
103
+ }
104
+ const f = frame;
105
+ if (typeof f['entity'] !== 'string' ||
106
+ typeof f['op'] !== 'string' ||
107
+ typeof f['mode'] !== 'string' ||
108
+ !['state', 'append', 'list'].includes(f['mode'])) {
109
+ return false;
110
+ }
111
+ if (f['op'] === 'snapshot') {
112
+ return Array.isArray(f['data']);
113
+ }
114
+ return (typeof f['key'] === 'string' &&
115
+ ['create', 'upsert', 'patch', 'delete'].includes(f['op']));
116
+ }
117
+
118
+ const TOKEN_REFRESH_BUFFER_SECONDS = 60;
119
+ const MIN_REFRESH_DELAY_MS = 1000;
120
+ const DEFAULT_QUERY_PARAMETER = 'hs_token';
121
+ const DEFAULT_HOSTED_TOKEN_ENDPOINT = 'https://api.arete.run/ws/sessions';
122
+ const HOSTED_WEBSOCKET_SUFFIX = '.stack.arete.run';
123
+ function normalizeTokenResult(result) {
124
+ if (typeof result === 'string') {
125
+ return { token: result };
126
+ }
127
+ return result;
128
+ }
129
+ function decodeBase64Url(value) {
130
+ const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
131
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
132
+ if (typeof atob === 'function') {
133
+ return atob(padded);
134
+ }
135
+ const bufferCtor = globalThis.Buffer;
136
+ if (bufferCtor) {
137
+ return bufferCtor.from(padded, 'base64').toString('utf-8');
138
+ }
139
+ return undefined;
140
+ }
141
+ function parseJwtExpiry(token) {
142
+ const parts = token.split('.');
143
+ if (parts.length !== 3) {
144
+ return undefined;
145
+ }
146
+ const payload = decodeBase64Url(parts[1] ?? '');
147
+ if (!payload) {
148
+ return undefined;
149
+ }
150
+ try {
151
+ const decoded = JSON.parse(payload);
152
+ return typeof decoded.exp === 'number' ? decoded.exp : undefined;
153
+ }
154
+ catch {
155
+ return undefined;
156
+ }
157
+ }
158
+ function normalizeExpiryTimestamp(expiresAt, expires_at) {
159
+ return expiresAt ?? expires_at;
160
+ }
161
+ function isRefreshAuthResponseMessage(value) {
162
+ if (typeof value !== 'object' || value === null) {
163
+ return false;
164
+ }
165
+ const candidate = value;
166
+ return typeof candidate['success'] === 'boolean'
167
+ && !('op' in candidate)
168
+ && !('entity' in candidate)
169
+ && !('mode' in candidate);
170
+ }
171
+ function isSocketIssueMessage(value) {
172
+ if (typeof value !== 'object' || value === null) {
173
+ return false;
174
+ }
175
+ const candidate = value;
176
+ return candidate['type'] === 'error'
177
+ && typeof candidate['message'] === 'string'
178
+ && typeof candidate['code'] === 'string'
179
+ && typeof candidate['retryable'] === 'boolean'
180
+ && typeof candidate['fatal'] === 'boolean';
181
+ }
182
+ function isHostedAreteWebsocketUrl(websocketUrl) {
183
+ try {
184
+ return new URL(websocketUrl).hostname.toLowerCase().endsWith(HOSTED_WEBSOCKET_SUFFIX);
185
+ }
186
+ catch {
187
+ return false;
188
+ }
189
+ }
190
+ class ConnectionManager {
191
+ constructor(config) {
192
+ this.ws = null;
193
+ this.reconnectAttempts = 0;
194
+ this.reconnectTimeout = null;
195
+ this.pingInterval = null;
196
+ this.tokenRefreshTimeout = null;
197
+ this.tokenRefreshInFlight = null;
198
+ this.currentState = 'disconnected';
199
+ this.subscriptionQueue = [];
200
+ this.activeSubscriptions = new Set();
201
+ this.frameHandlers = new Set();
202
+ this.stateHandlers = new Set();
203
+ this.socketIssueHandlers = new Set();
204
+ this.reconnectForTokenRefresh = false;
205
+ if (!config.websocketUrl) {
206
+ throw new AreteError('websocketUrl is required', 'INVALID_CONFIG');
207
+ }
208
+ this.websocketUrl = config.websocketUrl;
209
+ this.hostedAreteUrl = isHostedAreteWebsocketUrl(config.websocketUrl);
210
+ this.reconnectIntervals = config.reconnectIntervals ?? DEFAULT_CONFIG.reconnectIntervals;
211
+ this.maxReconnectAttempts =
212
+ config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts;
213
+ this.authConfig = config.auth;
214
+ if (config.initialSubscriptions) {
215
+ this.subscriptionQueue.push(...config.initialSubscriptions);
216
+ }
217
+ }
218
+ getTokenEndpoint() {
219
+ if (this.authConfig?.tokenEndpoint) {
220
+ return this.authConfig.tokenEndpoint;
221
+ }
222
+ // Require publishableKey for hosted token endpoint
223
+ if (this.hostedAreteUrl && this.authConfig?.publishableKey) {
224
+ return DEFAULT_HOSTED_TOKEN_ENDPOINT;
225
+ }
226
+ return undefined;
227
+ }
228
+ getAuthStrategy() {
229
+ if (this.authConfig?.token) {
230
+ return { kind: 'static-token', token: this.authConfig.token };
231
+ }
232
+ if (this.authConfig?.getToken) {
233
+ return { kind: 'token-provider', getToken: this.authConfig.getToken };
234
+ }
235
+ const tokenEndpoint = this.getTokenEndpoint();
236
+ if (tokenEndpoint) {
237
+ return { kind: 'token-endpoint', endpoint: tokenEndpoint };
238
+ }
239
+ return { kind: 'none' };
240
+ }
241
+ hasRefreshableAuth() {
242
+ const strategy = this.getAuthStrategy();
243
+ return strategy.kind === 'token-provider' || strategy.kind === 'token-endpoint';
244
+ }
245
+ updateTokenState(result) {
246
+ const normalized = normalizeTokenResult(result);
247
+ if (!normalized.token) {
248
+ throw new AreteError('Authentication provider returned an empty token', 'TOKEN_INVALID');
249
+ }
250
+ this.currentToken = normalized.token;
251
+ this.tokenExpiry = normalizeExpiryTimestamp(normalized.expiresAt, normalized.expires_at)
252
+ ?? parseJwtExpiry(normalized.token);
253
+ if (this.isTokenExpired()) {
254
+ throw new AreteError('Authentication token is expired', 'TOKEN_EXPIRED');
255
+ }
256
+ return normalized.token;
257
+ }
258
+ clearTokenState() {
259
+ this.currentToken = undefined;
260
+ this.tokenExpiry = undefined;
261
+ }
262
+ async getOrRefreshToken(forceRefresh = false) {
263
+ if (!forceRefresh && this.currentToken && !this.isTokenExpired()) {
264
+ return this.currentToken;
265
+ }
266
+ const strategy = this.getAuthStrategy();
267
+ // For hosted Arete URLs, auth is required - fail early with clear message
268
+ if (strategy.kind === 'none' && this.hostedAreteUrl) {
269
+ throw new AreteError('Arete authentication required. Please provide auth.publishableKey to AreteProvider. ' +
270
+ 'Get your key from https://arete.run/dashboard', 'AUTH_REQUIRED');
271
+ }
272
+ switch (strategy.kind) {
273
+ case 'static-token':
274
+ return this.updateTokenState(strategy.token);
275
+ case 'token-provider':
276
+ try {
277
+ return this.updateTokenState(await strategy.getToken());
278
+ }
279
+ catch (error) {
280
+ if (error instanceof AreteError) {
281
+ throw error;
282
+ }
283
+ throw new AreteError('Failed to get authentication token', 'AUTH_REQUIRED', error);
284
+ }
285
+ case 'token-endpoint':
286
+ try {
287
+ return this.updateTokenState(await this.fetchTokenFromEndpoint(strategy.endpoint));
288
+ }
289
+ catch (error) {
290
+ if (error instanceof AreteError) {
291
+ throw error;
292
+ }
293
+ throw new AreteError('Failed to fetch authentication token from endpoint', 'AUTH_REQUIRED', error);
294
+ }
295
+ case 'none':
296
+ return undefined;
297
+ }
298
+ }
299
+ createTokenEndpointRequestBody() {
300
+ return {
301
+ websocket_url: this.websocketUrl,
302
+ };
303
+ }
304
+ async fetchTokenFromEndpoint(tokenEndpoint) {
305
+ const response = await fetch(tokenEndpoint, {
306
+ method: 'POST',
307
+ headers: {
308
+ ...(this.authConfig?.publishableKey
309
+ ? { Authorization: `Bearer ${this.authConfig.publishableKey}` }
310
+ : {}),
311
+ ...(this.authConfig?.tokenEndpointHeaders ?? {}),
312
+ 'Content-Type': 'application/json',
313
+ },
314
+ credentials: this.authConfig?.tokenEndpointCredentials,
315
+ body: JSON.stringify(this.createTokenEndpointRequestBody()),
316
+ });
317
+ if (!response.ok) {
318
+ const rawError = await response.text();
319
+ let parsedError;
320
+ if (rawError) {
321
+ try {
322
+ parsedError = JSON.parse(rawError);
323
+ }
324
+ catch {
325
+ parsedError = undefined;
326
+ }
327
+ }
328
+ const wireErrorCode = response.headers.get('X-Error-Code')
329
+ ?? (typeof parsedError?.code === 'string' ? parsedError.code : null);
330
+ const errorCode = wireErrorCode
331
+ ? parseErrorCode(wireErrorCode)
332
+ : response.status === 429
333
+ ? 'QUOTA_EXCEEDED'
334
+ : 'AUTH_REQUIRED';
335
+ const errorMessage = typeof parsedError?.error === 'string' && parsedError.error.length > 0
336
+ ? parsedError.error
337
+ : rawError || response.statusText || 'Authentication request failed';
338
+ throw new AreteError(`Token endpoint returned ${response.status}: ${errorMessage}`, errorCode, {
339
+ status: response.status,
340
+ wireErrorCode,
341
+ responseBody: rawError || null,
342
+ });
343
+ }
344
+ const data = (await response.json());
345
+ if (!data.token) {
346
+ throw new AreteError('Token endpoint did not return a token', 'TOKEN_INVALID');
347
+ }
348
+ return data;
349
+ }
350
+ isTokenExpired() {
351
+ if (!this.tokenExpiry) {
352
+ return false;
353
+ }
354
+ return Date.now() >= (this.tokenExpiry - TOKEN_REFRESH_BUFFER_SECONDS) * 1000;
355
+ }
356
+ scheduleTokenRefresh() {
357
+ this.clearTokenRefreshTimeout();
358
+ if (!this.hasRefreshableAuth() || !this.tokenExpiry) {
359
+ return;
360
+ }
361
+ const refreshAtMs = Math.max(Date.now() + MIN_REFRESH_DELAY_MS, (this.tokenExpiry - TOKEN_REFRESH_BUFFER_SECONDS) * 1000);
362
+ const delayMs = Math.max(MIN_REFRESH_DELAY_MS, refreshAtMs - Date.now());
363
+ this.tokenRefreshTimeout = setTimeout(() => {
364
+ void this.refreshTokenInBackground();
365
+ }, delayMs);
366
+ }
367
+ clearTokenRefreshTimeout() {
368
+ if (this.tokenRefreshTimeout) {
369
+ clearTimeout(this.tokenRefreshTimeout);
370
+ this.tokenRefreshTimeout = null;
371
+ }
372
+ }
373
+ async refreshTokenInBackground() {
374
+ if (!this.hasRefreshableAuth()) {
375
+ return;
376
+ }
377
+ if (this.tokenRefreshInFlight) {
378
+ return this.tokenRefreshInFlight;
379
+ }
380
+ this.tokenRefreshInFlight = (async () => {
381
+ const previousToken = this.currentToken;
382
+ try {
383
+ await this.getOrRefreshToken(true);
384
+ if (previousToken &&
385
+ this.currentToken &&
386
+ this.currentToken !== previousToken &&
387
+ this.ws?.readyState === WebSocket.OPEN) {
388
+ // Try in-band auth refresh first
389
+ const refreshed = await this.sendInBandAuthRefresh(this.currentToken);
390
+ if (!refreshed) {
391
+ // Fall back to reconnecting if in-band refresh failed
392
+ this.rotateConnectionForTokenRefresh();
393
+ }
394
+ }
395
+ this.scheduleTokenRefresh();
396
+ }
397
+ catch {
398
+ this.scheduleTokenRefresh();
399
+ }
400
+ finally {
401
+ this.tokenRefreshInFlight = null;
402
+ }
403
+ })();
404
+ return this.tokenRefreshInFlight;
405
+ }
406
+ async sendInBandAuthRefresh(token) {
407
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
408
+ return false;
409
+ }
410
+ try {
411
+ const message = JSON.stringify({
412
+ type: 'refresh_auth',
413
+ token: token,
414
+ });
415
+ this.ws.send(message);
416
+ return true;
417
+ }
418
+ catch (error) {
419
+ console.warn('Failed to send in-band auth refresh:', error);
420
+ return false;
421
+ }
422
+ }
423
+ handleRefreshAuthResponse(message) {
424
+ if (message.success) {
425
+ const expiresAt = normalizeExpiryTimestamp(message.expiresAt, message.expires_at);
426
+ if (typeof expiresAt === 'number') {
427
+ this.tokenExpiry = expiresAt;
428
+ }
429
+ this.scheduleTokenRefresh();
430
+ return true;
431
+ }
432
+ const errorCode = message.error ? parseErrorCode(message.error) : 'INTERNAL_ERROR';
433
+ if (shouldRefreshToken(errorCode)) {
434
+ this.clearTokenState();
435
+ }
436
+ this.rotateConnectionForTokenRefresh();
437
+ return true;
438
+ }
439
+ handleSocketIssueMessage(message) {
440
+ this.notifySocketIssue(message);
441
+ if (message.fatal) {
442
+ this.updateState('error', message.message);
443
+ }
444
+ return true;
445
+ }
446
+ rotateConnectionForTokenRefresh() {
447
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || this.reconnectForTokenRefresh) {
448
+ return;
449
+ }
450
+ this.reconnectForTokenRefresh = true;
451
+ this.updateState('reconnecting');
452
+ this.ws.close(1000, 'token refresh');
453
+ }
454
+ buildAuthUrl(token) {
455
+ if (this.authConfig?.tokenTransport === 'bearer') {
456
+ return this.websocketUrl;
457
+ }
458
+ if (!token) {
459
+ return this.websocketUrl;
460
+ }
461
+ const separator = this.websocketUrl.includes('?') ? '&' : '?';
462
+ return `${this.websocketUrl}${separator}${DEFAULT_QUERY_PARAMETER}=${encodeURIComponent(token)}`;
463
+ }
464
+ createWebSocket(url, token) {
465
+ if (this.authConfig?.tokenTransport === 'bearer') {
466
+ const init = token
467
+ ? { headers: { Authorization: `Bearer ${token}` } }
468
+ : undefined;
469
+ if (this.authConfig.websocketFactory) {
470
+ return this.authConfig.websocketFactory(url, init);
471
+ }
472
+ throw new AreteError('auth.tokenTransport="bearer" requires auth.websocketFactory in this environment', 'INVALID_CONFIG');
473
+ }
474
+ if (this.authConfig?.websocketFactory) {
475
+ return this.authConfig.websocketFactory(url);
476
+ }
477
+ return new WebSocket(url);
478
+ }
479
+ getState() {
480
+ return this.currentState;
481
+ }
482
+ onFrame(handler) {
483
+ this.frameHandlers.add(handler);
484
+ return () => {
485
+ this.frameHandlers.delete(handler);
486
+ };
487
+ }
488
+ onStateChange(handler) {
489
+ this.stateHandlers.add(handler);
490
+ return () => {
491
+ this.stateHandlers.delete(handler);
492
+ };
493
+ }
494
+ onSocketIssue(handler) {
495
+ this.socketIssueHandlers.add(handler);
496
+ return () => {
497
+ this.socketIssueHandlers.delete(handler);
498
+ };
499
+ }
500
+ notifySocketIssue(message) {
501
+ const issue = {
502
+ error: message.error,
503
+ message: message.message,
504
+ code: parseErrorCode(message.code),
505
+ retryable: message.retryable,
506
+ retryAfter: message.retry_after,
507
+ suggestedAction: message.suggested_action,
508
+ docsUrl: message.docs_url,
509
+ fatal: message.fatal,
510
+ };
511
+ for (const handler of this.socketIssueHandlers) {
512
+ handler(issue);
513
+ }
514
+ return issue;
515
+ }
516
+ async connect() {
517
+ if (this.ws?.readyState === WebSocket.OPEN ||
518
+ this.ws?.readyState === WebSocket.CONNECTING ||
519
+ this.currentState === 'connecting') {
520
+ return;
521
+ }
522
+ this.updateState('connecting');
523
+ let token;
524
+ try {
525
+ token = await this.getOrRefreshToken();
526
+ }
527
+ catch (error) {
528
+ this.updateState('error', error instanceof Error ? error.message : 'Failed to get token');
529
+ throw error;
530
+ }
531
+ const wsUrl = this.buildAuthUrl(token);
532
+ return new Promise((resolve, reject) => {
533
+ let settled = false;
534
+ const finish = (fn) => {
535
+ if (settled) {
536
+ return;
537
+ }
538
+ settled = true;
539
+ fn();
540
+ };
541
+ try {
542
+ this.ws = this.createWebSocket(wsUrl, token);
543
+ this.ws.onopen = () => {
544
+ this.reconnectAttempts = 0;
545
+ this.updateState('connected');
546
+ this.startPingInterval();
547
+ this.scheduleTokenRefresh();
548
+ this.resubscribeActive();
549
+ this.flushSubscriptionQueue();
550
+ finish(() => resolve());
551
+ };
552
+ this.ws.onmessage = async (event) => {
553
+ try {
554
+ let frame;
555
+ if (event.data instanceof ArrayBuffer) {
556
+ frame = parseFrame(event.data);
557
+ }
558
+ else if (event.data instanceof Blob) {
559
+ frame = await parseFrameFromBlob(event.data);
560
+ }
561
+ else if (typeof event.data === 'string') {
562
+ const parsed = JSON.parse(event.data);
563
+ if (isRefreshAuthResponseMessage(parsed)) {
564
+ this.handleRefreshAuthResponse(parsed);
565
+ return;
566
+ }
567
+ if (isSocketIssueMessage(parsed)) {
568
+ this.handleSocketIssueMessage(parsed);
569
+ return;
570
+ }
571
+ frame = parseFrame(JSON.stringify(parsed));
572
+ }
573
+ else {
574
+ throw new AreteError(`Unsupported message type: ${typeof event.data}`, 'PARSE_ERROR');
575
+ }
576
+ this.notifyFrameHandlers(frame);
577
+ }
578
+ catch {
579
+ this.updateState('error', 'Failed to parse frame from server');
580
+ }
581
+ };
582
+ this.ws.onerror = () => {
583
+ const error = new AreteError('WebSocket connection error', 'CONNECTION_ERROR');
584
+ const wasConnecting = this.currentState === 'connecting';
585
+ this.updateState('error', error.message);
586
+ if (wasConnecting) {
587
+ finish(() => reject(error));
588
+ }
589
+ };
590
+ this.ws.onclose = (event) => {
591
+ this.stopPingInterval();
592
+ this.clearTokenRefreshTimeout();
593
+ this.ws = null;
594
+ if (!settled) {
595
+ const detail = event.reason
596
+ ? `${event.code}: ${event.reason}`
597
+ : `code ${event.code}`;
598
+ const errorMessage = `WebSocket closed before open (${detail})`;
599
+ this.updateState('error', errorMessage);
600
+ finish(() => reject(new AreteError(errorMessage, 'CONNECTION_ERROR')));
601
+ return;
602
+ }
603
+ if (this.reconnectForTokenRefresh) {
604
+ this.reconnectForTokenRefresh = false;
605
+ void this.connect().catch(() => {
606
+ this.handleReconnect();
607
+ });
608
+ return;
609
+ }
610
+ // Parse close reason for error codes (e.g., "token-expired: Token has expired")
611
+ const closeReason = event.reason || '';
612
+ const errorCodeMatch = closeReason.match(/^([\w-]+):/);
613
+ const errorCode = errorCodeMatch ? parseErrorCode(errorCodeMatch[1]) : null;
614
+ // Check for auth errors that require token refresh
615
+ if (event.code === 1008 || errorCode) {
616
+ const isAuthError = errorCode
617
+ ? shouldRefreshToken(errorCode)
618
+ : /expired|invalid|token/i.test(closeReason);
619
+ if (isAuthError) {
620
+ this.clearTokenState();
621
+ // Try to reconnect immediately with a fresh token
622
+ void this.connect().catch(() => {
623
+ this.handleReconnect();
624
+ });
625
+ return;
626
+ }
627
+ // Check for rate limit errors
628
+ const isRateLimit = errorCode === 'RATE_LIMIT_EXCEEDED' ||
629
+ errorCode === 'CONNECTION_LIMIT_EXCEEDED' ||
630
+ /rate.?limit|quota|limit.?exceeded/i.test(closeReason);
631
+ if (isRateLimit) {
632
+ this.updateState('error', `Rate limit exceeded: ${closeReason}`);
633
+ // Don't auto-reconnect on rate limits, let user handle it
634
+ return;
635
+ }
636
+ }
637
+ if (this.currentState !== 'disconnected') {
638
+ this.handleReconnect();
639
+ }
640
+ };
641
+ }
642
+ catch (error) {
643
+ const hsError = new AreteError('Failed to create WebSocket connection', 'CONNECTION_ERROR', error);
644
+ this.updateState('error', hsError.message);
645
+ reject(hsError);
646
+ }
647
+ });
648
+ }
649
+ disconnect() {
650
+ this.clearReconnectTimeout();
651
+ this.stopPingInterval();
652
+ this.clearTokenRefreshTimeout();
653
+ this.reconnectForTokenRefresh = false;
654
+ this.updateState('disconnected');
655
+ if (this.ws) {
656
+ this.ws.close();
657
+ this.ws = null;
658
+ }
659
+ }
660
+ subscribe(subscription) {
661
+ const subKey = this.makeSubKey(subscription);
662
+ if (this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN) {
663
+ if (this.activeSubscriptions.has(subKey)) {
664
+ return;
665
+ }
666
+ const subMsg = { type: 'subscribe', ...subscription };
667
+ this.ws.send(JSON.stringify(subMsg));
668
+ this.activeSubscriptions.add(subKey);
669
+ }
670
+ else {
671
+ const alreadyQueued = this.subscriptionQueue.some((queuedSubscription) => this.makeSubKey(queuedSubscription) === subKey);
672
+ if (!alreadyQueued) {
673
+ this.subscriptionQueue.push(subscription);
674
+ }
675
+ }
676
+ }
677
+ unsubscribe(view, key) {
678
+ const subscription = { view, key };
679
+ const subKey = this.makeSubKey(subscription);
680
+ if (this.activeSubscriptions.has(subKey)) {
681
+ this.activeSubscriptions.delete(subKey);
682
+ if (this.ws?.readyState === WebSocket.OPEN) {
683
+ const unsubMsg = { type: 'unsubscribe', view, key };
684
+ this.ws.send(JSON.stringify(unsubMsg));
685
+ }
686
+ }
687
+ }
688
+ isConnected() {
689
+ return this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN;
690
+ }
691
+ makeSubKey(subscription) {
692
+ return `${subscription.view}:${subscription.key ?? '*'}:${subscription.partition ?? ''}`;
693
+ }
694
+ flushSubscriptionQueue() {
695
+ while (this.subscriptionQueue.length > 0) {
696
+ const subscription = this.subscriptionQueue.shift();
697
+ if (subscription) {
698
+ this.subscribe(subscription);
699
+ }
700
+ }
701
+ }
702
+ resubscribeActive() {
703
+ for (const subKey of this.activeSubscriptions) {
704
+ const [view, key, partition] = subKey.split(':');
705
+ const subscription = {
706
+ view: view ?? '',
707
+ key: key === '*' ? undefined : key,
708
+ partition: partition || undefined,
709
+ };
710
+ if (this.ws?.readyState === WebSocket.OPEN) {
711
+ const subMsg = { type: 'subscribe', ...subscription };
712
+ this.ws.send(JSON.stringify(subMsg));
713
+ }
714
+ }
715
+ }
716
+ updateState(state, error) {
717
+ this.currentState = state;
718
+ for (const handler of this.stateHandlers) {
719
+ handler(state, error);
720
+ }
721
+ }
722
+ notifyFrameHandlers(frame) {
723
+ for (const handler of this.frameHandlers) {
724
+ handler(frame);
725
+ }
726
+ }
727
+ handleReconnect() {
728
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
729
+ this.updateState('error', `Max reconnection attempts (${this.reconnectAttempts}) reached`);
730
+ return;
731
+ }
732
+ this.updateState('reconnecting');
733
+ const attemptIndex = Math.min(this.reconnectAttempts, this.reconnectIntervals.length - 1);
734
+ const delay = this.reconnectIntervals[attemptIndex] ?? 1000;
735
+ this.reconnectAttempts++;
736
+ this.reconnectTimeout = setTimeout(() => {
737
+ this.connect().catch(() => {
738
+ /* retry handled by onclose */
739
+ });
740
+ }, delay);
741
+ }
742
+ clearReconnectTimeout() {
743
+ if (this.reconnectTimeout) {
744
+ clearTimeout(this.reconnectTimeout);
745
+ this.reconnectTimeout = null;
746
+ }
747
+ }
748
+ startPingInterval() {
749
+ this.stopPingInterval();
750
+ this.pingInterval = setInterval(() => {
751
+ if (this.ws?.readyState === WebSocket.OPEN) {
752
+ this.ws.send('{"type":"ping"}');
753
+ }
754
+ }, 15000);
755
+ }
756
+ stopPingInterval() {
757
+ if (this.pingInterval) {
758
+ clearInterval(this.pingInterval);
759
+ this.pingInterval = null;
760
+ }
761
+ }
762
+ }
763
+
764
+ function isObject$1(item) {
765
+ return item !== null && typeof item === 'object' && !Array.isArray(item);
766
+ }
767
+ function deepMergeWithAppend$1(target, source, appendPaths, currentPath = '') {
768
+ if (!isObject$1(target) || !isObject$1(source)) {
769
+ return source;
770
+ }
771
+ const result = { ...target };
772
+ for (const key in source) {
773
+ const sourceValue = source[key];
774
+ const targetValue = result[key];
775
+ const fieldPath = currentPath ? `${currentPath}.${key}` : key;
776
+ if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
777
+ if (appendPaths.includes(fieldPath)) {
778
+ result[key] = [...targetValue, ...sourceValue];
779
+ }
780
+ else {
781
+ result[key] = sourceValue;
782
+ }
783
+ }
784
+ else if (isObject$1(sourceValue) && isObject$1(targetValue)) {
785
+ result[key] = deepMergeWithAppend$1(targetValue, sourceValue, appendPaths, fieldPath);
786
+ }
787
+ else {
788
+ result[key] = sourceValue;
789
+ }
790
+ }
791
+ return result;
792
+ }
793
+ class FrameProcessor {
794
+ constructor(storage, config = {}) {
795
+ this.pendingUpdates = [];
796
+ this.flushTimer = null;
797
+ this.isProcessing = false;
798
+ this.storage = storage;
799
+ this.maxEntriesPerView = config.maxEntriesPerView === undefined
800
+ ? DEFAULT_MAX_ENTRIES_PER_VIEW
801
+ : config.maxEntriesPerView;
802
+ this.flushIntervalMs = config.flushIntervalMs ?? 0;
803
+ this.schemas = config.schemas;
804
+ }
805
+ getSchema(viewPath) {
806
+ const schemas = this.schemas;
807
+ if (!schemas)
808
+ return null;
809
+ const entityName = viewPath.split('/')[0];
810
+ if (typeof entityName !== 'string' || entityName.length === 0)
811
+ return null;
812
+ const entityKey = entityName;
813
+ return schemas[entityKey] ?? null;
814
+ }
815
+ validateEntity(viewPath, data) {
816
+ const schema = this.getSchema(viewPath);
817
+ if (!schema)
818
+ return true;
819
+ const result = schema.safeParse(data);
820
+ if (!result.success) {
821
+ console.warn('[Arete] Frame validation failed:', {
822
+ view: viewPath,
823
+ error: result.error,
824
+ });
825
+ return false;
826
+ }
827
+ return true;
828
+ }
829
+ handleFrame(frame) {
830
+ if (this.flushIntervalMs === 0) {
831
+ this.processFrame(frame);
832
+ return;
833
+ }
834
+ this.pendingUpdates.push({ frame });
835
+ this.scheduleFlush();
836
+ }
837
+ /**
838
+ * Immediately flush all pending updates.
839
+ * Useful for ensuring all updates are processed before reading state.
840
+ */
841
+ flush() {
842
+ if (this.flushTimer !== null) {
843
+ clearTimeout(this.flushTimer);
844
+ this.flushTimer = null;
845
+ }
846
+ this.flushPendingUpdates();
847
+ }
848
+ /**
849
+ * Clean up any pending timers. Call when disposing the processor.
850
+ */
851
+ dispose() {
852
+ if (this.flushTimer !== null) {
853
+ clearTimeout(this.flushTimer);
854
+ this.flushTimer = null;
855
+ }
856
+ this.pendingUpdates = [];
857
+ }
858
+ scheduleFlush() {
859
+ if (this.flushTimer !== null) {
860
+ return;
861
+ }
862
+ this.flushTimer = setTimeout(() => {
863
+ this.flushTimer = null;
864
+ this.flushPendingUpdates();
865
+ }, this.flushIntervalMs);
866
+ }
867
+ flushPendingUpdates() {
868
+ if (this.isProcessing || this.pendingUpdates.length === 0) {
869
+ return;
870
+ }
871
+ this.isProcessing = true;
872
+ const batch = this.pendingUpdates;
873
+ this.pendingUpdates = [];
874
+ const viewsToEnforce = new Set();
875
+ for (const { frame } of batch) {
876
+ const viewPath = this.processFrameWithoutEnforce(frame);
877
+ if (viewPath) {
878
+ viewsToEnforce.add(viewPath);
879
+ }
880
+ }
881
+ viewsToEnforce.forEach((viewPath) => {
882
+ this.enforceMaxEntries(viewPath);
883
+ });
884
+ this.isProcessing = false;
885
+ }
886
+ processFrame(frame) {
887
+ if (isSubscribedFrame(frame)) {
888
+ this.handleSubscribedFrame(frame);
889
+ }
890
+ else if (isSnapshotFrame(frame)) {
891
+ this.handleSnapshotFrame(frame);
892
+ }
893
+ else {
894
+ this.handleEntityFrame(frame);
895
+ }
896
+ }
897
+ processFrameWithoutEnforce(frame) {
898
+ if (isSubscribedFrame(frame)) {
899
+ this.handleSubscribedFrame(frame);
900
+ return null;
901
+ }
902
+ else if (isSnapshotFrame(frame)) {
903
+ this.handleSnapshotFrameWithoutEnforce(frame);
904
+ return frame.entity;
905
+ }
906
+ else {
907
+ this.handleEntityFrameWithoutEnforce(frame);
908
+ return frame.entity;
909
+ }
910
+ }
911
+ handleSubscribedFrame(frame) {
912
+ if (this.storage.setViewConfig && frame.sort) {
913
+ this.storage.setViewConfig(frame.view, { sort: frame.sort });
914
+ }
915
+ }
916
+ handleSnapshotFrame(frame) {
917
+ this.handleSnapshotFrameWithoutEnforce(frame);
918
+ this.enforceMaxEntries(frame.entity);
919
+ }
920
+ handleSnapshotFrameWithoutEnforce(frame) {
921
+ const viewPath = frame.entity;
922
+ for (const entity of frame.data) {
923
+ if (!this.validateEntity(viewPath, entity.data)) {
924
+ continue;
925
+ }
926
+ const previousValue = this.storage.get(viewPath, entity.key);
927
+ this.storage.set(viewPath, entity.key, entity.data);
928
+ this.storage.notifyUpdate(viewPath, entity.key, {
929
+ type: 'upsert',
930
+ key: entity.key,
931
+ data: entity.data,
932
+ });
933
+ this.emitRichUpdate(viewPath, entity.key, previousValue, entity.data, 'upsert');
934
+ }
935
+ }
936
+ handleEntityFrame(frame) {
937
+ this.handleEntityFrameWithoutEnforce(frame);
938
+ this.enforceMaxEntries(frame.entity);
939
+ }
940
+ handleEntityFrameWithoutEnforce(frame) {
941
+ const viewPath = frame.entity;
942
+ const previousValue = this.storage.get(viewPath, frame.key);
943
+ switch (frame.op) {
944
+ case 'create':
945
+ case 'upsert':
946
+ if (!this.validateEntity(viewPath, frame.data)) {
947
+ break;
948
+ }
949
+ this.storage.set(viewPath, frame.key, frame.data);
950
+ this.storage.notifyUpdate(viewPath, frame.key, {
951
+ type: 'upsert',
952
+ key: frame.key,
953
+ data: frame.data,
954
+ });
955
+ this.emitRichUpdate(viewPath, frame.key, previousValue, frame.data, frame.op);
956
+ break;
957
+ case 'patch': {
958
+ const existing = this.storage.get(viewPath, frame.key);
959
+ const appendPaths = frame.append ?? [];
960
+ const merged = existing
961
+ ? deepMergeWithAppend$1(existing, frame.data, appendPaths)
962
+ : frame.data;
963
+ if (!this.validateEntity(viewPath, merged)) {
964
+ break;
965
+ }
966
+ this.storage.set(viewPath, frame.key, merged);
967
+ this.storage.notifyUpdate(viewPath, frame.key, {
968
+ type: 'patch',
969
+ key: frame.key,
970
+ data: frame.data,
971
+ });
972
+ this.emitRichUpdate(viewPath, frame.key, previousValue, merged, 'patch', frame.data);
973
+ break;
974
+ }
975
+ case 'delete':
976
+ this.storage.delete(viewPath, frame.key);
977
+ this.storage.notifyUpdate(viewPath, frame.key, {
978
+ type: 'delete',
979
+ key: frame.key,
980
+ });
981
+ if (previousValue !== null) {
982
+ const richUpdate = { type: 'deleted', key: frame.key, lastKnown: previousValue };
983
+ this.storage.notifyRichUpdate(viewPath, frame.key, richUpdate);
984
+ }
985
+ break;
986
+ }
987
+ }
988
+ emitRichUpdate(viewPath, key, before, after, _op, patch) {
989
+ const richUpdate = before === null
990
+ ? { type: 'created', key, data: after }
991
+ : { type: 'updated', key, before, after, patch };
992
+ this.storage.notifyRichUpdate(viewPath, key, richUpdate);
993
+ }
994
+ enforceMaxEntries(viewPath) {
995
+ if (this.maxEntriesPerView === null)
996
+ return;
997
+ if (!this.storage.evictOldest)
998
+ return;
999
+ while (this.storage.size(viewPath) > this.maxEntriesPerView) {
1000
+ this.storage.evictOldest(viewPath);
1001
+ }
1002
+ }
1003
+ }
1004
+
1005
+ let ViewData$1 = class ViewData {
1006
+ constructor() {
1007
+ this.entities = new Map();
1008
+ this.accessOrder = [];
1009
+ }
1010
+ get(key) {
1011
+ return this.entities.get(key);
1012
+ }
1013
+ set(key, value) {
1014
+ if (!this.entities.has(key)) {
1015
+ this.accessOrder.push(key);
1016
+ }
1017
+ else {
1018
+ this.touch(key);
1019
+ }
1020
+ this.entities.set(key, value);
1021
+ }
1022
+ delete(key) {
1023
+ const idx = this.accessOrder.indexOf(key);
1024
+ if (idx !== -1) {
1025
+ this.accessOrder.splice(idx, 1);
1026
+ }
1027
+ return this.entities.delete(key);
1028
+ }
1029
+ has(key) {
1030
+ return this.entities.has(key);
1031
+ }
1032
+ values() {
1033
+ return this.entities.values();
1034
+ }
1035
+ keys() {
1036
+ return this.entities.keys();
1037
+ }
1038
+ get size() {
1039
+ return this.entities.size;
1040
+ }
1041
+ touch(key) {
1042
+ const idx = this.accessOrder.indexOf(key);
1043
+ if (idx !== -1) {
1044
+ this.accessOrder.splice(idx, 1);
1045
+ this.accessOrder.push(key);
1046
+ }
1047
+ }
1048
+ evictOldest() {
1049
+ const oldest = this.accessOrder.shift();
1050
+ if (oldest !== undefined) {
1051
+ this.entities.delete(oldest);
1052
+ }
1053
+ return oldest;
1054
+ }
1055
+ clear() {
1056
+ this.entities.clear();
1057
+ this.accessOrder = [];
1058
+ }
1059
+ };
1060
+ class MemoryAdapter {
1061
+ constructor(_config = {}) {
1062
+ this.views = new Map();
1063
+ this.updateCallbacks = new Set();
1064
+ this.richUpdateCallbacks = new Set();
1065
+ }
1066
+ get(viewPath, key) {
1067
+ const view = this.views.get(viewPath);
1068
+ if (!view)
1069
+ return null;
1070
+ const value = view.get(key);
1071
+ return value !== undefined ? value : null;
1072
+ }
1073
+ getAll(viewPath) {
1074
+ const view = this.views.get(viewPath);
1075
+ if (!view)
1076
+ return [];
1077
+ return Array.from(view.values());
1078
+ }
1079
+ getAllSync(viewPath) {
1080
+ const view = this.views.get(viewPath);
1081
+ if (!view)
1082
+ return undefined;
1083
+ return Array.from(view.values());
1084
+ }
1085
+ getSync(viewPath, key) {
1086
+ const view = this.views.get(viewPath);
1087
+ if (!view)
1088
+ return undefined;
1089
+ const value = view.get(key);
1090
+ return value !== undefined ? value : null;
1091
+ }
1092
+ has(viewPath, key) {
1093
+ return this.views.get(viewPath)?.has(key) ?? false;
1094
+ }
1095
+ keys(viewPath) {
1096
+ const view = this.views.get(viewPath);
1097
+ if (!view)
1098
+ return [];
1099
+ return Array.from(view.keys());
1100
+ }
1101
+ size(viewPath) {
1102
+ return this.views.get(viewPath)?.size ?? 0;
1103
+ }
1104
+ set(viewPath, key, data) {
1105
+ let view = this.views.get(viewPath);
1106
+ if (!view) {
1107
+ view = new ViewData$1();
1108
+ this.views.set(viewPath, view);
1109
+ }
1110
+ view.set(key, data);
1111
+ }
1112
+ delete(viewPath, key) {
1113
+ this.views.get(viewPath)?.delete(key);
1114
+ }
1115
+ clear(viewPath) {
1116
+ if (viewPath) {
1117
+ this.views.get(viewPath)?.clear();
1118
+ this.views.delete(viewPath);
1119
+ }
1120
+ else {
1121
+ this.views.clear();
1122
+ }
1123
+ }
1124
+ evictOldest(viewPath) {
1125
+ return this.views.get(viewPath)?.evictOldest();
1126
+ }
1127
+ onUpdate(callback) {
1128
+ this.updateCallbacks.add(callback);
1129
+ return () => this.updateCallbacks.delete(callback);
1130
+ }
1131
+ onRichUpdate(callback) {
1132
+ this.richUpdateCallbacks.add(callback);
1133
+ return () => this.richUpdateCallbacks.delete(callback);
1134
+ }
1135
+ notifyUpdate(viewPath, key, update) {
1136
+ for (const callback of this.updateCallbacks) {
1137
+ callback(viewPath, key, update);
1138
+ }
1139
+ }
1140
+ notifyRichUpdate(viewPath, key, update) {
1141
+ for (const callback of this.richUpdateCallbacks) {
1142
+ callback(viewPath, key, update);
1143
+ }
1144
+ }
1145
+ }
1146
+
1147
+ function getNestedValue$1(obj, path) {
1148
+ let current = obj;
1149
+ for (const segment of path) {
1150
+ if (current === null || current === undefined)
1151
+ return undefined;
1152
+ if (typeof current !== 'object')
1153
+ return undefined;
1154
+ current = current[segment];
1155
+ }
1156
+ return current;
1157
+ }
1158
+ function compareSortValues$1(a, b) {
1159
+ if (a === b)
1160
+ return 0;
1161
+ if (a === undefined || a === null)
1162
+ return -1;
1163
+ if (b === undefined || b === null)
1164
+ return 1;
1165
+ if (typeof a === 'number' && typeof b === 'number') {
1166
+ return a - b;
1167
+ }
1168
+ if (typeof a === 'string' && typeof b === 'string') {
1169
+ return a.localeCompare(b);
1170
+ }
1171
+ if (typeof a === 'boolean' && typeof b === 'boolean') {
1172
+ return (a ? 1 : 0) - (b ? 1 : 0);
1173
+ }
1174
+ return String(a).localeCompare(String(b));
1175
+ }
1176
+ class SortedStorageDecorator {
1177
+ constructor(inner) {
1178
+ this.sortConfigs = new Map();
1179
+ this.sortedKeysMap = new Map();
1180
+ this.inner = inner;
1181
+ }
1182
+ get(viewPath, key) {
1183
+ return this.inner.get(viewPath, key);
1184
+ }
1185
+ getAll(viewPath) {
1186
+ const sortedKeys = this.sortedKeysMap.get(viewPath);
1187
+ if (sortedKeys && sortedKeys.length > 0) {
1188
+ return sortedKeys
1189
+ .map(k => this.inner.get(viewPath, k))
1190
+ .filter((v) => v !== null);
1191
+ }
1192
+ return this.inner.getAll(viewPath);
1193
+ }
1194
+ getAllSync(viewPath) {
1195
+ const sortedKeys = this.sortedKeysMap.get(viewPath);
1196
+ if (sortedKeys && sortedKeys.length > 0) {
1197
+ return sortedKeys
1198
+ .map(k => this.inner.getSync(viewPath, k))
1199
+ .filter((v) => v !== null && v !== undefined);
1200
+ }
1201
+ return this.inner.getAllSync(viewPath);
1202
+ }
1203
+ getSync(viewPath, key) {
1204
+ return this.inner.getSync(viewPath, key);
1205
+ }
1206
+ has(viewPath, key) {
1207
+ return this.inner.has(viewPath, key);
1208
+ }
1209
+ keys(viewPath) {
1210
+ const sortedKeys = this.sortedKeysMap.get(viewPath);
1211
+ if (sortedKeys)
1212
+ return [...sortedKeys];
1213
+ return this.inner.keys(viewPath);
1214
+ }
1215
+ size(viewPath) {
1216
+ return this.inner.size(viewPath);
1217
+ }
1218
+ set(viewPath, key, data) {
1219
+ this.inner.set(viewPath, key, data);
1220
+ const sortConfig = this.sortConfigs.get(viewPath);
1221
+ if (sortConfig) {
1222
+ this.updateSortedPosition(viewPath, key, data, sortConfig);
1223
+ }
1224
+ }
1225
+ delete(viewPath, key) {
1226
+ const sortedKeys = this.sortedKeysMap.get(viewPath);
1227
+ if (sortedKeys) {
1228
+ const idx = sortedKeys.indexOf(key);
1229
+ if (idx !== -1) {
1230
+ sortedKeys.splice(idx, 1);
1231
+ }
1232
+ }
1233
+ this.inner.delete(viewPath, key);
1234
+ }
1235
+ clear(viewPath) {
1236
+ if (viewPath) {
1237
+ this.sortedKeysMap.delete(viewPath);
1238
+ this.sortConfigs.delete(viewPath);
1239
+ }
1240
+ else {
1241
+ this.sortedKeysMap.clear();
1242
+ this.sortConfigs.clear();
1243
+ }
1244
+ this.inner.clear(viewPath);
1245
+ }
1246
+ evictOldest(viewPath) {
1247
+ const sortedKeys = this.sortedKeysMap.get(viewPath);
1248
+ if (sortedKeys && sortedKeys.length > 0) {
1249
+ const oldest = sortedKeys.pop();
1250
+ this.inner.delete(viewPath, oldest);
1251
+ return oldest;
1252
+ }
1253
+ return this.inner.evictOldest?.(viewPath);
1254
+ }
1255
+ setViewConfig(viewPath, config) {
1256
+ if (config.sort && !this.sortConfigs.has(viewPath)) {
1257
+ this.sortConfigs.set(viewPath, config.sort);
1258
+ this.rebuildSortedKeys(viewPath, config.sort);
1259
+ }
1260
+ this.inner.setViewConfig?.(viewPath, config);
1261
+ }
1262
+ getViewConfig(viewPath) {
1263
+ const sortConfig = this.sortConfigs.get(viewPath);
1264
+ if (sortConfig)
1265
+ return { sort: sortConfig };
1266
+ return this.inner.getViewConfig?.(viewPath);
1267
+ }
1268
+ onUpdate(callback) {
1269
+ return this.inner.onUpdate(callback);
1270
+ }
1271
+ onRichUpdate(callback) {
1272
+ return this.inner.onRichUpdate(callback);
1273
+ }
1274
+ notifyUpdate(viewPath, key, update) {
1275
+ this.inner.notifyUpdate(viewPath, key, update);
1276
+ }
1277
+ notifyRichUpdate(viewPath, key, update) {
1278
+ this.inner.notifyRichUpdate(viewPath, key, update);
1279
+ }
1280
+ updateSortedPosition(viewPath, key, data, sortConfig) {
1281
+ let sortedKeys = this.sortedKeysMap.get(viewPath);
1282
+ if (!sortedKeys) {
1283
+ sortedKeys = [];
1284
+ this.sortedKeysMap.set(viewPath, sortedKeys);
1285
+ }
1286
+ const existingIdx = sortedKeys.indexOf(key);
1287
+ if (existingIdx !== -1) {
1288
+ sortedKeys.splice(existingIdx, 1);
1289
+ }
1290
+ const insertIdx = this.binarySearchInsertPosition(viewPath, sortedKeys, sortConfig, key, data);
1291
+ sortedKeys.splice(insertIdx, 0, key);
1292
+ }
1293
+ binarySearchInsertPosition(viewPath, sortedKeys, sortConfig, newKey, newValue) {
1294
+ const newSortValue = getNestedValue$1(newValue, sortConfig.field);
1295
+ const isDesc = sortConfig.order === 'desc';
1296
+ let low = 0;
1297
+ let high = sortedKeys.length;
1298
+ while (low < high) {
1299
+ const mid = Math.floor((low + high) / 2);
1300
+ const midKey = sortedKeys[mid];
1301
+ const midEntity = this.inner.get(viewPath, midKey);
1302
+ const midValue = getNestedValue$1(midEntity, sortConfig.field);
1303
+ let cmp = compareSortValues$1(newSortValue, midValue);
1304
+ if (isDesc)
1305
+ cmp = -cmp;
1306
+ if (cmp === 0) {
1307
+ cmp = newKey.localeCompare(midKey);
1308
+ }
1309
+ if (cmp < 0) {
1310
+ high = mid;
1311
+ }
1312
+ else {
1313
+ low = mid + 1;
1314
+ }
1315
+ }
1316
+ return low;
1317
+ }
1318
+ rebuildSortedKeys(viewPath, sortConfig) {
1319
+ const allKeys = this.inner.keys(viewPath);
1320
+ if (allKeys.length === 0)
1321
+ return;
1322
+ const isDesc = sortConfig.order === 'desc';
1323
+ const entries = allKeys.map(k => [k, this.inner.get(viewPath, k)]);
1324
+ entries.sort((a, b) => {
1325
+ const aValue = getNestedValue$1(a[1], sortConfig.field);
1326
+ const bValue = getNestedValue$1(b[1], sortConfig.field);
1327
+ let cmp = compareSortValues$1(aValue, bValue);
1328
+ if (isDesc)
1329
+ cmp = -cmp;
1330
+ if (cmp === 0) {
1331
+ cmp = a[0].localeCompare(b[0]);
1332
+ }
1333
+ return cmp;
1334
+ });
1335
+ this.sortedKeysMap.set(viewPath, entries.map(([k]) => k));
1336
+ }
1337
+ }
1338
+
1339
+ class SubscriptionRegistry {
1340
+ constructor(connection) {
1341
+ this.subscriptions = new Map();
1342
+ this.connection = connection;
1343
+ }
1344
+ subscribe(subscription) {
1345
+ const subKey = this.makeSubKey(subscription);
1346
+ const existing = this.subscriptions.get(subKey);
1347
+ if (existing) {
1348
+ existing.refCount++;
1349
+ }
1350
+ else {
1351
+ this.subscriptions.set(subKey, {
1352
+ subscription,
1353
+ refCount: 1,
1354
+ });
1355
+ this.connection.subscribe(subscription);
1356
+ }
1357
+ return () => this.unsubscribe(subscription);
1358
+ }
1359
+ unsubscribe(subscription) {
1360
+ const subKey = this.makeSubKey(subscription);
1361
+ const existing = this.subscriptions.get(subKey);
1362
+ if (existing) {
1363
+ existing.refCount--;
1364
+ if (existing.refCount <= 0) {
1365
+ this.subscriptions.delete(subKey);
1366
+ this.connection.unsubscribe(subscription.view, subscription.key);
1367
+ }
1368
+ }
1369
+ }
1370
+ getRefCount(subscription) {
1371
+ const subKey = this.makeSubKey(subscription);
1372
+ return this.subscriptions.get(subKey)?.refCount ?? 0;
1373
+ }
1374
+ getActiveSubscriptions() {
1375
+ return Array.from(this.subscriptions.values()).map((t) => t.subscription);
1376
+ }
1377
+ clear() {
1378
+ for (const { subscription } of this.subscriptions.values()) {
1379
+ this.connection.unsubscribe(subscription.view, subscription.key);
1380
+ }
1381
+ this.subscriptions.clear();
1382
+ }
1383
+ makeSubKey(subscription) {
1384
+ const filters = subscription.filters ? JSON.stringify(subscription.filters) : '{}';
1385
+ return `${subscription.view}:${subscription.key ?? '*'}:${subscription.partition ?? ''}:${filters}`;
1386
+ }
1387
+ }
1388
+
1389
+ const MAX_QUEUE_SIZE = 1000;
1390
+ function createUpdateStream(storage, subscriptionRegistry, subscription, keyFilter) {
1391
+ return {
1392
+ [Symbol.asyncIterator]() {
1393
+ const queue = [];
1394
+ let waitingResolve = null;
1395
+ let unsubscribeStorage = null;
1396
+ let unsubscribeRegistry = null;
1397
+ let done = false;
1398
+ const handler = (viewPath, key, update) => {
1399
+ if (viewPath !== subscription.view)
1400
+ return;
1401
+ if (keyFilter !== undefined && key !== keyFilter)
1402
+ return;
1403
+ const typedUpdate = update;
1404
+ if (waitingResolve) {
1405
+ const resolve = waitingResolve;
1406
+ waitingResolve = null;
1407
+ resolve({ value: typedUpdate, done: false });
1408
+ }
1409
+ else {
1410
+ if (queue.length >= MAX_QUEUE_SIZE) {
1411
+ queue.shift();
1412
+ }
1413
+ queue.push({
1414
+ update: typedUpdate,
1415
+ resolve: () => { },
1416
+ });
1417
+ }
1418
+ };
1419
+ const start = () => {
1420
+ unsubscribeStorage = storage.onUpdate(handler);
1421
+ unsubscribeRegistry = subscriptionRegistry.subscribe(subscription);
1422
+ };
1423
+ const cleanup = () => {
1424
+ done = true;
1425
+ unsubscribeStorage?.();
1426
+ unsubscribeRegistry?.();
1427
+ };
1428
+ start();
1429
+ return {
1430
+ async next() {
1431
+ if (done) {
1432
+ return { value: undefined, done: true };
1433
+ }
1434
+ const queued = queue.shift();
1435
+ if (queued) {
1436
+ return { value: queued.update, done: false };
1437
+ }
1438
+ return new Promise((resolve) => {
1439
+ waitingResolve = resolve;
1440
+ });
1441
+ },
1442
+ async return() {
1443
+ cleanup();
1444
+ return { value: undefined, done: true };
1445
+ },
1446
+ async throw(error) {
1447
+ cleanup();
1448
+ throw error;
1449
+ },
1450
+ };
1451
+ },
1452
+ };
1453
+ }
1454
+ function createEntityStream(storage, subscriptionRegistry, subscription, options, keyFilter) {
1455
+ const schema = options?.schema;
1456
+ return {
1457
+ [Symbol.asyncIterator]() {
1458
+ const queue = [];
1459
+ let waitingResolve = null;
1460
+ let unsubscribeStorage = null;
1461
+ let unsubscribeRegistry = null;
1462
+ let done = false;
1463
+ const handler = (viewPath, key, update) => {
1464
+ if (viewPath !== subscription.view)
1465
+ return;
1466
+ if (keyFilter !== undefined && key !== keyFilter)
1467
+ return;
1468
+ if (update.type === 'deleted')
1469
+ return;
1470
+ const entity = (update.type === 'created' ? update.data : update.after);
1471
+ let output;
1472
+ if (schema) {
1473
+ const parsed = schema.safeParse(entity);
1474
+ if (!parsed.success) {
1475
+ return;
1476
+ }
1477
+ output = parsed.data;
1478
+ }
1479
+ else {
1480
+ output = entity;
1481
+ }
1482
+ if (waitingResolve) {
1483
+ const resolve = waitingResolve;
1484
+ waitingResolve = null;
1485
+ resolve({ value: output, done: false });
1486
+ }
1487
+ else {
1488
+ if (queue.length >= MAX_QUEUE_SIZE) {
1489
+ queue.shift();
1490
+ }
1491
+ queue.push(output);
1492
+ }
1493
+ };
1494
+ const start = () => {
1495
+ unsubscribeStorage = storage.onRichUpdate(handler);
1496
+ unsubscribeRegistry = subscriptionRegistry.subscribe(subscription);
1497
+ };
1498
+ const cleanup = () => {
1499
+ done = true;
1500
+ unsubscribeStorage?.();
1501
+ unsubscribeRegistry?.();
1502
+ };
1503
+ start();
1504
+ const iterator = {
1505
+ async next() {
1506
+ if (done) {
1507
+ return { value: undefined, done: true };
1508
+ }
1509
+ const queued = queue.shift();
1510
+ if (queued) {
1511
+ return { value: queued, done: false };
1512
+ }
1513
+ return new Promise((resolve) => {
1514
+ waitingResolve = resolve;
1515
+ });
1516
+ },
1517
+ async return() {
1518
+ cleanup();
1519
+ return { value: undefined, done: true };
1520
+ },
1521
+ async throw(error) {
1522
+ cleanup();
1523
+ throw error;
1524
+ },
1525
+ };
1526
+ return iterator;
1527
+ },
1528
+ };
1529
+ }
1530
+ function createRichUpdateStream(storage, subscriptionRegistry, subscription, keyFilter) {
1531
+ return {
1532
+ [Symbol.asyncIterator]() {
1533
+ const queue = [];
1534
+ let waitingResolve = null;
1535
+ let unsubscribeStorage = null;
1536
+ let unsubscribeRegistry = null;
1537
+ let done = false;
1538
+ const handler = (viewPath, key, update) => {
1539
+ if (viewPath !== subscription.view)
1540
+ return;
1541
+ if (keyFilter !== undefined && key !== keyFilter)
1542
+ return;
1543
+ const typedUpdate = update;
1544
+ if (waitingResolve) {
1545
+ const resolve = waitingResolve;
1546
+ waitingResolve = null;
1547
+ resolve({ value: typedUpdate, done: false });
1548
+ }
1549
+ else {
1550
+ if (queue.length >= MAX_QUEUE_SIZE) {
1551
+ queue.shift();
1552
+ }
1553
+ queue.push({
1554
+ update: typedUpdate,
1555
+ resolve: () => { },
1556
+ });
1557
+ }
1558
+ };
1559
+ const start = () => {
1560
+ unsubscribeStorage = storage.onRichUpdate(handler);
1561
+ unsubscribeRegistry = subscriptionRegistry.subscribe(subscription);
1562
+ };
1563
+ const cleanup = () => {
1564
+ done = true;
1565
+ unsubscribeStorage?.();
1566
+ unsubscribeRegistry?.();
1567
+ };
1568
+ start();
1569
+ return {
1570
+ async next() {
1571
+ if (done) {
1572
+ return { value: undefined, done: true };
1573
+ }
1574
+ const queued = queue.shift();
1575
+ if (queued) {
1576
+ return { value: queued.update, done: false };
1577
+ }
1578
+ return new Promise((resolve) => {
1579
+ waitingResolve = resolve;
1580
+ });
1581
+ },
1582
+ async return() {
1583
+ cleanup();
1584
+ return { value: undefined, done: true };
1585
+ },
1586
+ async throw(error) {
1587
+ cleanup();
1588
+ throw error;
1589
+ },
1590
+ };
1591
+ },
1592
+ };
1593
+ }
1594
+
1595
+ function createTypedStateView(viewDef, storage, subscriptionRegistry) {
1596
+ return {
1597
+ use(key, options) {
1598
+ const { schema: _schema, ...subscriptionOptions } = options ?? {};
1599
+ return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, options, key);
1600
+ },
1601
+ watch(key, options) {
1602
+ const { schema: _schema, ...subscriptionOptions } = options ?? {};
1603
+ return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, key);
1604
+ },
1605
+ watchRich(key, options) {
1606
+ const { schema: _schema, ...subscriptionOptions } = options ?? {};
1607
+ return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, key);
1608
+ },
1609
+ async get(key) {
1610
+ return storage.get(viewDef.view, key);
1611
+ },
1612
+ getSync(key) {
1613
+ return storage.getSync(viewDef.view, key);
1614
+ },
1615
+ };
1616
+ }
1617
+ function createTypedListView(viewDef, storage, subscriptionRegistry) {
1618
+ return {
1619
+ use(options) {
1620
+ const { schema: _schema, ...subscriptionOptions } = options ?? {};
1621
+ return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions }, options);
1622
+ },
1623
+ watch(options) {
1624
+ const { schema: _schema, ...subscriptionOptions } = options ?? {};
1625
+ return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions });
1626
+ },
1627
+ watchRich(options) {
1628
+ const { schema: _schema, ...subscriptionOptions } = options ?? {};
1629
+ return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions });
1630
+ },
1631
+ async get() {
1632
+ return storage.getAll(viewDef.view);
1633
+ },
1634
+ getSync() {
1635
+ return storage.getAllSync(viewDef.view);
1636
+ },
1637
+ };
1638
+ }
1639
+ function createTypedViews(stack, storage, subscriptionRegistry) {
1640
+ const views = {};
1641
+ for (const [entityName, viewGroup] of Object.entries(stack.views)) {
1642
+ const group = viewGroup;
1643
+ const typedGroup = {};
1644
+ for (const [viewName, viewDef] of Object.entries(group)) {
1645
+ if (viewDef.mode === 'state') {
1646
+ typedGroup[viewName] = createTypedStateView(viewDef, storage, subscriptionRegistry);
1647
+ }
1648
+ else if (viewDef.mode === 'list') {
1649
+ typedGroup[viewName] = createTypedListView(viewDef, storage, subscriptionRegistry);
1650
+ }
1651
+ }
1652
+ views[entityName] = typedGroup;
1653
+ }
1654
+ return views;
1655
+ }
1656
+
1657
+ /**
1658
+ * PDA (Program Derived Address) derivation utilities.
1659
+ *
1660
+ * Implements Solana's PDA derivation algorithm without depending on @solana/web3.js.
1661
+ */
1662
+ // Base58 alphabet (Bitcoin/Solana style)
1663
+ const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
1664
+ /**
1665
+ * Decode base58 string to Uint8Array.
1666
+ */
1667
+ function decodeBase58(str) {
1668
+ if (str.length === 0) {
1669
+ return new Uint8Array(0);
1670
+ }
1671
+ const bytes = [0];
1672
+ for (const char of str) {
1673
+ const value = BASE58_ALPHABET.indexOf(char);
1674
+ if (value === -1) {
1675
+ throw new Error('Invalid base58 character: ' + char);
1676
+ }
1677
+ let carry = value;
1678
+ for (let i = 0; i < bytes.length; i++) {
1679
+ carry += (bytes[i] ?? 0) * 58;
1680
+ bytes[i] = carry & 0xff;
1681
+ carry >>= 8;
1682
+ }
1683
+ while (carry > 0) {
1684
+ bytes.push(carry & 0xff);
1685
+ carry >>= 8;
1686
+ }
1687
+ }
1688
+ // Add leading zeros for each leading '1' in input
1689
+ for (const char of str) {
1690
+ if (char !== '1')
1691
+ break;
1692
+ bytes.push(0);
1693
+ }
1694
+ return new Uint8Array(bytes.reverse());
1695
+ }
1696
+ /**
1697
+ * Encode Uint8Array to base58 string.
1698
+ */
1699
+ function encodeBase58(bytes) {
1700
+ if (bytes.length === 0) {
1701
+ return '';
1702
+ }
1703
+ const digits = [0];
1704
+ for (const byte of bytes) {
1705
+ let carry = byte;
1706
+ for (let i = 0; i < digits.length; i++) {
1707
+ carry += (digits[i] ?? 0) << 8;
1708
+ digits[i] = carry % 58;
1709
+ carry = (carry / 58) | 0;
1710
+ }
1711
+ while (carry > 0) {
1712
+ digits.push(carry % 58);
1713
+ carry = (carry / 58) | 0;
1714
+ }
1715
+ }
1716
+ // Add leading zeros for each leading 0 byte in input
1717
+ for (const byte of bytes) {
1718
+ if (byte !== 0)
1719
+ break;
1720
+ digits.push(0);
1721
+ }
1722
+ return digits.reverse().map(d => BASE58_ALPHABET[d]).join('');
1723
+ }
1724
+ /**
1725
+ * SHA-256 hash function (synchronous, Node.js).
1726
+ */
1727
+ function sha256Sync(data) {
1728
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1729
+ const { createHash } = require('crypto');
1730
+ return new Uint8Array(createHash('sha256').update(Buffer.from(data)).digest());
1731
+ }
1732
+ /**
1733
+ * SHA-256 hash function (async, works in browser and Node.js).
1734
+ */
1735
+ async function sha256Async(data) {
1736
+ if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.subtle) {
1737
+ // Create a copy of the data to ensure we have an ArrayBuffer
1738
+ const copy = new Uint8Array(data);
1739
+ const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', copy);
1740
+ return new Uint8Array(hashBuffer);
1741
+ }
1742
+ return sha256Sync(data);
1743
+ }
1744
+ /**
1745
+ * PDA marker bytes appended to seeds before hashing.
1746
+ */
1747
+ const PDA_MARKER = new TextEncoder().encode('ProgramDerivedAddress');
1748
+ /**
1749
+ * Build the hash input buffer for PDA derivation.
1750
+ */
1751
+ function buildPdaBuffer(seeds, programIdBytes, bump) {
1752
+ const totalLength = seeds.reduce((sum, s) => sum + s.length, 0)
1753
+ + 1 // bump
1754
+ + 32 // programId
1755
+ + PDA_MARKER.length;
1756
+ const buffer = new Uint8Array(totalLength);
1757
+ let offset = 0;
1758
+ // Copy seeds
1759
+ for (const seed of seeds) {
1760
+ buffer.set(seed, offset);
1761
+ offset += seed.length;
1762
+ }
1763
+ // Add bump seed
1764
+ buffer[offset++] = bump;
1765
+ // Add program ID
1766
+ buffer.set(programIdBytes, offset);
1767
+ offset += 32;
1768
+ // Add PDA marker
1769
+ buffer.set(PDA_MARKER, offset);
1770
+ return buffer;
1771
+ }
1772
+ /**
1773
+ * Validate seeds before PDA derivation.
1774
+ */
1775
+ function validateSeeds(seeds) {
1776
+ if (seeds.length > 16) {
1777
+ throw new Error('Maximum of 16 seeds allowed');
1778
+ }
1779
+ for (let i = 0; i < seeds.length; i++) {
1780
+ const seed = seeds[i];
1781
+ if (seed && seed.length > 32) {
1782
+ throw new Error('Seed ' + i + ' exceeds maximum length of 32 bytes');
1783
+ }
1784
+ }
1785
+ }
1786
+ /**
1787
+ * Derives a Program-Derived Address (PDA) from seeds and program ID.
1788
+ *
1789
+ * Algorithm:
1790
+ * 1. For bump = 255 down to 0:
1791
+ * a. Concatenate: seeds + [bump] + programId + "ProgramDerivedAddress"
1792
+ * b. SHA-256 hash the concatenation
1793
+ * c. If result is off the ed25519 curve, return it
1794
+ * 2. If no valid PDA found after 256 attempts, throw error
1795
+ *
1796
+ * @param seeds - Array of seed buffers (max 32 bytes each, max 16 seeds)
1797
+ * @param programId - The program ID (base58 string)
1798
+ * @returns Tuple of [derivedAddress (base58), bumpSeed]
1799
+ */
1800
+ async function findProgramAddress(seeds, programId) {
1801
+ validateSeeds(seeds);
1802
+ const programIdBytes = decodeBase58(programId);
1803
+ if (programIdBytes.length !== 32) {
1804
+ throw new Error('Program ID must be 32 bytes');
1805
+ }
1806
+ // Try bump seeds from 255 down to 0
1807
+ for (let bump = 255; bump >= 0; bump--) {
1808
+ const buffer = buildPdaBuffer(seeds, programIdBytes, bump);
1809
+ const hash = await sha256Async(buffer);
1810
+ {
1811
+ return [encodeBase58(hash), bump];
1812
+ }
1813
+ }
1814
+ throw new Error('Unable to find a valid PDA');
1815
+ }
1816
+ /**
1817
+ * Synchronous version of findProgramAddress.
1818
+ * Uses synchronous SHA-256 (Node.js crypto module).
1819
+ */
1820
+ function findProgramAddressSync(seeds, programId) {
1821
+ validateSeeds(seeds);
1822
+ const programIdBytes = decodeBase58(programId);
1823
+ if (programIdBytes.length !== 32) {
1824
+ throw new Error('Program ID must be 32 bytes');
1825
+ }
1826
+ // Try bump seeds from 255 down to 0
1827
+ for (let bump = 255; bump >= 0; bump--) {
1828
+ const buffer = buildPdaBuffer(seeds, programIdBytes, bump);
1829
+ const hash = sha256Sync(buffer);
1830
+ {
1831
+ return [encodeBase58(hash), bump];
1832
+ }
1833
+ }
1834
+ throw new Error('Unable to find a valid PDA');
1835
+ }
1836
+ /**
1837
+ * Creates a seed buffer from various input types.
1838
+ *
1839
+ * @param value - The value to convert to a seed
1840
+ * @returns Uint8Array suitable for PDA derivation
1841
+ */
1842
+ function createSeed(value) {
1843
+ if (value instanceof Uint8Array) {
1844
+ return value;
1845
+ }
1846
+ if (typeof value === 'string') {
1847
+ return new TextEncoder().encode(value);
1848
+ }
1849
+ if (typeof value === 'bigint') {
1850
+ // Convert bigint to 8-byte buffer (u64 little-endian)
1851
+ const buffer = new Uint8Array(8);
1852
+ let n = value;
1853
+ for (let i = 0; i < 8; i++) {
1854
+ buffer[i] = Number(n & BigInt(0xff));
1855
+ n >>= BigInt(8);
1856
+ }
1857
+ return buffer;
1858
+ }
1859
+ if (typeof value === 'number') {
1860
+ // Assume u64
1861
+ return createSeed(BigInt(value));
1862
+ }
1863
+ throw new Error('Cannot create seed from value');
1864
+ }
1865
+ /**
1866
+ * Creates a public key seed from a base58-encoded address.
1867
+ *
1868
+ * @param address - Base58-encoded public key
1869
+ * @returns 32-byte Uint8Array
1870
+ */
1871
+ function createPublicKeySeed(address) {
1872
+ const decoded = decodeBase58(address);
1873
+ if (decoded.length !== 32) {
1874
+ throw new Error('Invalid public key length: expected 32, got ' + decoded.length);
1875
+ }
1876
+ return decoded;
1877
+ }
1878
+
1879
+ /**
1880
+ * Topologically sort accounts so that dependencies (accountRef) are resolved first.
1881
+ * Non-PDA accounts come first, then PDAs in dependency order.
1882
+ */
1883
+ function sortAccountsByDependency(accountMetas) {
1884
+ // Separate non-PDA and PDA accounts
1885
+ const nonPda = [];
1886
+ const pda = [];
1887
+ for (const meta of accountMetas) {
1888
+ if (meta.category === 'pda') {
1889
+ pda.push(meta);
1890
+ }
1891
+ else {
1892
+ nonPda.push(meta);
1893
+ }
1894
+ }
1895
+ // Build dependency graph for PDAs
1896
+ const pdaDeps = new Map();
1897
+ for (const meta of pda) {
1898
+ const deps = new Set();
1899
+ if (meta.pdaConfig) {
1900
+ for (const seed of meta.pdaConfig.seeds) {
1901
+ if (seed.type === 'accountRef') {
1902
+ deps.add(seed.accountName);
1903
+ }
1904
+ }
1905
+ }
1906
+ pdaDeps.set(meta.name, deps);
1907
+ }
1908
+ // Topological sort PDAs
1909
+ const sortedPda = [];
1910
+ const visited = new Set();
1911
+ const visiting = new Set();
1912
+ function visit(name) {
1913
+ if (visited.has(name))
1914
+ return;
1915
+ if (visiting.has(name)) {
1916
+ throw new Error('Circular dependency in PDA accounts: ' + name);
1917
+ }
1918
+ const meta = pda.find(m => m.name === name);
1919
+ if (!meta)
1920
+ return; // Not a PDA, skip
1921
+ visiting.add(name);
1922
+ const deps = pdaDeps.get(name) || new Set();
1923
+ for (const dep of deps) {
1924
+ // Only visit if dep is also a PDA
1925
+ if (pda.some(m => m.name === dep)) {
1926
+ visit(dep);
1927
+ }
1928
+ }
1929
+ visiting.delete(name);
1930
+ visited.add(name);
1931
+ sortedPda.push(meta);
1932
+ }
1933
+ for (const meta of pda) {
1934
+ visit(meta.name);
1935
+ }
1936
+ return [...nonPda, ...sortedPda];
1937
+ }
1938
+ /**
1939
+ * Resolves instruction accounts by categorizing and deriving addresses.
1940
+ *
1941
+ * Resolution order:
1942
+ * 1. Non-PDA accounts (signer, known, userProvided) are resolved first
1943
+ * 2. PDA accounts are resolved in dependency order (accounts they reference come first)
1944
+ *
1945
+ * @param accountMetas - Account metadata from the instruction definition
1946
+ * @param args - Instruction arguments (used for PDA derivation with argRef seeds)
1947
+ * @param options - Resolution options including wallet, user-provided accounts, and programId
1948
+ * @returns Resolved accounts and any missing required accounts
1949
+ */
1950
+ function resolveAccounts(accountMetas, args, options) {
1951
+ // Sort accounts by dependency
1952
+ const sorted = sortAccountsByDependency(accountMetas);
1953
+ // Track resolved accounts for PDA accountRef lookups
1954
+ const resolvedMap = {};
1955
+ const missing = [];
1956
+ for (const meta of sorted) {
1957
+ const resolvedAccount = resolveSingleAccount(meta, args, options, resolvedMap);
1958
+ if (resolvedAccount) {
1959
+ resolvedMap[meta.name] = resolvedAccount;
1960
+ }
1961
+ else if (!meta.isOptional) {
1962
+ missing.push(meta.name);
1963
+ }
1964
+ }
1965
+ // Return accounts in original order (as defined in accountMetas)
1966
+ const orderedAccounts = [];
1967
+ for (const meta of accountMetas) {
1968
+ const resolved = resolvedMap[meta.name];
1969
+ if (resolved) {
1970
+ orderedAccounts.push(resolved);
1971
+ }
1972
+ }
1973
+ return {
1974
+ accounts: orderedAccounts,
1975
+ missingUserAccounts: missing,
1976
+ };
1977
+ }
1978
+ function resolveSingleAccount(meta, args, options, resolvedMap) {
1979
+ switch (meta.category) {
1980
+ case 'signer':
1981
+ return resolveSignerAccount(meta, options.wallet);
1982
+ case 'known':
1983
+ return resolveKnownAccount(meta);
1984
+ case 'pda':
1985
+ return resolvePdaAccount(meta, args, resolvedMap, options.programId);
1986
+ case 'userProvided':
1987
+ return resolveUserProvidedAccount(meta, options.accounts);
1988
+ default:
1989
+ return null;
1990
+ }
1991
+ }
1992
+ function resolveSignerAccount(meta, wallet) {
1993
+ if (!wallet) {
1994
+ return null;
1995
+ }
1996
+ return {
1997
+ name: meta.name,
1998
+ address: wallet.publicKey,
1999
+ isSigner: true,
2000
+ isWritable: meta.isWritable,
2001
+ };
2002
+ }
2003
+ function resolveKnownAccount(meta) {
2004
+ if (!meta.knownAddress) {
2005
+ return null;
2006
+ }
2007
+ return {
2008
+ name: meta.name,
2009
+ address: meta.knownAddress,
2010
+ isSigner: meta.isSigner,
2011
+ isWritable: meta.isWritable,
2012
+ };
2013
+ }
2014
+ function resolvePdaAccount(meta, args, resolvedMap, programId) {
2015
+ if (!meta.pdaConfig) {
2016
+ return null;
2017
+ }
2018
+ // Determine which program to derive against
2019
+ const pdaProgramId = meta.pdaConfig.programId || programId;
2020
+ if (!pdaProgramId) {
2021
+ throw new Error('Cannot derive PDA for "' + meta.name + '": no programId specified. ' +
2022
+ 'Either set pdaConfig.programId or pass programId in options.');
2023
+ }
2024
+ // Build seeds array
2025
+ const seeds = [];
2026
+ for (const seed of meta.pdaConfig.seeds) {
2027
+ switch (seed.type) {
2028
+ case 'literal':
2029
+ seeds.push(createSeed(seed.value));
2030
+ break;
2031
+ case 'argRef': {
2032
+ const argValue = args[seed.argName];
2033
+ if (argValue === undefined) {
2034
+ throw new Error('PDA seed references missing argument: ' + seed.argName +
2035
+ ' (for account "' + meta.name + '")');
2036
+ }
2037
+ seeds.push(createSeed(argValue));
2038
+ break;
2039
+ }
2040
+ case 'accountRef': {
2041
+ const refAccount = resolvedMap[seed.accountName];
2042
+ if (!refAccount) {
2043
+ throw new Error('PDA seed references unresolved account: ' + seed.accountName +
2044
+ ' (for account "' + meta.name + '")');
2045
+ }
2046
+ // Account addresses are 32 bytes
2047
+ seeds.push(decodeBase58(refAccount.address));
2048
+ break;
2049
+ }
2050
+ default:
2051
+ throw new Error('Unknown seed type');
2052
+ }
2053
+ }
2054
+ // Derive the PDA
2055
+ const [derivedAddress] = findProgramAddressSync(seeds, pdaProgramId);
2056
+ return {
2057
+ name: meta.name,
2058
+ address: derivedAddress,
2059
+ isSigner: meta.isSigner,
2060
+ isWritable: meta.isWritable,
2061
+ };
2062
+ }
2063
+ function resolveUserProvidedAccount(meta, accounts) {
2064
+ const address = accounts?.[meta.name];
2065
+ if (!address) {
2066
+ return null;
2067
+ }
2068
+ return {
2069
+ name: meta.name,
2070
+ address,
2071
+ isSigner: meta.isSigner,
2072
+ isWritable: meta.isWritable,
2073
+ };
2074
+ }
2075
+ /**
2076
+ * Validates that all required accounts are present.
2077
+ *
2078
+ * @param result - Account resolution result
2079
+ * @throws Error if any required accounts are missing
2080
+ */
2081
+ function validateAccountResolution(result) {
2082
+ if (result.missingUserAccounts.length > 0) {
2083
+ throw new Error('Missing required accounts: ' + result.missingUserAccounts.join(', '));
2084
+ }
2085
+ }
2086
+
2087
+ /**
2088
+ * Borsh-compatible instruction data serializer.
2089
+ *
2090
+ * This module handles serializing instruction arguments into the binary format
2091
+ * expected by Solana programs using Borsh serialization.
2092
+ */
2093
+ /**
2094
+ * Serializes instruction arguments into a Buffer using Borsh encoding.
2095
+ *
2096
+ * @param discriminator - The 8-byte instruction discriminator
2097
+ * @param args - Arguments to serialize
2098
+ * @param schema - Schema defining argument types
2099
+ * @returns Serialized instruction data
2100
+ */
2101
+ function serializeInstructionData(discriminator, args, schema) {
2102
+ const buffers = [Buffer.from(discriminator)];
2103
+ for (const field of schema) {
2104
+ const value = args[field.name];
2105
+ const serialized = serializeValue(value, field.type);
2106
+ buffers.push(serialized);
2107
+ }
2108
+ return Buffer.concat(buffers);
2109
+ }
2110
+ function serializeValue(value, type) {
2111
+ if (typeof type === 'string') {
2112
+ return serializePrimitive(value, type);
2113
+ }
2114
+ if ('vec' in type) {
2115
+ return serializeVec(value, type.vec);
2116
+ }
2117
+ if ('option' in type) {
2118
+ return serializeOption(value, type.option);
2119
+ }
2120
+ if ('array' in type) {
2121
+ return serializeArray(value, type.array[0], type.array[1]);
2122
+ }
2123
+ throw new Error(`Unknown type: ${JSON.stringify(type)}`);
2124
+ }
2125
+ function serializePrimitive(value, type) {
2126
+ switch (type) {
2127
+ case 'u8':
2128
+ return Buffer.from([value]);
2129
+ case 'u16':
2130
+ const u16 = Buffer.alloc(2);
2131
+ u16.writeUInt16LE(value, 0);
2132
+ return u16;
2133
+ case 'u32':
2134
+ const u32 = Buffer.alloc(4);
2135
+ u32.writeUInt32LE(value, 0);
2136
+ return u32;
2137
+ case 'u64':
2138
+ const u64 = Buffer.alloc(8);
2139
+ u64.writeBigUInt64LE(BigInt(value), 0);
2140
+ return u64;
2141
+ case 'u128':
2142
+ // u128 is 16 bytes, little-endian
2143
+ const u128 = Buffer.alloc(16);
2144
+ const bigU128 = BigInt(value);
2145
+ u128.writeBigUInt64LE(bigU128 & BigInt('0xFFFFFFFFFFFFFFFF'), 0);
2146
+ u128.writeBigUInt64LE(bigU128 >> BigInt(64), 8);
2147
+ return u128;
2148
+ case 'i8':
2149
+ return Buffer.from([value]);
2150
+ case 'i16':
2151
+ const i16 = Buffer.alloc(2);
2152
+ i16.writeInt16LE(value, 0);
2153
+ return i16;
2154
+ case 'i32':
2155
+ const i32 = Buffer.alloc(4);
2156
+ i32.writeInt32LE(value, 0);
2157
+ return i32;
2158
+ case 'i64':
2159
+ const i64 = Buffer.alloc(8);
2160
+ i64.writeBigInt64LE(BigInt(value), 0);
2161
+ return i64;
2162
+ case 'i128':
2163
+ const i128 = Buffer.alloc(16);
2164
+ const bigI128 = BigInt(value);
2165
+ i128.writeBigInt64LE(bigI128 & BigInt('0xFFFFFFFFFFFFFFFF'), 0);
2166
+ i128.writeBigInt64LE(bigI128 >> BigInt(64), 8);
2167
+ return i128;
2168
+ case 'bool':
2169
+ return Buffer.from([value ? 1 : 0]);
2170
+ case 'string':
2171
+ const str = value;
2172
+ const strBytes = Buffer.from(str, 'utf-8');
2173
+ const strLen = Buffer.alloc(4);
2174
+ strLen.writeUInt32LE(strBytes.length, 0);
2175
+ return Buffer.concat([strLen, strBytes]);
2176
+ case 'pubkey':
2177
+ // Public key is 32 bytes
2178
+ // In production, decode base58 to 32 bytes
2179
+ return Buffer.alloc(32, 0);
2180
+ default:
2181
+ throw new Error(`Unknown primitive type: ${type}`);
2182
+ }
2183
+ }
2184
+ function serializeVec(values, elementType) {
2185
+ const len = Buffer.alloc(4);
2186
+ len.writeUInt32LE(values.length, 0);
2187
+ const elementBuffers = values.map(v => serializeValue(v, elementType));
2188
+ return Buffer.concat([len, ...elementBuffers]);
2189
+ }
2190
+ function serializeOption(value, innerType) {
2191
+ if (value === null || value === undefined) {
2192
+ return Buffer.from([0]); // None
2193
+ }
2194
+ const inner = serializeValue(value, innerType);
2195
+ return Buffer.concat([Buffer.from([1]), inner]); // Some
2196
+ }
2197
+ function serializeArray(values, elementType, length) {
2198
+ if (values.length !== length) {
2199
+ throw new Error(`Array length mismatch: expected ${length}, got ${values.length}`);
2200
+ }
2201
+ const elementBuffers = values.map(v => serializeValue(v, elementType));
2202
+ return Buffer.concat(elementBuffers);
2203
+ }
2204
+
2205
+ /**
2206
+ * Waits for transaction confirmation.
2207
+ *
2208
+ * @param signature - Transaction signature
2209
+ * @param level - Desired confirmation level
2210
+ * @param timeout - Maximum wait time in milliseconds
2211
+ * @returns Confirmation result
2212
+ */
2213
+ async function waitForConfirmation(signature, level = 'confirmed', timeout = 60000) {
2214
+ const startTime = Date.now();
2215
+ while (Date.now() - startTime < timeout) {
2216
+ const status = await checkTransactionStatus();
2217
+ if (status.err) {
2218
+ throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
2219
+ }
2220
+ if (isConfirmationLevelSufficient(status.confirmations, level)) {
2221
+ return {
2222
+ level,
2223
+ slot: status.slot,
2224
+ };
2225
+ }
2226
+ await sleep(1000);
2227
+ }
2228
+ throw new Error(`Transaction confirmation timeout after ${timeout}ms`);
2229
+ }
2230
+ async function checkTransactionStatus(_signature) {
2231
+ // In production, query the Solana RPC
2232
+ return {
2233
+ err: null,
2234
+ confirmations: 32,
2235
+ slot: 123456789,
2236
+ };
2237
+ }
2238
+ function isConfirmationLevelSufficient(confirmations, level) {
2239
+ if (confirmations === null) {
2240
+ return false;
2241
+ }
2242
+ switch (level) {
2243
+ case 'processed':
2244
+ return confirmations >= 0;
2245
+ case 'confirmed':
2246
+ return confirmations >= 1;
2247
+ case 'finalized':
2248
+ return confirmations >= 32;
2249
+ default:
2250
+ return false;
2251
+ }
2252
+ }
2253
+ function sleep(ms) {
2254
+ return new Promise(resolve => setTimeout(resolve, ms));
2255
+ }
2256
+
2257
+ /**
2258
+ * Parses and handles instruction errors.
2259
+ */
2260
+ /**
2261
+ * Parses an error returned from a Solana transaction.
2262
+ *
2263
+ * @param error - The error from the transaction
2264
+ * @param errorMetadata - Error definitions from the IDL
2265
+ * @returns Parsed program error or null if not a program error
2266
+ */
2267
+ function parseInstructionError(error, errorMetadata) {
2268
+ if (!error) {
2269
+ return null;
2270
+ }
2271
+ const errorCode = extractErrorCode(error);
2272
+ if (errorCode === null) {
2273
+ return null;
2274
+ }
2275
+ const metadata = errorMetadata.find(e => e.code === errorCode);
2276
+ if (metadata) {
2277
+ return {
2278
+ code: metadata.code,
2279
+ name: metadata.name,
2280
+ message: metadata.msg,
2281
+ };
2282
+ }
2283
+ return {
2284
+ code: errorCode,
2285
+ name: `CustomError${errorCode}`,
2286
+ message: `Unknown error with code ${errorCode}`,
2287
+ };
2288
+ }
2289
+ function extractErrorCode(error) {
2290
+ if (typeof error !== 'object' || error === null) {
2291
+ return null;
2292
+ }
2293
+ const errorObj = error;
2294
+ // Check for InstructionError format
2295
+ if (errorObj.InstructionError) {
2296
+ const instructionError = errorObj.InstructionError;
2297
+ if (instructionError[1]?.Custom !== undefined) {
2298
+ return instructionError[1].Custom;
2299
+ }
2300
+ }
2301
+ // Check for direct code
2302
+ if (typeof errorObj.code === 'number') {
2303
+ return errorObj.code;
2304
+ }
2305
+ return null;
2306
+ }
2307
+ /**
2308
+ * Formats an error for display.
2309
+ *
2310
+ * @param error - The program error
2311
+ * @returns Human-readable error message
2312
+ */
2313
+ function formatProgramError(error) {
2314
+ return `${error.name} (${error.code}): ${error.message}`;
2315
+ }
2316
+
2317
+ /**
2318
+ * Converts resolved account array to a map for the builder.
2319
+ */
2320
+ function toResolvedAccountsMap(accounts) {
2321
+ const map = {};
2322
+ for (const account of accounts) {
2323
+ map[account.name] = account.address;
2324
+ }
2325
+ return map;
2326
+ }
2327
+ /**
2328
+ * Executes an instruction handler with the given arguments and options.
2329
+ *
2330
+ * This is the main function for executing Solana instructions. It handles:
2331
+ * 1. Account resolution (signer, PDA, user-provided)
2332
+ * 2. Calling the generated build() function
2333
+ * 3. Transaction signing and sending
2334
+ * 4. Confirmation waiting
2335
+ *
2336
+ * @param handler - Instruction handler from generated SDK
2337
+ * @param args - Instruction arguments
2338
+ * @param options - Execution options
2339
+ * @returns Execution result with signature
2340
+ */
2341
+ async function executeInstruction(handler, args, options = {}) {
2342
+ // Step 1: Resolve accounts using handler's account metadata
2343
+ const resolutionOptions = {
2344
+ accounts: options.accounts,
2345
+ wallet: options.wallet,
2346
+ programId: handler.programId, // Pass programId for PDA derivation
2347
+ };
2348
+ const resolution = resolveAccounts(handler.accounts, args, resolutionOptions);
2349
+ validateAccountResolution(resolution);
2350
+ // Step 2: Call generated build() function
2351
+ const resolvedAccountsMap = toResolvedAccountsMap(resolution.accounts);
2352
+ const instruction = handler.build(args, resolvedAccountsMap);
2353
+ // Step 3: Build transaction from the built instruction
2354
+ const transaction = buildTransaction(instruction);
2355
+ // Step 4: Sign and send
2356
+ if (!options.wallet) {
2357
+ throw new Error('Wallet required to sign transaction');
2358
+ }
2359
+ const signature = await options.wallet.signAndSend(transaction);
2360
+ // Step 5: Wait for confirmation
2361
+ const confirmationLevel = options.confirmationLevel ?? 'confirmed';
2362
+ const timeout = options.timeout ?? 60000;
2363
+ const confirmation = await waitForConfirmation(signature, confirmationLevel, timeout);
2364
+ return {
2365
+ signature,
2366
+ confirmationLevel: confirmation.level,
2367
+ slot: confirmation.slot,
2368
+ };
2369
+ }
2370
+ /**
2371
+ * Creates a transaction object from a built instruction.
2372
+ *
2373
+ * @param instruction - Built instruction from handler
2374
+ * @returns Transaction object ready for signing
2375
+ */
2376
+ function buildTransaction(instruction) {
2377
+ // This returns a framework-agnostic transaction representation.
2378
+ // The wallet adapter is responsible for converting this to the
2379
+ // appropriate format (@solana/web3.js Transaction, etc.)
2380
+ return {
2381
+ instructions: [{
2382
+ programId: instruction.programId,
2383
+ keys: instruction.keys,
2384
+ data: Array.from(instruction.data),
2385
+ }],
2386
+ };
2387
+ }
2388
+ /**
2389
+ * Creates an instruction executor bound to a specific wallet.
2390
+ *
2391
+ * @param wallet - Wallet adapter
2392
+ * @returns Bound executor function
2393
+ */
2394
+ function createInstructionExecutor(wallet) {
2395
+ return {
2396
+ execute: async (handler, args, options) => {
2397
+ return executeInstruction(handler, args, {
2398
+ ...options,
2399
+ wallet,
2400
+ });
2401
+ },
2402
+ };
2403
+ }
2404
+
2405
+ function literal(value) {
2406
+ return { type: 'literal', value };
2407
+ }
2408
+ function account(name) {
2409
+ return { type: 'accountRef', accountName: name };
2410
+ }
2411
+ function arg(name, type) {
2412
+ return { type: 'argRef', argName: name, argType: type };
2413
+ }
2414
+ function bytes(value) {
2415
+ return { type: 'bytes', value };
2416
+ }
2417
+ function resolveSeeds(seeds, context) {
2418
+ return seeds.map((seed) => {
2419
+ switch (seed.type) {
2420
+ case 'literal':
2421
+ return new TextEncoder().encode(seed.value);
2422
+ case 'bytes':
2423
+ return seed.value;
2424
+ case 'argRef': {
2425
+ const value = context.args?.[seed.argName];
2426
+ if (value === undefined) {
2427
+ throw new Error(`Missing arg for PDA seed: ${seed.argName}`);
2428
+ }
2429
+ return serializeArgForSeed(value, seed.argType);
2430
+ }
2431
+ case 'accountRef': {
2432
+ const address = context.accounts?.[seed.accountName];
2433
+ if (!address) {
2434
+ throw new Error(`Missing account for PDA seed: ${seed.accountName}`);
2435
+ }
2436
+ return decodeBase58(address);
2437
+ }
2438
+ }
2439
+ });
2440
+ }
2441
+ function serializeArgForSeed(value, argType) {
2442
+ if (value instanceof Uint8Array) {
2443
+ return value;
2444
+ }
2445
+ if (typeof value === 'string') {
2446
+ if (value.length === 43 || value.length === 44) {
2447
+ try {
2448
+ return decodeBase58(value);
2449
+ }
2450
+ catch {
2451
+ return new TextEncoder().encode(value);
2452
+ }
2453
+ }
2454
+ return new TextEncoder().encode(value);
2455
+ }
2456
+ if (typeof value === 'bigint' || typeof value === 'number') {
2457
+ const size = getArgSize(argType);
2458
+ return serializeNumber(value, size);
2459
+ }
2460
+ throw new Error(`Cannot serialize value for PDA seed: ${typeof value}`);
2461
+ }
2462
+ function getArgSize(argType) {
2463
+ if (!argType)
2464
+ return 8;
2465
+ const match = argType.match(/^[ui](\d+)$/);
2466
+ if (match && match[1]) {
2467
+ return parseInt(match[1], 10) / 8;
2468
+ }
2469
+ if (argType === 'pubkey')
2470
+ return 32;
2471
+ return 8;
2472
+ }
2473
+ function serializeNumber(value, size) {
2474
+ const buffer = new Uint8Array(size);
2475
+ let n = typeof value === 'bigint' ? value : BigInt(value);
2476
+ for (let i = 0; i < size; i++) {
2477
+ buffer[i] = Number(n & BigInt(0xff));
2478
+ n >>= BigInt(8);
2479
+ }
2480
+ return buffer;
2481
+ }
2482
+ function pda(programId, ...seeds) {
2483
+ return {
2484
+ seeds,
2485
+ programId,
2486
+ program(newProgramId) {
2487
+ return pda(newProgramId, ...seeds);
2488
+ },
2489
+ async derive(context) {
2490
+ const resolvedSeeds = resolveSeeds(this.seeds, context);
2491
+ const pid = context.programId ?? this.programId;
2492
+ const [address] = await findProgramAddress(resolvedSeeds, pid);
2493
+ return address;
2494
+ },
2495
+ deriveSync(context) {
2496
+ const resolvedSeeds = resolveSeeds(this.seeds, context);
2497
+ const pid = context.programId ?? this.programId;
2498
+ const [address] = findProgramAddressSync(resolvedSeeds, pid);
2499
+ return address;
2500
+ },
2501
+ };
2502
+ }
2503
+ function createProgramPdas(pdas) {
2504
+ return pdas;
2505
+ }
2506
+
2507
+ class Arete {
2508
+ constructor(url, options) {
2509
+ this.stack = options.stack;
2510
+ this.storage = new SortedStorageDecorator(options.storage ?? new MemoryAdapter());
2511
+ this.processor = new FrameProcessor(this.storage, {
2512
+ maxEntriesPerView: options.maxEntriesPerView,
2513
+ flushIntervalMs: options.flushIntervalMs,
2514
+ schemas: options.validateFrames ? this.stack.schemas : undefined,
2515
+ });
2516
+ this.connection = new ConnectionManager({
2517
+ websocketUrl: url,
2518
+ reconnectIntervals: options.reconnectIntervals,
2519
+ maxReconnectAttempts: options.maxReconnectAttempts,
2520
+ auth: options.auth,
2521
+ });
2522
+ this.subscriptionRegistry = new SubscriptionRegistry(this.connection);
2523
+ this.connection.onFrame((frame) => {
2524
+ this.processor.handleFrame(frame);
2525
+ });
2526
+ this._views = createTypedViews(this.stack, this.storage, this.subscriptionRegistry);
2527
+ this._instructions = this.buildInstructions();
2528
+ }
2529
+ buildInstructions() {
2530
+ const instructions = {};
2531
+ if (this.stack.instructions) {
2532
+ for (const [name, handler] of Object.entries(this.stack.instructions)) {
2533
+ instructions[name] = (args, options) => {
2534
+ return executeInstruction(handler, args, options);
2535
+ };
2536
+ }
2537
+ }
2538
+ return instructions;
2539
+ }
2540
+ static async connect(stack, options) {
2541
+ const url = options?.url ?? stack.url;
2542
+ if (!url) {
2543
+ throw new AreteError('URL is required (provide url option or define url in stack)', 'INVALID_CONFIG');
2544
+ }
2545
+ const internalOptions = {
2546
+ stack,
2547
+ storage: options?.storage,
2548
+ maxEntriesPerView: options?.maxEntriesPerView,
2549
+ flushIntervalMs: options?.flushIntervalMs,
2550
+ autoReconnect: options?.autoReconnect,
2551
+ reconnectIntervals: options?.reconnectIntervals,
2552
+ maxReconnectAttempts: options?.maxReconnectAttempts,
2553
+ validateFrames: options?.validateFrames,
2554
+ auth: options?.auth,
2555
+ };
2556
+ const client = new Arete(url, internalOptions);
2557
+ if (options?.autoReconnect !== false) {
2558
+ await client.connection.connect();
2559
+ }
2560
+ return client;
2561
+ }
2562
+ get views() {
2563
+ return this._views;
2564
+ }
2565
+ get instructions() {
2566
+ return this._instructions;
2567
+ }
2568
+ get connectionState() {
2569
+ return this.connection.getState();
2570
+ }
2571
+ get stackName() {
2572
+ return this.stack.name;
2573
+ }
2574
+ get store() {
2575
+ return this.storage;
2576
+ }
2577
+ onConnectionStateChange(callback) {
2578
+ return this.connection.onStateChange(callback);
2579
+ }
2580
+ onFrame(callback) {
2581
+ return this.connection.onFrame(callback);
2582
+ }
2583
+ onSocketIssue(callback) {
2584
+ return this.connection.onSocketIssue(callback);
2585
+ }
2586
+ async connect() {
2587
+ await this.connection.connect();
2588
+ }
2589
+ disconnect() {
2590
+ this.subscriptionRegistry.clear();
2591
+ this.connection.disconnect();
2592
+ }
2593
+ isConnected() {
2594
+ return this.connection.isConnected();
2595
+ }
2596
+ clearStore() {
2597
+ this.storage.clear();
2598
+ }
2599
+ getStore() {
2600
+ return this.storage;
2601
+ }
2602
+ getConnection() {
2603
+ return this.connection;
2604
+ }
2605
+ getSubscriptionRegistry() {
2606
+ return this.subscriptionRegistry;
2607
+ }
2608
+ }
2609
+
2610
+ function getNestedValue(obj, path) {
2611
+ let current = obj;
2612
+ for (const segment of path) {
2613
+ if (current === null || current === undefined)
2614
+ return undefined;
2615
+ if (typeof current !== 'object')
2616
+ return undefined;
2617
+ current = current[segment];
2618
+ }
2619
+ return current;
2620
+ }
2621
+ function compareSortValues(a, b) {
2622
+ if (a === b)
2623
+ return 0;
2624
+ if (a === undefined || a === null)
2625
+ return -1;
2626
+ if (b === undefined || b === null)
2627
+ return 1;
2628
+ if (typeof a === 'number' && typeof b === 'number') {
2629
+ return a - b;
2630
+ }
2631
+ if (typeof a === 'string' && typeof b === 'string') {
2632
+ return a.localeCompare(b);
2633
+ }
2634
+ if (typeof a === 'boolean' && typeof b === 'boolean') {
2635
+ return (a ? 1 : 0) - (b ? 1 : 0);
2636
+ }
2637
+ return String(a).localeCompare(String(b));
2638
+ }
2639
+ class ViewData {
2640
+ constructor(sortConfig) {
2641
+ this.entities = new Map();
2642
+ this.accessOrder = [];
2643
+ this.sortedKeys = [];
2644
+ this.sortConfig = sortConfig;
2645
+ }
2646
+ get(key) {
2647
+ return this.entities.get(key);
2648
+ }
2649
+ set(key, value) {
2650
+ const isNew = !this.entities.has(key);
2651
+ this.entities.set(key, value);
2652
+ if (this.sortConfig) {
2653
+ this.updateSortedPosition(key, value, isNew);
2654
+ }
2655
+ else {
2656
+ if (isNew) {
2657
+ this.accessOrder.push(key);
2658
+ }
2659
+ else {
2660
+ this.touch(key);
2661
+ }
2662
+ }
2663
+ }
2664
+ updateSortedPosition(key, value, isNew) {
2665
+ if (!isNew) {
2666
+ const existingIdx = this.sortedKeys.indexOf(key);
2667
+ if (existingIdx !== -1) {
2668
+ this.sortedKeys.splice(existingIdx, 1);
2669
+ }
2670
+ }
2671
+ const sortValue = getNestedValue(value, this.sortConfig.field);
2672
+ const isDesc = this.sortConfig.order === 'desc';
2673
+ let insertIdx = this.binarySearchInsertPosition(sortValue, key, isDesc);
2674
+ this.sortedKeys.splice(insertIdx, 0, key);
2675
+ }
2676
+ binarySearchInsertPosition(sortValue, key, isDesc) {
2677
+ let low = 0;
2678
+ let high = this.sortedKeys.length;
2679
+ while (low < high) {
2680
+ const mid = Math.floor((low + high) / 2);
2681
+ const midKey = this.sortedKeys[mid];
2682
+ if (midKey === undefined)
2683
+ break;
2684
+ const midEntity = this.entities.get(midKey);
2685
+ const midValue = getNestedValue(midEntity, this.sortConfig.field);
2686
+ let cmp = compareSortValues(sortValue, midValue);
2687
+ if (isDesc)
2688
+ cmp = -cmp;
2689
+ if (cmp === 0) {
2690
+ cmp = key.localeCompare(midKey);
2691
+ }
2692
+ if (cmp < 0) {
2693
+ high = mid;
2694
+ }
2695
+ else {
2696
+ low = mid + 1;
2697
+ }
2698
+ }
2699
+ return low;
2700
+ }
2701
+ delete(key) {
2702
+ if (this.sortConfig) {
2703
+ const idx = this.sortedKeys.indexOf(key);
2704
+ if (idx !== -1) {
2705
+ this.sortedKeys.splice(idx, 1);
2706
+ }
2707
+ }
2708
+ else {
2709
+ const idx = this.accessOrder.indexOf(key);
2710
+ if (idx !== -1) {
2711
+ this.accessOrder.splice(idx, 1);
2712
+ }
2713
+ }
2714
+ return this.entities.delete(key);
2715
+ }
2716
+ has(key) {
2717
+ return this.entities.has(key);
2718
+ }
2719
+ values() {
2720
+ if (this.sortConfig) {
2721
+ return this.sortedKeys.map(k => this.entities.get(k));
2722
+ }
2723
+ return Array.from(this.entities.values());
2724
+ }
2725
+ keys() {
2726
+ if (this.sortConfig) {
2727
+ return [...this.sortedKeys];
2728
+ }
2729
+ return Array.from(this.entities.keys());
2730
+ }
2731
+ get size() {
2732
+ return this.entities.size;
2733
+ }
2734
+ touch(key) {
2735
+ if (this.sortConfig)
2736
+ return;
2737
+ const idx = this.accessOrder.indexOf(key);
2738
+ if (idx !== -1) {
2739
+ this.accessOrder.splice(idx, 1);
2740
+ this.accessOrder.push(key);
2741
+ }
2742
+ }
2743
+ evictOldest() {
2744
+ if (this.sortConfig) {
2745
+ const oldest = this.sortedKeys.pop();
2746
+ if (oldest !== undefined) {
2747
+ this.entities.delete(oldest);
2748
+ }
2749
+ return oldest;
2750
+ }
2751
+ const oldest = this.accessOrder.shift();
2752
+ if (oldest !== undefined) {
2753
+ this.entities.delete(oldest);
2754
+ }
2755
+ return oldest;
2756
+ }
2757
+ setSortConfig(config) {
2758
+ if (this.sortConfig)
2759
+ return;
2760
+ this.sortConfig = config;
2761
+ this.rebuildSortedKeys();
2762
+ }
2763
+ rebuildSortedKeys() {
2764
+ if (!this.sortConfig)
2765
+ return;
2766
+ const entries = Array.from(this.entities.entries());
2767
+ const isDesc = this.sortConfig.order === 'desc';
2768
+ entries.sort((a, b) => {
2769
+ const aValue = getNestedValue(a[1], this.sortConfig.field);
2770
+ const bValue = getNestedValue(b[1], this.sortConfig.field);
2771
+ let cmp = compareSortValues(aValue, bValue);
2772
+ if (isDesc)
2773
+ cmp = -cmp;
2774
+ if (cmp === 0) {
2775
+ cmp = a[0].localeCompare(b[0]);
2776
+ }
2777
+ return cmp;
2778
+ });
2779
+ this.sortedKeys = entries.map(([k]) => k);
2780
+ this.accessOrder = [];
2781
+ }
2782
+ getSortConfig() {
2783
+ return this.sortConfig;
2784
+ }
2785
+ }
2786
+ function isObject(item) {
2787
+ return item !== null && typeof item === 'object' && !Array.isArray(item);
2788
+ }
2789
+ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
2790
+ if (!isObject(target) || !isObject(source)) {
2791
+ return source;
2792
+ }
2793
+ const result = { ...target };
2794
+ for (const key in source) {
2795
+ const sourceValue = source[key];
2796
+ const targetValue = result[key];
2797
+ const fieldPath = currentPath ? `${currentPath}.${key}` : key;
2798
+ if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
2799
+ if (appendPaths.includes(fieldPath)) {
2800
+ result[key] = [...targetValue, ...sourceValue];
2801
+ }
2802
+ else {
2803
+ result[key] = sourceValue;
2804
+ }
2805
+ }
2806
+ else if (isObject(sourceValue) && isObject(targetValue)) {
2807
+ result[key] = deepMergeWithAppend(targetValue, sourceValue, appendPaths, fieldPath);
2808
+ }
2809
+ else {
2810
+ result[key] = sourceValue;
2811
+ }
2812
+ }
2813
+ return result;
2814
+ }
2815
+ class EntityStore {
2816
+ constructor(config = {}) {
2817
+ this.views = new Map();
2818
+ this.viewConfigs = new Map();
2819
+ this.updateCallbacks = new Set();
2820
+ this.richUpdateCallbacks = new Set();
2821
+ this.maxEntriesPerView = config.maxEntriesPerView === undefined
2822
+ ? DEFAULT_MAX_ENTRIES_PER_VIEW
2823
+ : config.maxEntriesPerView;
2824
+ }
2825
+ enforceMaxEntries(viewData) {
2826
+ if (this.maxEntriesPerView === null)
2827
+ return;
2828
+ while (viewData.size > this.maxEntriesPerView) {
2829
+ viewData.evictOldest();
2830
+ }
2831
+ }
2832
+ handleFrame(frame) {
2833
+ if (isSubscribedFrame(frame)) {
2834
+ this.handleSubscribedFrame(frame);
2835
+ return;
2836
+ }
2837
+ if (isSnapshotFrame(frame)) {
2838
+ this.handleSnapshotFrame(frame);
2839
+ return;
2840
+ }
2841
+ this.handleEntityFrame(frame);
2842
+ }
2843
+ handleSubscribedFrame(frame) {
2844
+ const viewPath = frame.view;
2845
+ const config = {};
2846
+ if (frame.sort) {
2847
+ config.sort = frame.sort;
2848
+ }
2849
+ this.viewConfigs.set(viewPath, config);
2850
+ const existingView = this.views.get(viewPath);
2851
+ if (existingView && frame.sort) {
2852
+ existingView.setSortConfig(frame.sort);
2853
+ }
2854
+ }
2855
+ handleSnapshotFrame(frame) {
2856
+ const viewPath = frame.entity;
2857
+ let viewData = this.views.get(viewPath);
2858
+ const viewConfig = this.viewConfigs.get(viewPath);
2859
+ if (!viewData) {
2860
+ viewData = new ViewData(viewConfig?.sort);
2861
+ this.views.set(viewPath, viewData);
2862
+ }
2863
+ for (const entity of frame.data) {
2864
+ const previousValue = viewData.get(entity.key);
2865
+ viewData.set(entity.key, entity.data);
2866
+ this.notifyUpdate(viewPath, entity.key, {
2867
+ type: 'upsert',
2868
+ key: entity.key,
2869
+ data: entity.data,
2870
+ });
2871
+ this.notifyRichUpdate(viewPath, entity.key, previousValue, entity.data, 'upsert');
2872
+ }
2873
+ this.enforceMaxEntries(viewData);
2874
+ }
2875
+ handleEntityFrame(frame) {
2876
+ const viewPath = frame.entity;
2877
+ let viewData = this.views.get(viewPath);
2878
+ const viewConfig = this.viewConfigs.get(viewPath);
2879
+ if (!viewData) {
2880
+ viewData = new ViewData(viewConfig?.sort);
2881
+ this.views.set(viewPath, viewData);
2882
+ }
2883
+ const previousValue = viewData.get(frame.key);
2884
+ switch (frame.op) {
2885
+ case 'create':
2886
+ case 'upsert':
2887
+ viewData.set(frame.key, frame.data);
2888
+ this.enforceMaxEntries(viewData);
2889
+ this.notifyUpdate(viewPath, frame.key, {
2890
+ type: 'upsert',
2891
+ key: frame.key,
2892
+ data: frame.data,
2893
+ });
2894
+ this.notifyRichUpdate(viewPath, frame.key, previousValue, frame.data, frame.op);
2895
+ break;
2896
+ case 'patch': {
2897
+ const existing = viewData.get(frame.key);
2898
+ const appendPaths = frame.append ?? [];
2899
+ const merged = existing
2900
+ ? deepMergeWithAppend(existing, frame.data, appendPaths)
2901
+ : frame.data;
2902
+ viewData.set(frame.key, merged);
2903
+ this.enforceMaxEntries(viewData);
2904
+ this.notifyUpdate(viewPath, frame.key, {
2905
+ type: 'patch',
2906
+ key: frame.key,
2907
+ data: frame.data,
2908
+ });
2909
+ this.notifyRichUpdate(viewPath, frame.key, previousValue, merged, 'patch', frame.data);
2910
+ break;
2911
+ }
2912
+ case 'delete':
2913
+ viewData.delete(frame.key);
2914
+ this.notifyUpdate(viewPath, frame.key, {
2915
+ type: 'delete',
2916
+ key: frame.key,
2917
+ });
2918
+ if (previousValue !== undefined) {
2919
+ this.notifyRichDelete(viewPath, frame.key, previousValue);
2920
+ }
2921
+ break;
2922
+ }
2923
+ }
2924
+ getAll(viewPath) {
2925
+ const viewData = this.views.get(viewPath);
2926
+ if (!viewData)
2927
+ return [];
2928
+ return viewData.values();
2929
+ }
2930
+ get(viewPath, key) {
2931
+ const viewData = this.views.get(viewPath);
2932
+ if (!viewData)
2933
+ return null;
2934
+ const value = viewData.get(key);
2935
+ return value !== undefined ? value : null;
2936
+ }
2937
+ getAllSync(viewPath) {
2938
+ const viewData = this.views.get(viewPath);
2939
+ if (!viewData)
2940
+ return undefined;
2941
+ return viewData.values();
2942
+ }
2943
+ getSync(viewPath, key) {
2944
+ const viewData = this.views.get(viewPath);
2945
+ if (!viewData)
2946
+ return undefined;
2947
+ const value = viewData.get(key);
2948
+ return value !== undefined ? value : null;
2949
+ }
2950
+ keys(viewPath) {
2951
+ const viewData = this.views.get(viewPath);
2952
+ if (!viewData)
2953
+ return [];
2954
+ return viewData.keys();
2955
+ }
2956
+ size(viewPath) {
2957
+ const viewData = this.views.get(viewPath);
2958
+ return viewData?.size ?? 0;
2959
+ }
2960
+ clear() {
2961
+ this.views.clear();
2962
+ }
2963
+ clearView(viewPath) {
2964
+ this.views.delete(viewPath);
2965
+ this.viewConfigs.delete(viewPath);
2966
+ }
2967
+ getViewConfig(viewPath) {
2968
+ return this.viewConfigs.get(viewPath);
2969
+ }
2970
+ setViewConfig(viewPath, config) {
2971
+ this.viewConfigs.set(viewPath, config);
2972
+ const existingView = this.views.get(viewPath);
2973
+ if (existingView && config.sort) {
2974
+ existingView.setSortConfig(config.sort);
2975
+ }
2976
+ }
2977
+ onUpdate(callback) {
2978
+ this.updateCallbacks.add(callback);
2979
+ return () => {
2980
+ this.updateCallbacks.delete(callback);
2981
+ };
2982
+ }
2983
+ onRichUpdate(callback) {
2984
+ this.richUpdateCallbacks.add(callback);
2985
+ return () => {
2986
+ this.richUpdateCallbacks.delete(callback);
2987
+ };
2988
+ }
2989
+ subscribe(viewPath, callback) {
2990
+ const handler = (path, _key, update) => {
2991
+ if (path === viewPath) {
2992
+ callback(update);
2993
+ }
2994
+ };
2995
+ this.updateCallbacks.add(handler);
2996
+ return () => {
2997
+ this.updateCallbacks.delete(handler);
2998
+ };
2999
+ }
3000
+ subscribeToKey(viewPath, key, callback) {
3001
+ const handler = (path, updateKey, update) => {
3002
+ if (path === viewPath && updateKey === key) {
3003
+ callback(update);
3004
+ }
3005
+ };
3006
+ this.updateCallbacks.add(handler);
3007
+ return () => {
3008
+ this.updateCallbacks.delete(handler);
3009
+ };
3010
+ }
3011
+ notifyUpdate(viewPath, key, update) {
3012
+ for (const callback of this.updateCallbacks) {
3013
+ callback(viewPath, key, update);
3014
+ }
3015
+ }
3016
+ notifyRichUpdate(viewPath, key, before, after, _op, patch) {
3017
+ const richUpdate = before === undefined
3018
+ ? { type: 'created', key, data: after }
3019
+ : { type: 'updated', key, before, after, patch };
3020
+ for (const callback of this.richUpdateCallbacks) {
3021
+ callback(viewPath, key, richUpdate);
3022
+ }
3023
+ }
3024
+ notifyRichDelete(viewPath, key, lastKnown) {
3025
+ const richUpdate = { type: 'deleted', key, lastKnown };
3026
+ for (const callback of this.richUpdateCallbacks) {
3027
+ callback(viewPath, key, richUpdate);
3028
+ }
3029
+ }
3030
+ }
3031
+
3032
+ exports.Arete = Arete;
3033
+ exports.AreteError = AreteError;
3034
+ exports.ConnectionManager = ConnectionManager;
3035
+ exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
3036
+ exports.DEFAULT_MAX_ENTRIES_PER_VIEW = DEFAULT_MAX_ENTRIES_PER_VIEW;
3037
+ exports.EntityStore = EntityStore;
3038
+ exports.FrameProcessor = FrameProcessor;
3039
+ exports.MemoryAdapter = MemoryAdapter;
3040
+ exports.SubscriptionRegistry = SubscriptionRegistry;
3041
+ exports.account = account;
3042
+ exports.arg = arg;
3043
+ exports.bytes = bytes;
3044
+ exports.createEntityStream = createEntityStream;
3045
+ exports.createInstructionExecutor = createInstructionExecutor;
3046
+ exports.createProgramPdas = createProgramPdas;
3047
+ exports.createPublicKeySeed = createPublicKeySeed;
3048
+ exports.createRichUpdateStream = createRichUpdateStream;
3049
+ exports.createSeed = createSeed;
3050
+ exports.createTypedListView = createTypedListView;
3051
+ exports.createTypedStateView = createTypedStateView;
3052
+ exports.createTypedViews = createTypedViews;
3053
+ exports.createUpdateStream = createUpdateStream;
3054
+ exports.decodeBase58 = decodeBase58;
3055
+ exports.derivePda = findProgramAddress;
3056
+ exports.encodeBase58 = encodeBase58;
3057
+ exports.executeInstruction = executeInstruction;
3058
+ exports.findProgramAddress = findProgramAddress;
3059
+ exports.findProgramAddressSync = findProgramAddressSync;
3060
+ exports.formatProgramError = formatProgramError;
3061
+ exports.isEntityFrame = isEntityFrame;
3062
+ exports.isSnapshotFrame = isSnapshotFrame;
3063
+ exports.isSubscribedFrame = isSubscribedFrame;
3064
+ exports.isValidFrame = isValidFrame;
3065
+ exports.literal = literal;
3066
+ exports.parseFrame = parseFrame;
3067
+ exports.parseFrameFromBlob = parseFrameFromBlob;
3068
+ exports.parseInstructionError = parseInstructionError;
3069
+ exports.pda = pda;
3070
+ exports.resolveAccounts = resolveAccounts;
3071
+ exports.serializeInstructionData = serializeInstructionData;
3072
+ exports.validateAccountResolution = validateAccountResolution;
3073
+ exports.waitForConfirmation = waitForConfirmation;
3074
+ //# sourceMappingURL=index.js.map