@levo-so/core 0.1.59 → 0.1.60

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.
Files changed (55) hide show
  1. package/dist/index.d.ts +1763 -8
  2. package/dist/index.js +435 -89
  3. package/package.json +12 -12
  4. package/dist/client.d.ts +0 -82
  5. package/dist/constants/collection/defaultByKind.d.ts +0 -3
  6. package/dist/constants/collection/fieldInterfaceFormat.d.ts +0 -3
  7. package/dist/constants/collection/fieldInterfaces.d.ts +0 -6
  8. package/dist/constants/collection/fieldKind.d.ts +0 -26
  9. package/dist/constants/index.d.ts +0 -8
  10. package/dist/constants/integration.d.ts +0 -23
  11. package/dist/constants/media.d.ts +0 -3
  12. package/dist/constants/oauthProvider.d.ts +0 -3
  13. package/dist/control/audience.d.ts +0 -22
  14. package/dist/control/index.d.ts +0 -23
  15. package/dist/httpClient.d.ts +0 -13
  16. package/dist/logger/batch.d.ts +0 -9
  17. package/dist/logger/index.d.ts +0 -22
  18. package/dist/modules/blog.d.ts +0 -10
  19. package/dist/modules/collection.d.ts +0 -26
  20. package/dist/modules/media.d.ts +0 -11
  21. package/dist/modules/membership.d.ts +0 -21
  22. package/dist/types/audience.d.ts +0 -100
  23. package/dist/types/blog/author.d.ts +0 -30
  24. package/dist/types/blog/blogClient.d.ts +0 -51
  25. package/dist/types/blog/category.d.ts +0 -20
  26. package/dist/types/blog/index.d.ts +0 -64
  27. package/dist/types/blog/post.d.ts +0 -63
  28. package/dist/types/blog/tag.d.ts +0 -19
  29. package/dist/types/collection/collectionView.d.ts +0 -36
  30. package/dist/types/collection/draft.d.ts +0 -9
  31. package/dist/types/collection/index.d.ts +0 -7
  32. package/dist/types/collection/lemaCollection.d.ts +0 -95
  33. package/dist/types/collection/lemaField.d.ts +0 -55
  34. package/dist/types/collection/lemaSection.d.ts +0 -10
  35. package/dist/types/collection/relation.d.ts +0 -11
  36. package/dist/types/emoji.d.ts +0 -22
  37. package/dist/types/event.d.ts +0 -225
  38. package/dist/types/index.d.ts +0 -14
  39. package/dist/types/levo-query.d.ts +0 -104
  40. package/dist/types/location.d.ts +0 -6
  41. package/dist/types/media.d.ts +0 -56
  42. package/dist/types/membership/index.d.ts +0 -182
  43. package/dist/types/query.d.ts +0 -46
  44. package/dist/types/schema/field.d.ts +0 -56
  45. package/dist/types/schema/index.d.ts +0 -3
  46. package/dist/types/schema/schema.d.ts +0 -58
  47. package/dist/types/utils.d.ts +0 -8
  48. package/dist/types/workspace.d.ts +0 -172
  49. package/dist/utils/LevoError.d.ts +0 -21
  50. package/dist/utils/formatImagePath.d.ts +0 -3
  51. package/dist/utils/getLevoError.d.ts +0 -3
  52. package/dist/utils/getUserProperties.d.ts +0 -8
  53. package/dist/utils/isIncognito.d.ts +0 -9
  54. package/dist/utils/listToX.d.ts +0 -3
  55. package/dist/utils/lock.d.ts +0 -21
package/dist/index.js CHANGED
@@ -356,58 +356,282 @@ var getLevoError = (error) => {
356
356
  });
357
357
  };
358
358
 
