@helipod/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,642 @@
1
+ import {
2
+ AUTH_REFRESH_UDF_PATH,
3
+ DEFAULT_DRAIN_CHUNK_SIZE,
4
+ DEFAULT_DRAIN_INTERVAL_MS,
5
+ HelipodClient,
6
+ MutationUndeliveredError,
7
+ NON_REPLAYABLE_MUTATION_DROPPED,
8
+ OFFLINE_IDENTITY_CHANGED,
9
+ OutboxDrain,
10
+ anyApi,
11
+ buildConnectMessage,
12
+ computeDrainBackoff,
13
+ createOptimisticLocalStore,
14
+ dropIfNonReplayable,
15
+ getFunctionPath,
16
+ outboxAckedThrough,
17
+ outboxHeldFromLog,
18
+ outboxHeldFromStore,
19
+ sessionFingerprintKey,
20
+ sha256Hex
21
+ } from "./chunk-I7ZJFW4E.js";
22
+ import {
23
+ DEFAULT_OUTBOX_MAX_QUEUE_SIZE,
24
+ OUTBOX_VERSION,
25
+ OfflineClientResetError,
26
+ OutboxOverflowError,
27
+ defaultMintClientId,
28
+ indexedDBOutbox,
29
+ memoryOutbox,
30
+ mintIdentity
31
+ } from "./chunk-DW55SNHW.js";
32
+
33
+ // src/transport.ts
34
+ function loopbackTransport(connection) {
35
+ const closeListeners = /* @__PURE__ */ new Set();
36
+ let closed = false;
37
+ return {
38
+ send(message) {
39
+ if (!closed) void Promise.resolve(connection.send(message));
40
+ },
41
+ onMessage(listener) {
42
+ return connection.onMessage(listener);
43
+ },
44
+ onClose(listener) {
45
+ closeListeners.add(listener);
46
+ return () => closeListeners.delete(listener);
47
+ },
48
+ close() {
49
+ if (closed) return;
50
+ closed = true;
51
+ connection.close();
52
+ for (const l of closeListeners) l();
53
+ }
54
+ };
55
+ }
56
+ function reconnectDelayMs(attempt, initialBackoffMs, maxBackoffMs, rand = Math.random) {
57
+ const exp = Math.min(maxBackoffMs, initialBackoffMs * 2 ** attempt);
58
+ const half = exp / 2;
59
+ return half + rand() * half;
60
+ }
61
+ function webSocketTransport(url, opts = {}) {
62
+ const reconnect = opts.reconnect ?? true;
63
+ const initialBackoffMs = opts.initialBackoffMs ?? 300;
64
+ const maxBackoffMs = opts.maxBackoffMs ?? 3e4;
65
+ const createWebSocket = opts.createWebSocket ?? ((u) => new WebSocket(u));
66
+ const listeners = /* @__PURE__ */ new Set();
67
+ const closeListeners = /* @__PURE__ */ new Set();
68
+ const reopenListeners = /* @__PURE__ */ new Set();
69
+ const queue = [];
70
+ let ws;
71
+ let open = false;
72
+ let everOpened = false;
73
+ let hadFailedConnect = false;
74
+ let announced = false;
75
+ let terminated = false;
76
+ let attempt = 0;
77
+ let reconnectTimer;
78
+ const clearReconnectTimer = () => {
79
+ if (reconnectTimer !== void 0) {
80
+ clearTimeout(reconnectTimer);
81
+ reconnectTimer = void 0;
82
+ }
83
+ };
84
+ const announceClose = () => {
85
+ if (announced) return;
86
+ announced = true;
87
+ queue.length = 0;
88
+ for (const l of closeListeners) l();
89
+ };
90
+ const handleDisconnect = () => {
91
+ open = false;
92
+ if (!everOpened) hadFailedConnect = true;
93
+ announceClose();
94
+ if (terminated || !reconnect) {
95
+ terminated = true;
96
+ return;
97
+ }
98
+ const delay = reconnectDelayMs(attempt, initialBackoffMs, maxBackoffMs);
99
+ attempt++;
100
+ clearReconnectTimer();
101
+ reconnectTimer = setTimeout(() => {
102
+ reconnectTimer = void 0;
103
+ if (terminated) return;
104
+ ws = createWebSocket(url);
105
+ wire(ws);
106
+ }, delay);
107
+ reconnectTimer.unref?.();
108
+ };
109
+ function wire(socket) {
110
+ socket.addEventListener("open", () => {
111
+ if (terminated) return;
112
+ open = true;
113
+ announced = false;
114
+ attempt = 0;
115
+ for (const m of queue) socket.send(JSON.stringify(m));
116
+ queue.length = 0;
117
+ if (everOpened || hadFailedConnect) {
118
+ for (const l of reopenListeners) l();
119
+ }
120
+ hadFailedConnect = false;
121
+ everOpened = true;
122
+ });
123
+ socket.addEventListener("message", (ev) => {
124
+ if (typeof ev.data !== "string") return;
125
+ const msg = JSON.parse(ev.data);
126
+ for (const l of listeners) l(msg);
127
+ });
128
+ socket.addEventListener("close", handleDisconnect);
129
+ socket.addEventListener("error", handleDisconnect);
130
+ }
131
+ ws = createWebSocket(url);
132
+ wire(ws);
133
+ return {
134
+ send(message) {
135
+ if (terminated) return;
136
+ if (open) {
137
+ ws.send(JSON.stringify(message));
138
+ return;
139
+ }
140
+ if (everOpened && reconnect) return;
141
+ queue.push(message);
142
+ },
143
+ onMessage(listener) {
144
+ listeners.add(listener);
145
+ return () => listeners.delete(listener);
146
+ },
147
+ onClose(listener) {
148
+ closeListeners.add(listener);
149
+ return () => closeListeners.delete(listener);
150
+ },
151
+ onReopen(listener) {
152
+ reopenListeners.add(listener);
153
+ return () => reopenListeners.delete(listener);
154
+ },
155
+ close() {
156
+ terminated = true;
157
+ clearReconnectTimer();
158
+ announceClose();
159
+ try {
160
+ ws.close();
161
+ } catch {
162
+ }
163
+ }
164
+ };
165
+ }
166
+
167
+ // src/headless-drain.ts
168
+ function originTag() {
169
+ const loc = globalThis.location;
170
+ return loc?.origin ?? "app";
171
+ }
172
+ function probeLockManager() {
173
+ const nav = globalThis.navigator;
174
+ const locks = nav?.locks;
175
+ if (locks && typeof locks.request === "function") {
176
+ return {
177
+ request: (name, options, callback) => locks.request(name, options, callback)
178
+ };
179
+ }
180
+ return void 0;
181
+ }
182
+ async function isLockAvailable(locks, name) {
183
+ let available = false;
184
+ await locks.request(name, { ifAvailable: true }, async (lock) => {
185
+ available = lock !== null;
186
+ });
187
+ return available;
188
+ }
189
+ function countActive(entries) {
190
+ return entries.filter((e) => e.status === "unsent" || e.status === "inflight" || e.status === "parked").length;
191
+ }
192
+ function toOutboxEntry(entry) {
193
+ return {
194
+ clientId: entry.clientId,
195
+ seq: entry.seq,
196
+ requestId: entry.requestId,
197
+ udfPath: entry.udfPath,
198
+ args: entry.args,
199
+ seed: entry.seed,
200
+ order: entry.order,
201
+ status: "unsent",
202
+ identityFingerprint: entry.identityFingerprint,
203
+ outboxVersion: OUTBOX_VERSION,
204
+ enqueuedAt: entry.enqueuedAt ?? Date.now()
205
+ };
206
+ }
207
+ async function drainOutboxOnce(opts) {
208
+ const outbox = opts.outbox ?? indexedDBOutbox();
209
+ const deployment = opts.deployment ?? "default";
210
+ const lockName = `helipod:outbox:${originTag()}:${deployment}`;
211
+ const timeoutMs = opts.timeoutMs ?? 3e4;
212
+ const initial = await outbox.loadAll();
213
+ if (countActive(initial.entries) === 0) {
214
+ return { drained: 0, failed: 0, remaining: 0 };
215
+ }
216
+ const locks = opts.locks === void 0 ? probeLockManager() : opts.locks ?? void 0;
217
+ if (locks) {
218
+ const available = await isLockAvailable(locks, lockName);
219
+ if (!available) {
220
+ return { drained: 0, failed: 0, remaining: countActive(initial.entries) };
221
+ }
222
+ }
223
+ let transport = opts._transport;
224
+ try {
225
+ let fingerprint = "anon";
226
+ let authToken = null;
227
+ if (opts.getAuthToken) {
228
+ authToken = await opts.getAuthToken();
229
+ if (authToken) fingerprint = await sha256Hex(authToken);
230
+ }
231
+ if (opts.getSessionId) {
232
+ const sessionId = await opts.getSessionId();
233
+ if (sessionId) fingerprint = await sha256Hex(sessionFingerprintKey(sessionId));
234
+ }
235
+ transport = transport ?? webSocketTransport(opts.url, { reconnect: false });
236
+ if (authToken) transport.send({ type: "SetAuth", token: authToken });
237
+ return await runDrain(transport, opts, outbox, initial.entries, deployment, lockName, timeoutMs, fingerprint);
238
+ } finally {
239
+ transport?.close();
240
+ }
241
+ }
242
+ async function runDrain(transport, opts, outbox, initialEntries, deployment, lockName, timeoutMs, fingerprint) {
243
+ let drained = 0;
244
+ let failed = 0;
245
+ let armed = false;
246
+ let closed = false;
247
+ let connectSent = false;
248
+ let orderCounter = Date.now();
249
+ const log = /* @__PURE__ */ new Map();
250
+ let nextRequestId = 1;
251
+ const heldAtConnect = outboxHeldFromStore(initialEntries);
252
+ function addHydrated(e) {
253
+ if (dropIfNonReplayable(outbox, e)) return;
254
+ for (const existing of log.values()) {
255
+ if (existing.clientId === e.clientId && existing.seq === e.seq) return;
256
+ }
257
+ const status = e.status === "parked" ? { type: "parked" } : { type: "unsent" };
258
+ const entry = {
259
+ requestId: String(nextRequestId++),
260
+ udfPath: e.udfPath,
261
+ args: e.args,
262
+ seed: e.seed,
263
+ touched: /* @__PURE__ */ new Set(),
264
+ status,
265
+ clientId: e.clientId,
266
+ seq: e.seq,
267
+ order: e.order,
268
+ identityFingerprint: e.identityFingerprint,
269
+ enqueuedAt: e.enqueuedAt,
270
+ durable: true
271
+ };
272
+ log.set(entry.requestId, entry);
273
+ }
274
+ function drainable() {
275
+ return [...log.values()].filter((e) => e.clientId !== void 0 && e.seq !== void 0 && e.durable === true && (e.status.type === "unsent" || e.status.type === "parked")).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
276
+ }
277
+ let doneResolve;
278
+ function checkDone() {
279
+ if (log.size === 0 || drain.isPaused || closed) doneResolve?.();
280
+ }
281
+ const host = {
282
+ outbox,
283
+ // No live identity is ever minted for NEW mutations here (this function never calls
284
+ // `mutation()`) — nothing of this session's own needs protecting from `pruneDeadMeta`.
285
+ currentClientId: () => void 0,
286
+ currentFingerprint: () => fingerprint,
287
+ transportOpen: () => !closed,
288
+ isArmed: () => armed,
289
+ drainable,
290
+ addHydrated,
291
+ ensureInitialHandshake: () => {
292
+ if (connectSent) return;
293
+ connectSent = true;
294
+ transport.send(buildConnectMessage(`${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`, void 0, heldAtConnect));
295
+ },
296
+ setStatus: (entry, status) => {
297
+ entry.status = { type: status };
298
+ if (entry.clientId !== void 0 && entry.seq !== void 0) {
299
+ void outbox.updateStatus(entry.clientId, entry.seq, status).catch(() => {
300
+ });
301
+ }
302
+ },
303
+ batchEntry: (entry) => ({ requestId: entry.requestId, udfPath: entry.udfPath, args: entry.args, clientId: entry.clientId, seq: entry.seq }),
304
+ sendBatch: (entries) => transport.send({ type: "MutationBatch", entries }),
305
+ settleApplied: (requestId) => {
306
+ const entry = log.get(requestId);
307
+ log.delete(requestId);
308
+ if (entry?.clientId !== void 0 && entry.seq !== void 0) void outbox.dequeue(entry.clientId, entry.seq).catch(() => {
309
+ });
310
+ drained++;
311
+ checkDone();
312
+ },
313
+ settleTerminal: (requestId, code, message) => {
314
+ const entry = log.get(requestId);
315
+ log.delete(requestId);
316
+ if (entry?.clientId !== void 0 && entry.seq !== void 0) {
317
+ void outbox.updateStatus(entry.clientId, entry.seq, "failed", { message, code }).catch(() => {
318
+ });
319
+ }
320
+ failed++;
321
+ checkDone();
322
+ },
323
+ // No live queries — nothing to re-baseline (matches `client.ts#beginBaselineAwait`'s
324
+ // `expectTransition: false` branch, which adopts immediately with nothing to wait for).
325
+ whenBaselineAdopted: () => Promise.resolve()
326
+ };
327
+ const drain = new OutboxDrain(host, {
328
+ lockName,
329
+ locks: opts.locks,
330
+ poisonPolicy: opts.poisonPolicy ?? "skip",
331
+ intervalMs: 0,
332
+ // one-shot: progress is driven by responses/backoff timers, not a periodic nudge
333
+ onPause: () => checkDone()
334
+ });
335
+ async function handleClientReset() {
336
+ const fresh = defaultMintClientId();
337
+ let freshSeq = 0;
338
+ for (const entry of [...log.values()]) {
339
+ if (entry.status.type === "parked") {
340
+ log.delete(entry.requestId);
341
+ failed++;
342
+ if (entry.clientId !== void 0 && entry.seq !== void 0) {
343
+ void outbox.updateStatus(entry.clientId, entry.seq, "failed", {
344
+ message: "the server disowned this client's mutation history (swept/foreign timeline)",
345
+ code: "OFFLINE_CLIENT_RESET"
346
+ }).catch(() => {
347
+ });
348
+ }
349
+ } else if (entry.status.type === "unsent") {
350
+ const oldClientId = entry.clientId;
351
+ const oldSeq = entry.seq;
352
+ entry.clientId = fresh;
353
+ entry.seq = freshSeq++;
354
+ entry.order = ++orderCounter;
355
+ if (oldClientId !== void 0 && oldSeq !== void 0) void outbox.dequeue(oldClientId, oldSeq).catch(() => {
356
+ });
357
+ void outbox.append(toOutboxEntry(entry)).catch(() => {
358
+ });
359
+ }
360
+ }
361
+ await outbox.setMeta(fresh, { nextSeq: freshSeq, deployment });
362
+ }
363
+ const disposeMessage = transport.onMessage((msg) => {
364
+ if (msg.type === "MutationResponse") {
365
+ if (drain.handles(msg.requestId)) drain.onResponse(msg);
366
+ return;
367
+ }
368
+ if (msg.type === "ConnectAck") {
369
+ armed = true;
370
+ if (msg.known) {
371
+ drain.nudge();
372
+ } else {
373
+ void handleClientReset().then(() => {
374
+ drain.nudge();
375
+ checkDone();
376
+ }).catch((err) => {
377
+ console.error("[helipod] outbox: handleClientReset failed", err);
378
+ checkDone();
379
+ });
380
+ }
381
+ }
382
+ });
383
+ const disposeClose = transport.onClose(() => {
384
+ closed = true;
385
+ drain.onTransportClosed();
386
+ checkDone();
387
+ });
388
+ const donePromise = new Promise((resolve) => {
389
+ doneResolve = resolve;
390
+ });
391
+ drain.start();
392
+ let timeoutTimer;
393
+ const timeoutPromise = new Promise((resolve) => {
394
+ timeoutTimer = setTimeout(resolve, timeoutMs);
395
+ timeoutTimer.unref?.();
396
+ });
397
+ await Promise.race([donePromise, timeoutPromise]);
398
+ if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
399
+ drain.stop();
400
+ disposeMessage();
401
+ disposeClose();
402
+ return { drained, failed, remaining: log.size };
403
+ }
404
+
405
+ // src/auth-client.ts
406
+ var KEY = "helipod.session";
407
+ var SESSION_STORAGE_KEY = KEY;
408
+ function localStorageSession(key = KEY) {
409
+ let ls;
410
+ try {
411
+ ls = typeof localStorage !== "undefined" ? localStorage : void 0;
412
+ if (ls) {
413
+ ls.setItem(`${key}.probe`, "1");
414
+ ls.removeItem(`${key}.probe`);
415
+ }
416
+ } catch {
417
+ ls = void 0;
418
+ }
419
+ if (!ls) return memorySession();
420
+ return {
421
+ load() {
422
+ try {
423
+ const raw = ls.getItem(key);
424
+ return raw ? JSON.parse(raw) : null;
425
+ } catch {
426
+ return null;
427
+ }
428
+ },
429
+ save(info) {
430
+ try {
431
+ ls.setItem(key, JSON.stringify(info));
432
+ } catch {
433
+ }
434
+ },
435
+ clear() {
436
+ try {
437
+ ls.removeItem(key);
438
+ } catch {
439
+ }
440
+ }
441
+ };
442
+ }
443
+ function memorySession() {
444
+ let cur = null;
445
+ return { load: () => cur, save: (i) => {
446
+ cur = i;
447
+ }, clear: () => {
448
+ cur = null;
449
+ } };
450
+ }
451
+ function defaultLock() {
452
+ const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
453
+ if (locks) {
454
+ return { run: (fn) => locks.request("helipod:auth:refresh", fn) };
455
+ }
456
+ let tail = Promise.resolve();
457
+ return {
458
+ run(fn) {
459
+ const next = tail.then(fn, fn);
460
+ tail = next.catch(() => {
461
+ });
462
+ return next;
463
+ }
464
+ };
465
+ }
466
+ function defaultBroadcast() {
467
+ const BC = typeof BroadcastChannel !== "undefined" ? BroadcastChannel : void 0;
468
+ if (!BC) return { post: () => {
469
+ }, onMessage: () => {
470
+ }, close: () => {
471
+ } };
472
+ const ch = new BC("helipod:auth:pair");
473
+ return {
474
+ post: (info) => ch.postMessage(info),
475
+ onMessage: (cb) => {
476
+ ch.onmessage = (e) => cb(e.data);
477
+ },
478
+ close: () => ch.close()
479
+ };
480
+ }
481
+ function codeOf(err) {
482
+ if (err && typeof err === "object") {
483
+ const c = err.code;
484
+ if (typeof c === "string") return c;
485
+ const m = err.message;
486
+ if (typeof m === "string") return m;
487
+ }
488
+ return String(err);
489
+ }
490
+ function createAuthClient(client, opts = {}) {
491
+ const storage = opts.storage ?? localStorageSession();
492
+ const lock = opts.lock ?? defaultLock();
493
+ const broadcast = opts.broadcast ?? defaultBroadcast();
494
+ const now = opts.now ?? (() => Date.now());
495
+ const fraction = opts.refreshAtFraction ?? 0.8;
496
+ const refreshPath = opts.refreshPath ?? "auth:refresh";
497
+ let info = storage.load();
498
+ let timer;
499
+ let closed = false;
500
+ let refreshFailureCount = 0;
501
+ function apply(next) {
502
+ refreshFailureCount = 0;
503
+ info = next;
504
+ if (next) {
505
+ storage.save(next);
506
+ client.setSessionFingerprint(next.sessionId);
507
+ client.setAuth(next.token);
508
+ schedule();
509
+ } else {
510
+ storage.clear();
511
+ client.setSessionFingerprint(null);
512
+ client.setAuth(null);
513
+ if (timer) clearTimeout(timer);
514
+ }
515
+ }
516
+ function schedule() {
517
+ if (timer) clearTimeout(timer);
518
+ if (!info || closed) return;
519
+ const remaining = info.expiresAt - now();
520
+ const delay = Math.max(0, remaining * fraction);
521
+ timer = setTimeout(() => {
522
+ void doRefresh();
523
+ }, delay);
524
+ }
525
+ const backoffBaseMs = 1e3;
526
+ const backoffCapMs = 6e4;
527
+ function scheduleBackoff() {
528
+ if (timer) clearTimeout(timer);
529
+ if (!info || closed) return;
530
+ refreshFailureCount++;
531
+ const delay = Math.min(backoffCapMs, backoffBaseMs * 2 ** (refreshFailureCount - 1));
532
+ timer = setTimeout(() => {
533
+ void doRefresh();
534
+ }, delay);
535
+ }
536
+ function isNewer(candidate, currentInfo) {
537
+ return candidate.refreshToken !== currentInfo.refreshToken && candidate.expiresAt > currentInfo.expiresAt;
538
+ }
539
+ async function doRefresh() {
540
+ if (!info || closed) return;
541
+ const before = info;
542
+ try {
543
+ const result = await lock.run(async () => {
544
+ const latest = storage.load();
545
+ if (latest && isNewer(latest, before)) return { adopted: latest };
546
+ const next = await client.mutation(refreshPath, { refreshToken: before.refreshToken }, { transient: true });
547
+ return { minted: next };
548
+ });
549
+ if (closed) return;
550
+ if ("adopted" in result && result.adopted) {
551
+ apply(result.adopted);
552
+ return;
553
+ }
554
+ if ("minted" in result && result.minted) {
555
+ apply(result.minted);
556
+ broadcast.post(result.minted);
557
+ }
558
+ } catch (err) {
559
+ if (closed) return;
560
+ const code = codeOf(err);
561
+ if (code === "REFRESH_STALE") {
562
+ setTimeout(() => {
563
+ if (closed) return;
564
+ const latest = storage.load();
565
+ if (latest && isNewer(latest, before)) apply(latest);
566
+ else schedule();
567
+ }, 250);
568
+ return;
569
+ }
570
+ if (code === "REFRESH_EXPIRED" || code === "REFRESH_REUSED") {
571
+ apply(null);
572
+ opts.onSignedOut?.();
573
+ return;
574
+ }
575
+ scheduleBackoff();
576
+ }
577
+ }
578
+ broadcast.onMessage((incoming) => {
579
+ if (closed || !info) return;
580
+ if (isNewer(incoming, info)) apply(incoming);
581
+ });
582
+ if (info) apply(info);
583
+ return {
584
+ setSession(next) {
585
+ apply(next);
586
+ },
587
+ clearSession() {
588
+ apply(null);
589
+ opts.onSignedOut?.();
590
+ },
591
+ getSessionInfo() {
592
+ return info ? { ...info } : null;
593
+ },
594
+ // shallow copy — never hand out the live internal object
595
+ close() {
596
+ closed = true;
597
+ if (timer) clearTimeout(timer);
598
+ broadcast.close();
599
+ }
600
+ };
601
+ }
602
+
603
+ // src/index.ts
604
+ import { mintEncodedDocumentId } from "@helipod/id-codec";
605
+ export {
606
+ AUTH_REFRESH_UDF_PATH,
607
+ DEFAULT_DRAIN_CHUNK_SIZE,
608
+ DEFAULT_DRAIN_INTERVAL_MS,
609
+ DEFAULT_OUTBOX_MAX_QUEUE_SIZE,
610
+ HelipodClient,
611
+ MutationUndeliveredError,
612
+ NON_REPLAYABLE_MUTATION_DROPPED,
613
+ OFFLINE_IDENTITY_CHANGED,
614
+ OUTBOX_VERSION,
615
+ OfflineClientResetError,
616
+ OutboxDrain,
617
+ OutboxOverflowError,
618
+ SESSION_STORAGE_KEY,
619
+ anyApi,
620
+ buildConnectMessage,
621
+ computeDrainBackoff,
622
+ createAuthClient,
623
+ createOptimisticLocalStore,
624
+ defaultMintClientId,
625
+ drainOutboxOnce,
626
+ getFunctionPath,
627
+ indexedDBOutbox,
628
+ localStorageSession,
629
+ loopbackTransport,
630
+ memoryOutbox,
631
+ memorySession,
632
+ mintEncodedDocumentId as mintDocumentId,
633
+ mintIdentity,
634
+ outboxAckedThrough,
635
+ outboxHeldFromLog,
636
+ outboxHeldFromStore,
637
+ reconnectDelayMs,
638
+ sessionFingerprintKey,
639
+ sha256Hex,
640
+ webSocketTransport
641
+ };
642
+ //# sourceMappingURL=index.js.map