hyperstack-typescript 0.2.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.
@@ -0,0 +1,691 @@
1
+ const DEFAULT_CONFIG = {
2
+ reconnectIntervals: [1000, 2000, 4000, 8000, 16000],
3
+ maxReconnectAttempts: 5,
4
+ };
5
+ class HyperStackError extends Error {
6
+ constructor(message, code, details) {
7
+ super(message);
8
+ this.code = code;
9
+ this.details = details;
10
+ this.name = 'HyperStackError';
11
+ }
12
+ }
13
+
14
+ function parseFrame(data) {
15
+ if (typeof data === 'string') {
16
+ return JSON.parse(data);
17
+ }
18
+ const decoder = new TextDecoder('utf-8');
19
+ const jsonString = decoder.decode(data);
20
+ return JSON.parse(jsonString);
21
+ }
22
+ async function parseFrameFromBlob(blob) {
23
+ const arrayBuffer = await blob.arrayBuffer();
24
+ return parseFrame(arrayBuffer);
25
+ }
26
+ function isValidFrame(frame) {
27
+ if (typeof frame !== 'object' || frame === null) {
28
+ return false;
29
+ }
30
+ const f = frame;
31
+ return (typeof f['entity'] === 'string' &&
32
+ typeof f['key'] === 'string' &&
33
+ typeof f['op'] === 'string' &&
34
+ ['create', 'upsert', 'patch', 'delete'].includes(f['op']) &&
35
+ typeof f['mode'] === 'string' &&
36
+ ['state', 'append', 'list'].includes(f['mode']));
37
+ }
38
+
39
+ class ConnectionManager {
40
+ constructor(config) {
41
+ this.ws = null;
42
+ this.reconnectAttempts = 0;
43
+ this.reconnectTimeout = null;
44
+ this.pingInterval = null;
45
+ this.currentState = 'disconnected';
46
+ this.subscriptionQueue = [];
47
+ this.activeSubscriptions = new Set();
48
+ this.frameHandlers = new Set();
49
+ this.stateHandlers = new Set();
50
+ if (!config.websocketUrl) {
51
+ throw new HyperStackError('websocketUrl is required', 'INVALID_CONFIG');
52
+ }
53
+ this.websocketUrl = config.websocketUrl;
54
+ this.reconnectIntervals = config.reconnectIntervals ?? DEFAULT_CONFIG.reconnectIntervals;
55
+ this.maxReconnectAttempts = config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts;
56
+ if (config.initialSubscriptions) {
57
+ this.subscriptionQueue.push(...config.initialSubscriptions);
58
+ }
59
+ }
60
+ getState() {
61
+ return this.currentState;
62
+ }
63
+ onFrame(handler) {
64
+ this.frameHandlers.add(handler);
65
+ return () => {
66
+ this.frameHandlers.delete(handler);
67
+ };
68
+ }
69
+ onStateChange(handler) {
70
+ this.stateHandlers.add(handler);
71
+ return () => {
72
+ this.stateHandlers.delete(handler);
73
+ };
74
+ }
75
+ connect() {
76
+ return new Promise((resolve, reject) => {
77
+ if (this.ws?.readyState === WebSocket.OPEN ||
78
+ this.ws?.readyState === WebSocket.CONNECTING ||
79
+ this.currentState === 'connecting') {
80
+ resolve();
81
+ return;
82
+ }
83
+ this.updateState('connecting');
84
+ try {
85
+ this.ws = new WebSocket(this.websocketUrl);
86
+ this.ws.onopen = () => {
87
+ this.reconnectAttempts = 0;
88
+ this.updateState('connected');
89
+ this.startPingInterval();
90
+ this.flushSubscriptionQueue();
91
+ this.resubscribeActive();
92
+ resolve();
93
+ };
94
+ this.ws.onmessage = async (event) => {
95
+ try {
96
+ let frame;
97
+ if (event.data instanceof ArrayBuffer) {
98
+ frame = parseFrame(event.data);
99
+ }
100
+ else if (event.data instanceof Blob) {
101
+ frame = await parseFrameFromBlob(event.data);
102
+ }
103
+ else if (typeof event.data === 'string') {
104
+ frame = parseFrame(event.data);
105
+ }
106
+ else {
107
+ throw new HyperStackError(`Unsupported message type: ${typeof event.data}`, 'PARSE_ERROR');
108
+ }
109
+ this.notifyFrameHandlers(frame);
110
+ }
111
+ catch (error) {
112
+ this.updateState('error', 'Failed to parse frame from server');
113
+ }
114
+ };
115
+ this.ws.onerror = () => {
116
+ const error = new HyperStackError('WebSocket connection error', 'CONNECTION_ERROR');
117
+ this.updateState('error', error.message);
118
+ if (this.currentState === 'connecting') {
119
+ reject(error);
120
+ }
121
+ };
122
+ this.ws.onclose = () => {
123
+ this.stopPingInterval();
124
+ this.ws = null;
125
+ if (this.currentState !== 'disconnected') {
126
+ this.handleReconnect();
127
+ }
128
+ };
129
+ }
130
+ catch (error) {
131
+ const hsError = new HyperStackError('Failed to create WebSocket connection', 'CONNECTION_ERROR', error);
132
+ this.updateState('error', hsError.message);
133
+ reject(hsError);
134
+ }
135
+ });
136
+ }
137
+ disconnect() {
138
+ this.clearReconnectTimeout();
139
+ this.stopPingInterval();
140
+ this.updateState('disconnected');
141
+ if (this.ws) {
142
+ this.ws.close();
143
+ this.ws = null;
144
+ }
145
+ }
146
+ subscribe(subscription) {
147
+ const subKey = this.makeSubKey(subscription);
148
+ if (this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN) {
149
+ this.ws.send(JSON.stringify(subscription));
150
+ this.activeSubscriptions.add(subKey);
151
+ }
152
+ else {
153
+ this.subscriptionQueue.push(subscription);
154
+ }
155
+ }
156
+ unsubscribe(view, key) {
157
+ const subscription = { view, key };
158
+ const subKey = this.makeSubKey(subscription);
159
+ this.activeSubscriptions.delete(subKey);
160
+ }
161
+ isConnected() {
162
+ return this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN;
163
+ }
164
+ makeSubKey(subscription) {
165
+ return `${subscription.view}:${subscription.key ?? '*'}:${subscription.partition ?? ''}`;
166
+ }
167
+ flushSubscriptionQueue() {
168
+ while (this.subscriptionQueue.length > 0) {
169
+ const sub = this.subscriptionQueue.shift();
170
+ if (sub) {
171
+ this.subscribe(sub);
172
+ }
173
+ }
174
+ }
175
+ resubscribeActive() {
176
+ for (const subKey of this.activeSubscriptions) {
177
+ const [view, key, partition] = subKey.split(':');
178
+ const subscription = {
179
+ view: view ?? '',
180
+ key: key === '*' ? undefined : key,
181
+ partition: partition || undefined,
182
+ };
183
+ if (this.ws?.readyState === WebSocket.OPEN) {
184
+ this.ws.send(JSON.stringify(subscription));
185
+ }
186
+ }
187
+ }
188
+ updateState(state, error) {
189
+ this.currentState = state;
190
+ for (const handler of this.stateHandlers) {
191
+ handler(state, error);
192
+ }
193
+ }
194
+ notifyFrameHandlers(frame) {
195
+ for (const handler of this.frameHandlers) {
196
+ handler(frame);
197
+ }
198
+ }
199
+ handleReconnect() {
200
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
201
+ this.updateState('error', `Max reconnection attempts (${this.reconnectAttempts}) reached`);
202
+ return;
203
+ }
204
+ this.updateState('reconnecting');
205
+ const attemptIndex = Math.min(this.reconnectAttempts, this.reconnectIntervals.length - 1);
206
+ const delay = this.reconnectIntervals[attemptIndex] ?? 1000;
207
+ this.reconnectAttempts++;
208
+ this.reconnectTimeout = setTimeout(() => {
209
+ this.connect().catch(() => {
210
+ /* retry handled by onclose */
211
+ });
212
+ }, delay);
213
+ }
214
+ clearReconnectTimeout() {
215
+ if (this.reconnectTimeout) {
216
+ clearTimeout(this.reconnectTimeout);
217
+ this.reconnectTimeout = null;
218
+ }
219
+ }
220
+ startPingInterval() {
221
+ this.stopPingInterval();
222
+ this.pingInterval = setInterval(() => {
223
+ if (this.ws?.readyState === WebSocket.OPEN) {
224
+ this.ws.send(JSON.stringify({ type: 'ping' }));
225
+ }
226
+ }, 15000);
227
+ }
228
+ stopPingInterval() {
229
+ if (this.pingInterval) {
230
+ clearInterval(this.pingInterval);
231
+ this.pingInterval = null;
232
+ }
233
+ }
234
+ }
235
+
236
+ function isObject(item) {
237
+ return item !== null && typeof item === 'object' && !Array.isArray(item);
238
+ }
239
+ function deepMerge(target, source) {
240
+ if (!isObject(target) || !isObject(source)) {
241
+ return source;
242
+ }
243
+ const result = { ...target };
244
+ for (const key in source) {
245
+ const sourceValue = source[key];
246
+ const targetValue = result[key];
247
+ if (isObject(sourceValue) && isObject(targetValue)) {
248
+ result[key] = deepMerge(targetValue, sourceValue);
249
+ }
250
+ else {
251
+ result[key] = sourceValue;
252
+ }
253
+ }
254
+ return result;
255
+ }
256
+ class EntityStore {
257
+ constructor() {
258
+ this.entities = new Map();
259
+ this.updateCallbacks = new Set();
260
+ this.richUpdateCallbacks = new Set();
261
+ }
262
+ handleFrame(frame) {
263
+ const viewPath = frame.entity;
264
+ let viewMap = this.entities.get(viewPath);
265
+ if (!viewMap) {
266
+ viewMap = new Map();
267
+ this.entities.set(viewPath, viewMap);
268
+ }
269
+ const previousValue = viewMap.get(frame.key);
270
+ switch (frame.op) {
271
+ case 'create':
272
+ case 'upsert':
273
+ viewMap.set(frame.key, frame.data);
274
+ this.notifyUpdate(viewPath, frame.key, {
275
+ type: 'upsert',
276
+ key: frame.key,
277
+ data: frame.data,
278
+ });
279
+ this.notifyRichUpdate(viewPath, frame.key, previousValue, frame.data, frame.op);
280
+ break;
281
+ case 'patch': {
282
+ const existing = viewMap.get(frame.key);
283
+ const merged = existing ? deepMerge(existing, frame.data) : frame.data;
284
+ viewMap.set(frame.key, merged);
285
+ this.notifyUpdate(viewPath, frame.key, {
286
+ type: 'patch',
287
+ key: frame.key,
288
+ data: frame.data,
289
+ });
290
+ this.notifyRichUpdate(viewPath, frame.key, previousValue, merged, 'patch', frame.data);
291
+ break;
292
+ }
293
+ case 'delete':
294
+ viewMap.delete(frame.key);
295
+ this.notifyUpdate(viewPath, frame.key, {
296
+ type: 'delete',
297
+ key: frame.key,
298
+ });
299
+ if (previousValue !== undefined) {
300
+ this.notifyRichDelete(viewPath, frame.key, previousValue);
301
+ }
302
+ break;
303
+ }
304
+ }
305
+ getAll(viewPath) {
306
+ const viewMap = this.entities.get(viewPath);
307
+ if (!viewMap)
308
+ return [];
309
+ return Array.from(viewMap.values());
310
+ }
311
+ get(viewPath, key) {
312
+ const viewMap = this.entities.get(viewPath);
313
+ if (!viewMap)
314
+ return null;
315
+ const value = viewMap.get(key);
316
+ return value !== undefined ? value : null;
317
+ }
318
+ getAllSync(viewPath) {
319
+ const viewMap = this.entities.get(viewPath);
320
+ if (!viewMap)
321
+ return undefined;
322
+ return Array.from(viewMap.values());
323
+ }
324
+ getSync(viewPath, key) {
325
+ const viewMap = this.entities.get(viewPath);
326
+ if (!viewMap)
327
+ return undefined;
328
+ const value = viewMap.get(key);
329
+ return value !== undefined ? value : null;
330
+ }
331
+ keys(viewPath) {
332
+ const viewMap = this.entities.get(viewPath);
333
+ if (!viewMap)
334
+ return [];
335
+ return Array.from(viewMap.keys());
336
+ }
337
+ size(viewPath) {
338
+ const viewMap = this.entities.get(viewPath);
339
+ return viewMap?.size ?? 0;
340
+ }
341
+ clear() {
342
+ this.entities.clear();
343
+ }
344
+ clearView(viewPath) {
345
+ this.entities.delete(viewPath);
346
+ }
347
+ onUpdate(callback) {
348
+ this.updateCallbacks.add(callback);
349
+ return () => {
350
+ this.updateCallbacks.delete(callback);
351
+ };
352
+ }
353
+ onRichUpdate(callback) {
354
+ this.richUpdateCallbacks.add(callback);
355
+ return () => {
356
+ this.richUpdateCallbacks.delete(callback);
357
+ };
358
+ }
359
+ subscribe(viewPath, callback) {
360
+ const handler = (path, _key, update) => {
361
+ if (path === viewPath) {
362
+ callback(update);
363
+ }
364
+ };
365
+ this.updateCallbacks.add(handler);
366
+ return () => {
367
+ this.updateCallbacks.delete(handler);
368
+ };
369
+ }
370
+ subscribeToKey(viewPath, key, callback) {
371
+ const handler = (path, updateKey, update) => {
372
+ if (path === viewPath && updateKey === key) {
373
+ callback(update);
374
+ }
375
+ };
376
+ this.updateCallbacks.add(handler);
377
+ return () => {
378
+ this.updateCallbacks.delete(handler);
379
+ };
380
+ }
381
+ notifyUpdate(viewPath, key, update) {
382
+ for (const callback of this.updateCallbacks) {
383
+ callback(viewPath, key, update);
384
+ }
385
+ }
386
+ notifyRichUpdate(viewPath, key, before, after, _op, patch) {
387
+ const richUpdate = before === undefined
388
+ ? { type: 'created', key, data: after }
389
+ : { type: 'updated', key, before, after, patch };
390
+ for (const callback of this.richUpdateCallbacks) {
391
+ callback(viewPath, key, richUpdate);
392
+ }
393
+ }
394
+ notifyRichDelete(viewPath, key, lastKnown) {
395
+ const richUpdate = { type: 'deleted', key, lastKnown };
396
+ for (const callback of this.richUpdateCallbacks) {
397
+ callback(viewPath, key, richUpdate);
398
+ }
399
+ }
400
+ }
401
+
402
+ class SubscriptionRegistry {
403
+ constructor(connection) {
404
+ this.subscriptions = new Map();
405
+ this.connection = connection;
406
+ }
407
+ subscribe(subscription) {
408
+ const subKey = this.makeSubKey(subscription);
409
+ const existing = this.subscriptions.get(subKey);
410
+ if (existing) {
411
+ existing.refCount++;
412
+ }
413
+ else {
414
+ this.subscriptions.set(subKey, {
415
+ subscription,
416
+ refCount: 1,
417
+ });
418
+ this.connection.subscribe(subscription);
419
+ }
420
+ return () => this.unsubscribe(subscription);
421
+ }
422
+ unsubscribe(subscription) {
423
+ const subKey = this.makeSubKey(subscription);
424
+ const existing = this.subscriptions.get(subKey);
425
+ if (existing) {
426
+ existing.refCount--;
427
+ if (existing.refCount <= 0) {
428
+ this.subscriptions.delete(subKey);
429
+ this.connection.unsubscribe(subscription.view, subscription.key);
430
+ }
431
+ }
432
+ }
433
+ getRefCount(subscription) {
434
+ const subKey = this.makeSubKey(subscription);
435
+ return this.subscriptions.get(subKey)?.refCount ?? 0;
436
+ }
437
+ getActiveSubscriptions() {
438
+ return Array.from(this.subscriptions.values()).map((t) => t.subscription);
439
+ }
440
+ clear() {
441
+ for (const { subscription } of this.subscriptions.values()) {
442
+ this.connection.unsubscribe(subscription.view, subscription.key);
443
+ }
444
+ this.subscriptions.clear();
445
+ }
446
+ makeSubKey(subscription) {
447
+ const filters = subscription.filters ? JSON.stringify(subscription.filters) : '{}';
448
+ return `${subscription.view}:${subscription.key ?? '*'}:${subscription.partition ?? ''}:${filters}`;
449
+ }
450
+ }
451
+
452
+ function createUpdateStream(store, subscriptionRegistry, subscription, keyFilter) {
453
+ return {
454
+ [Symbol.asyncIterator]() {
455
+ const queue = [];
456
+ let waitingResolve = null;
457
+ let unsubscribeStore = null;
458
+ let unsubscribeRegistry = null;
459
+ let done = false;
460
+ const handler = (viewPath, key, update) => {
461
+ if (viewPath !== subscription.view)
462
+ return;
463
+ if (keyFilter !== undefined && key !== keyFilter)
464
+ return;
465
+ const typedUpdate = update;
466
+ if (waitingResolve) {
467
+ const resolve = waitingResolve;
468
+ waitingResolve = null;
469
+ resolve({ value: typedUpdate, done: false });
470
+ }
471
+ else {
472
+ queue.push({
473
+ update: typedUpdate,
474
+ resolve: () => { },
475
+ });
476
+ }
477
+ };
478
+ const start = () => {
479
+ unsubscribeStore = store.onUpdate(handler);
480
+ unsubscribeRegistry = subscriptionRegistry.subscribe(subscription);
481
+ };
482
+ const cleanup = () => {
483
+ done = true;
484
+ unsubscribeStore?.();
485
+ unsubscribeRegistry?.();
486
+ };
487
+ start();
488
+ return {
489
+ async next() {
490
+ if (done) {
491
+ return { value: undefined, done: true };
492
+ }
493
+ const queued = queue.shift();
494
+ if (queued) {
495
+ return { value: queued.update, done: false };
496
+ }
497
+ return new Promise((resolve) => {
498
+ waitingResolve = resolve;
499
+ });
500
+ },
501
+ async return() {
502
+ cleanup();
503
+ return { value: undefined, done: true };
504
+ },
505
+ async throw(error) {
506
+ cleanup();
507
+ throw error;
508
+ },
509
+ };
510
+ },
511
+ };
512
+ }
513
+ function createRichUpdateStream(store, subscriptionRegistry, subscription, keyFilter) {
514
+ return {
515
+ [Symbol.asyncIterator]() {
516
+ const queue = [];
517
+ let waitingResolve = null;
518
+ let unsubscribeStore = null;
519
+ let unsubscribeRegistry = null;
520
+ let done = false;
521
+ const handler = (viewPath, key, update) => {
522
+ if (viewPath !== subscription.view)
523
+ return;
524
+ if (keyFilter !== undefined && key !== keyFilter)
525
+ return;
526
+ const typedUpdate = update;
527
+ if (waitingResolve) {
528
+ const resolve = waitingResolve;
529
+ waitingResolve = null;
530
+ resolve({ value: typedUpdate, done: false });
531
+ }
532
+ else {
533
+ queue.push({
534
+ update: typedUpdate,
535
+ resolve: () => { },
536
+ });
537
+ }
538
+ };
539
+ const start = () => {
540
+ unsubscribeStore = store.onRichUpdate(handler);
541
+ unsubscribeRegistry = subscriptionRegistry.subscribe(subscription);
542
+ };
543
+ const cleanup = () => {
544
+ done = true;
545
+ unsubscribeStore?.();
546
+ unsubscribeRegistry?.();
547
+ };
548
+ start();
549
+ return {
550
+ async next() {
551
+ if (done) {
552
+ return { value: undefined, done: true };
553
+ }
554
+ const queued = queue.shift();
555
+ if (queued) {
556
+ return { value: queued.update, done: false };
557
+ }
558
+ return new Promise((resolve) => {
559
+ waitingResolve = resolve;
560
+ });
561
+ },
562
+ async return() {
563
+ cleanup();
564
+ return { value: undefined, done: true };
565
+ },
566
+ async throw(error) {
567
+ cleanup();
568
+ throw error;
569
+ },
570
+ };
571
+ },
572
+ };
573
+ }
574
+
575
+ function createTypedStateView(viewDef, store, subscriptionRegistry) {
576
+ return {
577
+ watch(key) {
578
+ return createUpdateStream(store, subscriptionRegistry, { view: viewDef.view, key }, key);
579
+ },
580
+ watchRich(key) {
581
+ return createRichUpdateStream(store, subscriptionRegistry, { view: viewDef.view, key }, key);
582
+ },
583
+ async get(key) {
584
+ return store.get(viewDef.view, key);
585
+ },
586
+ getSync(key) {
587
+ return store.getSync(viewDef.view, key);
588
+ },
589
+ };
590
+ }
591
+ function createTypedListView(viewDef, store, subscriptionRegistry) {
592
+ return {
593
+ watch() {
594
+ return createUpdateStream(store, subscriptionRegistry, { view: viewDef.view });
595
+ },
596
+ watchRich() {
597
+ return createRichUpdateStream(store, subscriptionRegistry, { view: viewDef.view });
598
+ },
599
+ async get() {
600
+ return store.getAll(viewDef.view);
601
+ },
602
+ getSync() {
603
+ return store.getAllSync(viewDef.view);
604
+ },
605
+ };
606
+ }
607
+ function createTypedViews(stack, store, subscriptionRegistry) {
608
+ const views = {};
609
+ for (const [viewName, viewGroup] of Object.entries(stack.views)) {
610
+ const group = viewGroup;
611
+ const typedGroup = {};
612
+ if (group.state) {
613
+ typedGroup.state = createTypedStateView(group.state, store, subscriptionRegistry);
614
+ }
615
+ if (group.list) {
616
+ typedGroup.list = createTypedListView(group.list, store, subscriptionRegistry);
617
+ }
618
+ views[viewName] = typedGroup;
619
+ }
620
+ return views;
621
+ }
622
+
623
+ class HyperStack {
624
+ constructor(url, options) {
625
+ this.stack = options.stack;
626
+ this.store = new EntityStore();
627
+ this.connection = new ConnectionManager({
628
+ websocketUrl: url,
629
+ reconnectIntervals: options.reconnectIntervals,
630
+ maxReconnectAttempts: options.maxReconnectAttempts,
631
+ });
632
+ this.subscriptionRegistry = new SubscriptionRegistry(this.connection);
633
+ this.connection.onFrame((frame) => {
634
+ this.store.handleFrame(frame);
635
+ });
636
+ this._views = createTypedViews(this.stack, this.store, this.subscriptionRegistry);
637
+ }
638
+ static async connect(url, options) {
639
+ if (!url) {
640
+ throw new HyperStackError('URL is required', 'INVALID_CONFIG');
641
+ }
642
+ if (!options.stack) {
643
+ throw new HyperStackError('Stack definition is required', 'INVALID_CONFIG');
644
+ }
645
+ const client = new HyperStack(url, options);
646
+ if (options.autoReconnect !== false) {
647
+ await client.connection.connect();
648
+ }
649
+ return client;
650
+ }
651
+ get views() {
652
+ return this._views;
653
+ }
654
+ get connectionState() {
655
+ return this.connection.getState();
656
+ }
657
+ get stackName() {
658
+ return this.stack.name;
659
+ }
660
+ onConnectionStateChange(callback) {
661
+ return this.connection.onStateChange(callback);
662
+ }
663
+ onFrame(callback) {
664
+ return this.connection.onFrame(callback);
665
+ }
666
+ async connect() {
667
+ await this.connection.connect();
668
+ }
669
+ disconnect() {
670
+ this.subscriptionRegistry.clear();
671
+ this.connection.disconnect();
672
+ }
673
+ isConnected() {
674
+ return this.connection.isConnected();
675
+ }
676
+ clearStore() {
677
+ this.store.clear();
678
+ }
679
+ getStore() {
680
+ return this.store;
681
+ }
682
+ getConnection() {
683
+ return this.connection;
684
+ }
685
+ getSubscriptionRegistry() {
686
+ return this.subscriptionRegistry;
687
+ }
688
+ }
689
+
690
+ export { ConnectionManager, DEFAULT_CONFIG, EntityStore, HyperStack, HyperStackError, SubscriptionRegistry, createRichUpdateStream, createTypedListView, createTypedStateView, createTypedViews, createUpdateStream, isValidFrame, parseFrame, parseFrameFromBlob };
691
+ //# sourceMappingURL=index.esm.js.map