359
- // src/utils/lock.ts
360
- var LocalStorageLock = class {
361
- key;
362
- constructor(lockKey) {
363
- this.key = lockKey;
364
- }
365
- acquireLock(expiryMs = 5e3) {
366
- const now = Date.now();
367
- const lockValue = localStorage.getItem(this.key);
368
- if (lockValue) {
369
- const timestamp = Number(lockValue);
370
- if (now - timestamp < expiryMs) {
371
- return false;
372
- }
373
- }
374
- localStorage.setItem(this.key, now.toString());
375
- setTimeout(() => {
376
- if (this.isLocked(expiryMs)) {
377
- this.releaseLock();
378
- }
379
- }, expiryMs);
380
- return true;
381
- }
382
- releaseLock() {
383
- localStorage.removeItem(this.key);
384
- }
385
- isLocked(expiryMs = 5e3) {
386
- const lockValue = localStorage.getItem(this.key);
387
- if (!lockValue) {
388
- return false;
389
- }
390
- const timestamp = Number(lockValue);
391
- return Date.now() - timestamp < expiryMs;
392
- }
393
- };
394
-
395
359
  // src/control/audience.ts
360
+ var BATCH_SIZE = 50;
361
+ var BATCH_INTERVAL_MS = 5e3;
362
+ var MAX_EVENTS_PER_PAGE = 500;
363
+ var BROADCAST_CHANNEL_NAME = "levo_analytics";
396
364
  var createLevoAudienceModule = (core, httpClient) => {
397
365
  const _IDLE_THROTTLE = 1 * 1e3;
398
366
  const _IDLE_TIMEOUT = 60 * 1e3;
399
367
  let instance = null;
400
368
  let idle_listener = null;
401
369
  const storage_keys = {
370
+ /** Legacy: Lock key for identify operation (no longer used internally) */
402
371
  identify: "lock:lv_aud_identify",
372
+ /** Key for tracking open tab count */
403
373
  open_tabs: "lv_insights_ot",
374
+ /** Key for current tab ID (sessionStorage) */
404
375
  current_tab: "lv_insights_ct",
405
- all_tabs: "lv_insights_at"
376
+ /** Key for all tabs registry */
377
+ all_tabs: "lv_insights_at",
378
+ /** Device JWT for direct mode (1yr expiry, rotated on /welcome, analytics-only scope) */
379
+ device_token: "lv_aud_device",
380
+ /** Session JWT for direct mode (1yr expiry, rotated on /welcome, analytics-only scope) */
381
+ session_token: "lv_aud_session",
382
+ /** Cached insights mode ('proxy' | 'direct') */
383
+ mode: "lv_aud_mode"
384
+ };
385
+ let _insightsMode = "proxy";
386
+ let _insightsBaseUrl = "";
387
+ let _localStorageAvailable = true;
388
+ const _memoryStorage = /* @__PURE__ */ new Map();
389
+ const checkLocalStorageAvailable = () => {
390
+ if (typeof window === "undefined" || !("localStorage" in window)) {
391
+ return false;
392
+ }
393
+ try {
394
+ const testKey = "__levo_test__";
395
+ window.localStorage.setItem(testKey, "test");
396
+ window.localStorage.removeItem(testKey);
397
+ return true;
398
+ } catch {
399
+ return false;
400
+ }
401
+ };
402
+ const getFromStorage = (key) => {
403
+ if (typeof window === "undefined") {
404
+ return _memoryStorage.get(key) || null;
405
+ }
406
+ if (_localStorageAvailable) {
407
+ try {
408
+ return window.localStorage.getItem(key);
409
+ } catch {
410
+ return _memoryStorage.get(key) || null;
411
+ }
412
+ }
413
+ return _memoryStorage.get(key) || null;
414
+ };
415
+ const setInStorage = (key, value) => {
416
+ if (typeof window === "undefined") {
417
+ _memoryStorage.set(key, value);
418
+ return;
419
+ }
420
+ if (_localStorageAvailable) {
421
+ try {
422
+ window.localStorage.setItem(key, value);
423
+ return;
424
+ } catch {
425
+ }
426
+ }
427
+ _memoryStorage.set(key, value);
428
+ };
429
+ const removeFromStorage = (key) => {
430
+ _memoryStorage.delete(key);
431
+ if (typeof window === "undefined" || !_localStorageAvailable) {
432
+ return;
433
+ }
434
+ try {
435
+ window.localStorage.removeItem(key);
436
+ } catch {
437
+ }
438
+ };
439
+ const getAuthHeaders = () => {
440
+ const headers = {};
441
+ if (_insightsMode === "direct") {
442
+ const deviceToken = getFromStorage(storage_keys.device_token);
443
+ const sessionToken = getFromStorage(storage_keys.session_token);
444
+ if (deviceToken) headers["Levo-Audience-Device"] = deviceToken;
445
+ if (sessionToken) headers["Levo-Audience-Session"] = sessionToken;
446
+ }
447
+ return headers;
448
+ };
449
+ const pingEndpoint = async (url, timeout) => {
450
+ let timeoutId = null;
451
+ try {
452
+ const controller = new AbortController();
453
+ timeoutId = setTimeout(() => controller.abort(), timeout);
454
+ const response = await fetch(url, {
455
+ method: "GET",
456
+ signal: controller.signal,
457
+ credentials: "omit"
458
+ });
459
+ return response.ok;
460
+ } catch {
461
+ return false;
462
+ } finally {
463
+ if (timeoutId) {
464
+ clearTimeout(timeoutId);
465
+ }
466
+ }
467
+ };
468
+ const detectInsightsMode = async () => {
469
+ if (core.insightsMode === "proxy") {
470
+ _insightsBaseUrl = core.insightsUrl;
471
+ return "proxy";
472
+ }
473
+ if (core.insightsMode === "direct") {
474
+ if (!core.directInsightsUrl) {
475
+ console.warn(
476
+ "[@levo-so/core] insightsMode='direct' but directInsightsUrl not set. Falling back to proxy."
477
+ );
478
+ _insightsBaseUrl = core.insightsUrl;
479
+ return "proxy";
480
+ }
481
+ _insightsBaseUrl = core.directInsightsUrl;
482
+ setInStorage(storage_keys.mode, "direct");
483
+ return "direct";
484
+ }
485
+ const cachedMode = getFromStorage(storage_keys.mode);
486
+ if (cachedMode === "proxy" || cachedMode === "direct") {
487
+ if (cachedMode === "direct" && !core.directInsightsUrl) {
488
+ removeFromStorage(storage_keys.mode);
489
+ _insightsBaseUrl = "";
490
+ } else {
491
+ const pingUrl = cachedMode === "proxy" ? `${core.insightsUrl}/ping` : `${core.directInsightsUrl}/ping`;
492
+ if (await pingEndpoint(pingUrl, core.pingTimeout)) {
493
+ _insightsBaseUrl = cachedMode === "proxy" ? core.insightsUrl : core.directInsightsUrl;
494
+ return cachedMode;
495
+ }
496
+ }
497
+ }
498
+ if (await pingEndpoint(`${core.insightsUrl}/ping`, core.pingTimeout)) {
499
+ setInStorage(storage_keys.mode, "proxy");
500
+ _insightsBaseUrl = core.insightsUrl;
501
+ return "proxy";
502
+ }
503
+ if (core.directInsightsUrl) {
504
+ setInStorage(storage_keys.mode, "direct");
505
+ _insightsBaseUrl = core.directInsightsUrl;
506
+ return "direct";
507
+ }
508
+ _insightsBaseUrl = core.insightsUrl;
509
+ return "proxy";
406
510
  };
407
511
  let _session = "";
408
512
  let _device = "";
409
513
  let is_identified = false;
410
- let _batch = [];
514
+ let _eventCount = 0;
515
+ let _eventQueue = [];
516
+ let _flushTimer = null;
517
+ let _channel = null;
518
+ let _pendingIdentify = false;
519
+ const buildSessionSyncMessage = () => {
520
+ const message = {
521
+ type: "SESSION_SYNC",
522
+ session: _session,
523
+ device: _device
524
+ };
525
+ if (_insightsMode === "direct") {
526
+ const storedDeviceToken = getFromStorage(storage_keys.device_token);
527
+ const storedSessionToken = getFromStorage(storage_keys.session_token);
528
+ if (storedDeviceToken) message.deviceToken = storedDeviceToken;
529
+ if (storedSessionToken) message.sessionToken = storedSessionToken;
530
+ }
531
+ return message;
532
+ };
533
+ const initBroadcastChannel = () => {
534
+ if (typeof BroadcastChannel === "undefined") {
535
+ return;
536
+ }
537
+ try {
538
+ _channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
539
+ _channel.onmessage = (event) => {
540
+ const { type, session, device, deviceToken, sessionToken } = event.data;
541
+ if (type === "SESSION_SYNC" && !is_identified) {
542
+ _session = session;
543
+ _device = device;
544
+ is_identified = true;
545
+ _pendingIdentify = false;
546
+ if (_insightsMode === "direct" && deviceToken && sessionToken) {
547
+ setInStorage(storage_keys.device_token, deviceToken);
548
+ setInStorage(storage_keys.session_token, sessionToken);
549
+ }
550
+ }
551
+ if (type === "SESSION_REQUEST" && is_identified) {
552
+ _channel?.postMessage(buildSessionSyncMessage());
553
+ }
554
+ };
555
+ } catch (e) {
556
+ }
557
+ };
558
+ const requestSessionFromOtherTabs = () => {
559
+ _channel?.postMessage({ type: "SESSION_REQUEST" });
560
+ };
561
+ const broadcastSession = () => {
562
+ _channel?.postMessage(buildSessionSyncMessage());
563
+ };
564
+ const flushEventQueue = () => {
565
+ const baseUrl = _insightsBaseUrl || core?.insightsUrl;
566
+ if (_eventQueue.length === 0 || !baseUrl || !core.workspace) {
567
+ return;
568
+ }
569
+ const batch = _eventQueue.map((event) => ({
570
+ ...event,
571
+ session_id: event.session_id || _session,
572
+ device_id: event.device_id || _device
573
+ }));
574
+ _eventQueue = [];
575
+ const url = `${baseUrl}/v1/insights/event/bulk?workspace=${core.workspace}`;
576
+ const headers = {
577
+ "Content-Type": "application/json",
578
+ ...getAuthHeaders()
579
+ };
580
+ if (_insightsMode === "proxy" && navigator.sendBeacon) {
581
+ const success = navigator.sendBeacon(url, JSON.stringify(batch));
582
+ if (success) {
583
+ return;
584
+ }
585
+ }
586
+ fetch(url, {
587
+ method: "POST",
588
+ body: JSON.stringify(batch),
589
+ headers,
590
+ keepalive: true,
591
+ credentials: _insightsMode === "proxy" ? "include" : "omit"
592
+ }).catch(() => {
593
+ });
594
+ };
595
+ const startFlushTimer = () => {
596
+ if (_flushTimer) {
597
+ return;
598
+ }
599
+ _flushTimer = setInterval(() => {
600
+ flushEventQueue();
601
+ }, BATCH_INTERVAL_MS);
602
+ if (typeof document !== "undefined") {
603
+ document.addEventListener("visibilitychange", () => {
604
+ if (document.visibilityState === "hidden") {
605
+ flushEventQueue();
606
+ }
607
+ });
608
+ window.addEventListener("beforeunload", () => {
609
+ flushEventQueue();
610
+ });
611
+ window.addEventListener("pagehide", () => {
612
+ flushEventQueue();
613
+ });
614
+ }
615
+ };
616
+ const stopFlushTimer = () => {
617
+ if (_flushTimer) {
618
+ clearInterval(_flushTimer);
619
+ _flushTimer = null;
620
+ }
621
+ };
622
+ const queueEvent = (collectInput) => {
623
+ if (_eventCount >= MAX_EVENTS_PER_PAGE) {
624
+ console.warn(
625
+ `[@levo-so/core] Event limit (${MAX_EVENTS_PER_PAGE}) reached for this page`
626
+ );
627
+ return;
628
+ }
629
+ _eventCount++;
630
+ _eventQueue.push(collectInput);
631
+ if (_eventQueue.length >= BATCH_SIZE) {
632
+ flushEventQueue();
633
+ }
634
+ };
411
635
  const getReferrer = () => {
412
636
  if (typeof document === "undefined") {
413
637
  return {};
@@ -477,11 +701,13 @@ var createLevoAudienceModule = (core, httpClient) => {
477
701
  const request = async (url, data) => {
478
702
  const headers = {
479
703
  Accept: "application/json",
480
- "Content-Type": "application/json"
704
+ "Content-Type": "application/json",
705
+ ...getAuthHeaders()
481
706
  };
482
707
  try {
483
- const response = await httpClient.instance.url(core?.insightsUrl, true).url(url).options({
484
- keepAlive: true
708
+ const response = await httpClient.instance.url(_insightsBaseUrl || core?.insightsUrl, true).url(url).options({
709
+ keepAlive: true,
710
+ credentials: _insightsMode === "proxy" ? "include" : "omit"
485
711
  }).headers(headers).post(data).json();
486
712
  return response;
487
713
  } catch (error) {
@@ -493,8 +719,26 @@ var createLevoAudienceModule = (core, httpClient) => {
493
719
  if (core.NODE_ENV === "development") {
494
720
  return;
495
721
  }
722
+ _eventCount = 0;
723
+ _localStorageAvailable = checkLocalStorageAvailable();
724
+ _insightsMode = await detectInsightsMode();
725
+ initBroadcastChannel();
726
+ requestSessionFromOtherTabs();
727
+ startFlushTimer();
728
+ await new Promise((resolve) => setTimeout(resolve, 50));
496
729
  const plugin = {
497
730
  name: core.workspace,
731
+ /**
732
+ * Handle track events (custom events like clicks, scrolls, etc.)
733
+ *
734
+ * Enriches the event with:
735
+ * - Session/device IDs
736
+ * - Page metadata (URL, title, etc.)
737
+ * - Tab information
738
+ * - Timestamp
739
+ *
740
+ * Then queues for batched sending.
741
+ */
498
742
  track: ({ payload }) => {
499
743
  if (!core?.insightsUrl || !core.workspace) {
500
744
  return;
@@ -506,9 +750,11 @@ var createLevoAudienceModule = (core, httpClient) => {
506
750
  event: payload.event,
507
751
  version: payload.version || 1,
508
752
  workspace_id: core.workspace,
753
+ site_id: core.site || void 0,
509
754
  properties: {
510
755
  ...properties,
511
756
  ...payloadProperties,
757
+ // Remove metadata fields from properties (they're top-level)
512
758
  version: void 0,
513
759
  resource: void 0,
514
760
  identifier: void 0,
@@ -528,21 +774,19 @@ var createLevoAudienceModule = (core, httpClient) => {
528
774
  tab_id: getCurrentTabId(),
529
775
  tab_count: getTabCount()
530
776
  };
531
- let isQueued = false;
532
- if (navigator.sendBeacon) {
533
- isQueued = navigator.sendBeacon(
534
- `${core?.insightsUrl}/v1/insights/event/collect`,
535
- JSON.stringify(collectInput)
536
- );
537
- }
538
- if (!isQueued) {
539
- request("/v1/insights/event/collect", collectInput);
540
- }
777
+ queueEvent(collectInput);
541
778
  },
779
+ /**
780
+ * Handle page view events.
781
+ *
782
+ * Called when navigating to a new page (in SPAs) or on initial load.
783
+ * Resets the rate limit counter and tracks a page.view event.
784
+ */
542
785
  page: ({ payload }) => {
543
786
  if (!core?.insightsUrl) {
544
787
  return;
545
788
  }
789
+ _eventCount = 0;
546
790
  const payloadProperties = payload?.properties || {};
547
791
  track("page.view", {
548
792
  ...properties,
@@ -552,11 +796,28 @@ var createLevoAudienceModule = (core, httpClient) => {
552
796
  resource: "page"
553
797
  });
554
798
  },
799
+ /**
800
+ * Handle identify events (session establishment).
801
+ *
802
+ * This is called when identify() is invoked. It:
803
+ * 1. Checks if already identified (via BroadcastChannel) - if so, skip
804
+ * 2. Calls /welcome API to get session/device IDs
805
+ * 3. On success, broadcasts session to other tabs
806
+ *
807
+ * The /welcome API uses HttpOnly cookies to maintain persistent
808
+ * device identity across sessions.
809
+ */
555
810
  identify: ({ payload }) => {
556
811
  if (!core?.insightsUrl || !core.workspace) {
557
812
  return;
558
813
  }
559
- const withLock = payload?.traits?.withLock ?? true;
814
+ if (is_identified) {
815
+ return;
816
+ }
817
+ if (_pendingIdentify) {
818
+ return;
819
+ }
820
+ _pendingIdentify = true;
560
821
  const welcomeInput = {
561
822
  private_mode: payload.traits.private_mode,
562
823
  dark_mode: payload.traits.dark_mode,
@@ -565,6 +826,7 @@ var createLevoAudienceModule = (core, httpClient) => {
565
826
  referrer: payload.traits.referrer,
566
827
  properties: payload.traits.properties,
567
828
  workspace_id: core.workspace,
829
+ site_id: core.site || void 0,
568
830
  hostname: window.location.hostname,
569
831
  pathname: window.location.pathname,
570
832
  page_title: document.title,
@@ -573,36 +835,33 @@ var createLevoAudienceModule = (core, httpClient) => {
573
835
  tab_id: getCurrentTabId(),
574
836
  tab_count: getTabCount()
575
837
  };
576
- let identifyLock = null;
577
- if (withLock) {
578
- identifyLock = new LocalStorageLock(storage_keys.identify);
579
- }
580
- if (!identifyLock || identifyLock.acquireLock()) {
581
- request(`/v1/insights/event/welcome`, welcomeInput).then((data) => {
582
- is_identified = true;
583
- if (data?.content?.data?.session) {
584
- _session = data?.content.data.session;
585
- }
586
- if (data?.content?.data?.device) {
587
- _device = data?.content.data.device;
588
- }
589
- let isQueued = false;
590
- if (navigator.sendBeacon) {
591
- isQueued = navigator.sendBeacon(
592
- `${core?.insightsUrl}/v1/insights/event/bulk?workspace=${core?.workspace}`,
593
- JSON.stringify(_batch)
594
- );
838
+ request(`/v1/insights/event/welcome`, welcomeInput).then((data) => {
839
+ if (is_identified) {
840
+ _pendingIdentify = false;
841
+ return;
842
+ }
843
+ is_identified = true;
844
+ _pendingIdentify = false;
845
+ if (data?.content?.data?.session) {
846
+ _session = data?.content.data.session;
847
+ }
848
+ if (data?.content?.data?.device) {
849
+ _device = data?.content.data.device;
850
+ }
851
+ if (_insightsMode === "direct" && data?.content?.meta) {
852
+ const meta = data.content.meta;
853
+ if (meta.device_token) {
854
+ setInStorage(storage_keys.device_token, meta.device_token);
595
855
  }
596
- if (!isQueued) {
597
- request("/v1/insights/event/bulk", _batch);
856
+ if (meta.session_token) {
857
+ setInStorage(storage_keys.session_token, meta.session_token);
598
858
  }
599
- _batch = [];
600
- identifyLock?.releaseLock();
601
- }).catch((e) => {
602
- identifyLock?.releaseLock();
603
- return getLevoError(e);
604
- });
605
- }
859
+ }
860
+ broadcastSession();
861
+ }).catch((e) => {
862
+ _pendingIdentify = false;
863
+ return getLevoError(e);
864
+ });
606
865
  }
607
866
  };
608
867
  instance = Analytics({
@@ -650,10 +909,10 @@ var createLevoAudienceModule = (core, httpClient) => {
650
909
  if (!core.workspace) {
651
910
  return;
652
911
  }
653
- if (instance && is_identified) {
912
+ if (instance) {
654
913
  return instance.track(event, properties);
655
914
  }
656
- return _batch.push({
915
+ const collectInput = {
657
916
  event,
658
917
  version: properties?.version || 1,
659
918
  properties: {
@@ -674,23 +933,39 @@ var createLevoAudienceModule = (core, httpClient) => {
674
933
  resource: properties?.resource || "page",
675
934
  identifier: properties?.identifier || properties?.id,
676
935
  workspace_id: core.workspace,
936
+ site_id: core.site || void 0,
677
937
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
678
938
  tab_id: getCurrentTabId(),
679
939
  tab_count: getTabCount()
680
- });
940
+ };
941
+ queueEvent(collectInput);
942
+ };
943
+ const destroy = () => {
944
+ flushEventQueue();
945
+ stopFlushTimer();
946
+ _channel?.close();
681
947
  };
682
948
  return {
949
+ /** Get the underlying analytics instance (read-only) */
683
950
  get instance() {
684
951
  return instance;
685
952
  },
953
+ /** Check if session has been established (read-only) */
686
954
  get is_identified() {
687
955
  return is_identified;
688
956
  },
957
+ /** Storage keys used by the module */
689
958
  storage_keys,
959
+ /** Initialize the module for a page */
690
960
  initiate,
961
+ /** Identify/establish session */
691
962
  identify,
963
+ /** Track a custom event */
692
964
  track,
693
- bounce
965
+ /** Track a bounce event */
966
+ bounce,
967
+ /** Cleanup the module */
968
+ destroy
694
969
  };
695
970
  };
696
971
 
@@ -700,11 +975,16 @@ var createLevoControl = (options) => {
700
975
  const {
701
976
  appName,
702
977
  workspace = null,
978
+ site = null,
703
979
  apiUrl = "https://public-api.levo.so",
704
980
  insightsUrl = "/.levo/insights/api",
981
+ directInsightsUrl,
982
+ insightsMode = "auto",
983
+ pingTimeout = 3e3,
705
984
  NODE_ENV = "",
706
985
  APP_MODE = "production"
707
986
  } = options;
987
+ const sanitizedPingTimeout = Number.isFinite(pingTimeout) && pingTimeout >= 0 ? pingTimeout : 3e3;
708
988
  if (workspace) {
709
989
  if (!workspaceIdRegex.test(workspace)) {
710
990
  throw new Error(
@@ -713,6 +993,7 @@ var createLevoControl = (options) => {
713
993
  }
714
994
  }
715
995
  let _workspace = workspace;
996
+ let _site = site;
716
997
  return {
717
998
  get workspace() {
718
999
  return _workspace;
@@ -727,6 +1008,12 @@ var createLevoControl = (options) => {
727
1008
  }
728
1009
  _workspace = value;
729
1010
  },
1011
+ get site() {
1012
+ return _site;
1013
+ },
1014
+ set site(value) {
1015
+ _site = value;
1016
+ },
730
1017
  get appName() {
731
1018
  return appName;
732
1019
  },
@@ -736,6 +1023,15 @@ var createLevoControl = (options) => {
736
1023
  get insightsUrl() {
737
1024
  return insightsUrl;
738
1025
  },
1026
+ get directInsightsUrl() {
1027
+ return directInsightsUrl || "";
1028
+ },
1029
+ get insightsMode() {
1030
+ return insightsMode;
1031
+ },
1032
+ get pingTimeout() {
1033
+ return sanitizedPingTimeout;
1034
+ },
739
1035
  get NODE_ENV() {
740
1036
  return NODE_ENV;
741
1037
  },
@@ -1295,16 +1591,30 @@ var AnalyticsEventsList = [
1295
1591
  "page.impression",
1296
1592
  "page.scroll",
1297
1593
  "page.view",
1298
- "page.metrics",
1299
1594
  "page.selection",
1300
- "page.speedtest",
1301
1595
  // User Activity Events
1302
1596
  "user.active",
1303
- "user.speedtest",
1304
1597
  "user.idle"
1305
1598
  ];
1306
1599
  var AnalyticsEvents = listToRecord(AnalyticsEventsList);
1307
1600
 
1601
+ // src/types/forum/post.ts
1602
+ var FORUM_POST_KIND = {
1603
+ post: "post",
1604
+ poll: "poll",
1605
+ gallery: "gallery",
1606
+ link: "link"
1607
+ };
1608
+ var ForumPostKindExpandedList = Object.keys(FORUM_POST_KIND);
1609
+
1610
+ // src/types/forum/comment.ts
1611
+ var COMMENT_STATUS = {
1612
+ active: "active",
1613
+ edited: "edited",
1614
+ deleted: "deleted",
1615
+ pending_moderation: "pending_moderation"
1616
+ };
1617
+
1308
1618
  // src/constants/collection/fieldInterfaceFormat.ts
1309
1619
  var formatsByInterface = {
1310
1620
  ArrayWidget: [],
@@ -1350,7 +1660,6 @@ var defaultByKinds = {
1350
1660
  record: null,
1351
1661
  group: [],
1352
1662
  collection: {
1353
- o2m: null,
1354
1663
  m2o: null,
1355
1664
  o2o: null,
1356
1665
  m2m: []
@@ -1515,6 +1824,7 @@ var LEVO_INTEGRATIONS = {
1515
1824
  event: "so.levo.event",
1516
1825
  membership: "so.levo.membership",
1517
1826
  payment: "so.levo.payment",
1827
+ community: "so.levo.community",
1518
1828
  search_console: "com.google.search_console"
1519
1829
  };
1520
1830
  var LEVO_INTEGRATIONS_LIST = Object.values(LEVO_INTEGRATIONS);
@@ -1530,4 +1840,40 @@ var formatImagePath = (string) => {
1530
1840
  return "";
1531
1841
  };
1532
1842
 
1533
- export { AnalyticsEvents, AnalyticsEventsList, CommonFieldIntefaces, CommonFieldInterfacesList, ComplexFieldInterfacesList, FieldInterfaces, FieldInterfacesList, FieldKind, FieldKindList, LEVO_INTEGRATIONS, LEVO_INTEGRATIONS_LIST, LevoError, LevoOAuthProviderList, LocalStorageLock, MediaKind, MediaKindList, createLevoClient, defaultByKinds, formatImagePath, formatsByInterface, getLevoError, interfaceKinds };
1843
+ // src/utils/lock.ts
1844
+ var LocalStorageLock = class {
1845
+ key;
1846
+ constructor(lockKey) {
1847
+ this.key = lockKey;
1848
+ }
1849
+ acquireLock(expiryMs = 5e3) {
1850
+ const now = Date.now();
1851
+ const lockValue = localStorage.getItem(this.key);
1852
+ if (lockValue) {
1853
+ const timestamp = Number(lockValue);
1854
+ if (now - timestamp < expiryMs) {
1855
+ return false;
1856
+ }
1857
+ }
1858
+ localStorage.setItem(this.key, now.toString());
1859
+ setTimeout(() => {
1860
+ if (this.isLocked(expiryMs)) {
1861
+ this.releaseLock();
1862
+ }
1863
+ }, expiryMs);
1864
+ return true;
1865
+ }
1866
+ releaseLock() {
1867
+ localStorage.removeItem(this.key);
1868
+ }
1869
+ isLocked(expiryMs = 5e3) {
1870
+ const lockValue = localStorage.getItem(this.key);
1871
+ if (!lockValue) {
1872
+ return false;
1873
+ }
1874
+ const timestamp = Number(lockValue);
1875
+ return Date.now() - timestamp < expiryMs;
1876
+ }
1877
+ };
1878
+
1879
+ export { AnalyticsEvents, AnalyticsEventsList, COMMENT_STATUS, CommonFieldIntefaces, CommonFieldInterfacesList, ComplexFieldInterfacesList, FORUM_POST_KIND, FieldInterfaces, FieldInterfacesList, FieldKind, FieldKindList, ForumPostKindExpandedList, LEVO_INTEGRATIONS, LEVO_INTEGRATIONS_LIST, LevoError, LevoOAuthProviderList, LocalStorageLock, MediaKind, MediaKindList, createLevoClient, defaultByKinds, formatImagePath, formatsByInterface, getLevoError, interfaceKinds };