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