@needle-tools/engine 5.1.12 → 5.1.13

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 (85) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/SKILL.md +1 -1
  3. package/dist/gltf-progressive-BnLTsXZw.umd.cjs +10 -0
  4. package/dist/gltf-progressive-D6RT9IkB.min.js +10 -0
  5. package/dist/{gltf-progressive-B4DQEeBz.js → gltf-progressive-DaeP5KBr.js} +527 -514
  6. package/dist/{needle-engine.bundle-DdH96IqH.umd.cjs → needle-engine.bundle-B_ZctaDt.umd.cjs} +146 -146
  7. package/dist/{needle-engine.bundle-BmdWLmLa.min.js → needle-engine.bundle-DR967VjU.min.js} +145 -145
  8. package/dist/{needle-engine.bundle-9m-VdGg2.js → needle-engine.bundle-DTvJ1zCJ.js} +7302 -7153
  9. package/dist/needle-engine.d.ts +23 -15
  10. package/dist/needle-engine.js +517 -518
  11. package/dist/needle-engine.min.js +1 -1
  12. package/dist/needle-engine.umd.cjs +1 -1
  13. package/lib/engine/api.d.ts +4 -3
  14. package/lib/engine/api.js +3 -9
  15. package/lib/engine/api.js.map +1 -1
  16. package/lib/engine/engine_addressables.d.ts +32 -1
  17. package/lib/engine/engine_addressables.js +71 -9
  18. package/lib/engine/engine_addressables.js.map +1 -1
  19. package/lib/engine/engine_application.d.ts +2 -0
  20. package/lib/engine/engine_application.js +7 -3
  21. package/lib/engine/engine_application.js.map +1 -1
  22. package/lib/engine/engine_context.js +25 -1
  23. package/lib/engine/engine_context.js.map +1 -1
  24. package/lib/engine/engine_init.js +2 -2
  25. package/lib/engine/engine_input.js +1 -1
  26. package/lib/engine/engine_input.js.map +1 -1
  27. package/lib/engine/engine_license.d.ts +7 -7
  28. package/lib/engine/engine_license.js +121 -78
  29. package/lib/engine/engine_license.js.map +1 -1
  30. package/lib/engine/engine_lifecycle_functions_internal.js +20 -2
  31. package/lib/engine/engine_lifecycle_functions_internal.js.map +1 -1
  32. package/lib/engine/engine_networking_blob.js +3 -3
  33. package/lib/engine/engine_networking_blob.js.map +1 -1
  34. package/lib/engine/engine_networking_streams.d.ts +3 -0
  35. package/lib/engine/engine_networking_streams.js +64 -17
  36. package/lib/engine/engine_networking_streams.js.map +1 -1
  37. package/lib/engine/engine_utils_qrcode.js +2 -2
  38. package/lib/engine/engine_utils_qrcode.js.map +1 -1
  39. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +5 -0
  40. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js.map +1 -1
  41. package/lib/engine/webcomponents/fonts.d.ts +1 -1
  42. package/lib/engine/webcomponents/fonts.js +16 -1
  43. package/lib/engine/webcomponents/fonts.js.map +1 -1
  44. package/lib/engine/webcomponents/icons.js +8 -1
  45. package/lib/engine/webcomponents/icons.js.map +1 -1
  46. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
  47. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  48. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +5 -0
  49. package/lib/engine/webcomponents/needle menu/needle-menu.js +34 -16
  50. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  51. package/lib/engine/webcomponents/needle-engine.d.ts +1 -0
  52. package/lib/engine/webcomponents/needle-engine.js +18 -6
  53. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  54. package/lib/engine/webcomponents/needle-engine.loading.d.ts +2 -0
  55. package/lib/engine/webcomponents/needle-engine.loading.js +12 -2
  56. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  57. package/lib/engine/xr/TempXRContext.js +2 -2
  58. package/lib/engine/xr/TempXRContext.js.map +1 -1
  59. package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
  60. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  61. package/package.json +2 -2
  62. package/plugins/common/license.js +4 -4
  63. package/plugins/vite/license.js +4 -4
  64. package/src/engine/api.ts +5 -3
  65. package/src/engine/engine_addressables.ts +75 -9
  66. package/src/engine/engine_application.ts +7 -2
  67. package/src/engine/engine_context.ts +25 -1
  68. package/src/engine/engine_init.ts +2 -2
  69. package/src/engine/engine_input.ts +2 -2
  70. package/src/engine/engine_license.ts +113 -75
  71. package/src/engine/engine_lifecycle_functions_internal.ts +21 -2
  72. package/src/engine/engine_networking_blob.ts +3 -3
  73. package/src/engine/engine_networking_streams.ts +61 -17
  74. package/src/engine/engine_utils_qrcode.ts +2 -2
  75. package/src/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +8 -1
  76. package/src/engine/webcomponents/fonts.ts +16 -1
  77. package/src/engine/webcomponents/icons.ts +8 -1
  78. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
  79. package/src/engine/webcomponents/needle menu/needle-menu.ts +33 -15
  80. package/src/engine/webcomponents/needle-engine.loading.ts +19 -7
  81. package/src/engine/webcomponents/needle-engine.ts +16 -6
  82. package/src/engine/xr/TempXRContext.ts +2 -2
  83. package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
  84. package/dist/gltf-progressive-CdjpU5eP.min.js +0 -10
  85. package/dist/gltf-progressive-DeZioRZe.umd.cjs +0 -10
@@ -7,21 +7,53 @@ import { SSR } from "./engine_ssr.js";
7
7
  import type { IContext } from "./engine_types.js";
8
8
  import { DeviceUtilities, getParam } from "./engine_utils.js";
9
9
 
10
+ // License UI polling must stop when its context goes away; otherwise timers retain scenes.
11
+ const licenseIntervals = new WeakMap<IContext, Set<ReturnType<typeof setInterval>>>();
12
+ const licenseTimeouts = new WeakMap<IContext, Set<ReturnType<typeof setTimeout>>>();
13
+ const disposedLicenseContexts = new WeakSet<IContext>();
14
+ ContextRegistry.registerCallback(ContextEvent.ContextDestroying, ({ context }) => {
15
+ disposedLicenseContexts.add(context);
16
+ for (const timer of licenseIntervals.get(context) ?? []) clearInterval(timer);
17
+ licenseIntervals.delete(context);
18
+ for (const timer of licenseTimeouts.get(context) ?? []) clearTimeout(timer);
19
+ licenseTimeouts.delete(context);
20
+ });
21
+ function setLicenseInterval(ctx: IContext, callback: () => void, delay: number) {
22
+ if (disposedLicenseContexts.has(ctx)) return undefined;
23
+ let timers = licenseIntervals.get(ctx);
24
+ if (!timers) licenseIntervals.set(ctx, timers = new Set());
25
+ const timer = setInterval(callback, delay);
26
+ timers.add(timer);
27
+ return timer;
28
+ }
29
+
30
+ function setLicenseTimeout(ctx: IContext, callback: () => void, delay: number) {
31
+ if (disposedLicenseContexts.has(ctx)) return undefined;
32
+ let timers = licenseTimeouts.get(ctx);
33
+ if (!timers) licenseTimeouts.set(ctx, timers = new Set());
34
+ const timer = setTimeout(() => {
35
+ timers.delete(timer);
36
+ callback();
37
+ }, delay);
38
+ timers.add(timer);
39
+ return timer;
40
+ }
41
+
10
42
  const debug = getParam("__debuglic__");
11
43
 
12
- const _cEnXH: ((result: boolean) => void)[] = [];
44
+ const _$zQIZ: ((result: boolean) => void)[] = [];
13
45
 
14
46
  // DO NOT EDIT MANUALLY
15
- let $ZtH: string = "";
47
+ let __VqzR: string = "";
16
48
  // eslint-disable-next-line prefer-const
17
- let $YjoJUHqQ: string = "";
49
+ let $OQRl: string = "";
18
50
 
19
51
  if (debug) {
20
- console.log("License Type: " + $ZtH);
21
- if ($YjoJUHqQ) {
22
- console.log("License JWT: " + $YjoJUHqQ);
52
+ console.log("License Type: " + __VqzR);
53
+ if ($OQRl) {
54
+ console.log("License JWT: " + $OQRl);
23
55
  try {
24
- const payload = JSON.parse(atob($YjoJUHqQ.split(".")[1].replace(/-/g, '+').replace(/_/g, '/')));
56
+ const payload = JSON.parse(atob($OQRl.split(".")[1].replace(/-/g, '+').replace(/_/g, '/')));
25
57
  console.log("License JWT payload:", payload);
26
58
  }
27
59
  catch { console.log("License JWT payload: (failed to decode)"); }
@@ -32,8 +64,8 @@ if (debug) {
32
64
  }
33
65
 
34
66
  /** @internal */
35
- export function _$VfAckLjB() {
36
- switch ($ZtH) {
67
+ export function __csRKN() {
68
+ switch (__VqzR) {
37
69
  case "pro":
38
70
  case "enterprise":
39
71
  return true;
@@ -42,8 +74,8 @@ export function _$VfAckLjB() {
42
74
  }
43
75
 
44
76
  /** @internal */
45
- export function _$EHSDVcNK() {
46
- switch ($ZtH) {
77
+ export function _qxvTsQJJ() {
78
+ switch (__VqzR) {
47
79
  case "indie":
48
80
  return true;
49
81
  }
@@ -51,8 +83,8 @@ export function _$EHSDVcNK() {
51
83
  }
52
84
 
53
85
  /** @internal */
54
- export function hTrHNMkg() {
55
- switch ($ZtH) {
86
+ export function $yVfpT() {
87
+ switch (__VqzR) {
56
88
  case "edu":
57
89
  return true;
58
90
  }
@@ -60,20 +92,26 @@ export function hTrHNMkg() {
60
92
  }
61
93
 
62
94
  /** @internal */
63
- export function $uxbFepO() {
64
- return _$VfAckLjB() || _$EHSDVcNK() || hTrHNMkg();
95
+ export function __PemN() {
96
+ return __csRKN() || _qxvTsQJJ() || $yVfpT();
65
97
  }
66
98
 
67
99
 
68
100
  /** @internal */
69
- export function OLF(cb: (result: boolean) => void) {
70
- if (_$VfAckLjB() || _$EHSDVcNK() || hTrHNMkg())
71
- return cb(true);
72
- _cEnXH.push(cb);
101
+ export function _$EPASa(cb: (result: boolean) => void) {
102
+ if (__csRKN() || _qxvTsQJJ() || $yVfpT()) {
103
+ cb(true);
104
+ return () => {};
105
+ }
106
+ _$zQIZ.push(cb);
107
+ return () => {
108
+ const index = _$zQIZ.indexOf(cb);
109
+ if (index >= 0) _$zQIZ.splice(index, 1);
110
+ };
73
111
  }
74
112
 
75
- function IAHYYGp(result: boolean) {
76
- for (const cb of _cEnXH) {
113
+ function _SUQSFYGK(result: boolean) {
114
+ for (const cb of _$zQIZ) {
77
115
  try {
78
116
  cb(result);
79
117
  }
@@ -88,7 +126,7 @@ function IAHYYGp(result: boolean) {
88
126
 
89
127
  // ECDSA P-256 public key for verifying license JWTs (verification-only, safe to ship)
90
128
  /* eslint-disable no-secrets/no-secrets -- public key, not a secret */
91
- const __uWGGJiBM = {
129
+ const _IIilbQX = {
92
130
  kty: "EC",
93
131
  crv: "P-256",
94
132
  x: "A34nyKMjhQYVgzeE4tyLUYdx34TAKogDa7v7PRaO9Lg",
@@ -112,7 +150,7 @@ function base64urlDecode(str: string): Uint8Array {
112
150
  * Verify a JWT license token and return the `type` claim if valid.
113
151
  * Returns null if the JWT is missing, malformed, or has an invalid signature.
114
152
  */
115
- async function tRvZ(jwt: string): Promise<string | null> {
153
+ async function _$powD(jwt: string): Promise<string | null> {
116
154
  if (!jwt) return null;
117
155
  try {
118
156
  const parts = jwt.split(".");
@@ -122,7 +160,7 @@ async function tRvZ(jwt: string): Promise<string | null> {
122
160
 
123
161
  const key = await crypto.subtle.importKey(
124
162
  "jwk",
125
- __uWGGJiBM,
163
+ _IIilbQX,
126
164
  { name: "ECDSA", namedCurve: "P-256" },
127
165
  false,
128
166
  ["verify"]
@@ -182,24 +220,24 @@ async function tRvZ(jwt: string): Promise<string | null> {
182
220
  }
183
221
 
184
222
  /** Verify the injected JWT and update the license type if valid.
185
- * The engine ONLY trusts the JWT — the plain $ZtH string is ignored. */
223
+ * The engine ONLY trusts the JWT — the plain __VqzR string is ignored. */
186
224
  let _jwtVerificationPromise: Promise<void> | undefined = undefined;
187
- async function _$TVYwursi(): Promise<void> {
188
- // NOTE: do NOT add an `if (!$YjoJUHqQ) return` guard here.
225
+ async function __EYbYX(): Promise<void> {
226
+ // NOTE: do NOT add an `if (!$OQRl) return` guard here.
189
227
  // esbuild (Vite dependency pre-bundling) would see the variable as constant ""
190
228
  // and tree-shake the entire JWT verification code path.
191
- const jwt = $YjoJUHqQ;
229
+ const jwt = $OQRl;
192
230
  // Clear after reading — this reassignment also prevents esbuild from
193
231
  // constant-folding the variable (it now has multiple assignment sites).
194
- $YjoJUHqQ = "";
195
- const verifiedType = await tRvZ(jwt);
232
+ $OQRl = "";
233
+ const verifiedType = await _$powD(jwt);
196
234
  if (verifiedType) {
197
- $ZtH = verifiedType;
235
+ __VqzR = verifiedType;
198
236
  if (debug) console.log("License type set from verified JWT: " + verifiedType);
199
- IAHYYGp($uxbFepO());
237
+ _SUQSFYGK(__PemN());
200
238
  }
201
239
  else {
202
- $ZtH = "basic";
240
+ __VqzR = "basic";
203
241
  if (debug && jwt) console.warn("JWT verification failed — license reset to basic");
204
242
  }
205
243
  }
@@ -650,18 +688,18 @@ export namespace Telemetry {
650
688
  }
651
689
 
652
690
 
653
- export function $LgNkf() {
691
+ export function _$oLIu() {
654
692
 
655
- if($ZtH === "") $ZtH = "basic";
693
+ if(__VqzR === "") __VqzR = "basic";
656
694
 
657
695
  // Start JWT verification — must be here (not top-level) to avoid tree-shaking
658
- _jwtVerificationPromise = _$TVYwursi();
696
+ _jwtVerificationPromise = __EYbYX();
659
697
 
660
698
  Telemetry.init();
661
699
  ContextRegistry.registerCallback(ContextEvent.ContextRegistered, evt => {
662
- _$xBCv(evt.context);
663
- __iMpjhj(evt.context);
664
- setTimeout(() => _ZQiK(evt.context), 2000);
700
+ gep(evt.context);
701
+ _$TNE(evt.context);
702
+ setLicenseTimeout(evt.context, () => cVal(evt.context), 2000);
665
703
  });
666
704
  // ContextCreated fires after the renderer exists, so the canvas is available for context-loss monitoring
667
705
  ContextRegistry.registerCallback(ContextEvent.ContextCreated, evt => {
@@ -670,17 +708,17 @@ export function $LgNkf() {
670
708
  });
671
709
  }
672
710
 
673
- export let UxRDt: Promise<void> | undefined = undefined;
711
+ export let _$zwcLl: Promise<void> | undefined = undefined;
674
712
  let applicationIsForbidden = false;
675
713
  let applicationForbiddenText = "";
676
- async function _dFNhgjF() {
714
+ async function _kAPEtsE() {
677
715
  // Only perform the runtime license check once
678
- if (UxRDt) return UxRDt;
716
+ if (_$zwcLl) return _$zwcLl;
679
717
  // Wait for JWT verification to complete first (if running)
680
718
  if (_jwtVerificationPromise) {
681
719
  await _jwtVerificationPromise;
682
720
  }
683
- if ($ZtH === "basic") {
721
+ if (__VqzR === "basic") {
684
722
  try {
685
723
  const licenseUrl = "https://needle.tools/api/v1/needle-engine/check?location=" + encodeURIComponent(window.location.href) + "&version=" + VERSION + "&generator=" + encodeURIComponent(GENERATOR);
686
724
  const res = await fetch(licenseUrl, {
@@ -692,29 +730,29 @@ async function _dFNhgjF() {
692
730
  if (res?.status === 200) {
693
731
  applicationIsForbidden = false;
694
732
  if (debug) console.log("License check succeeded");
695
- $ZtH = "pro";
696
- IAHYYGp(true);
733
+ __VqzR = "pro";
734
+ _SUQSFYGK(true);
697
735
  }
698
736
  else if (res?.status === 403) {
699
- IAHYYGp(false);
737
+ _SUQSFYGK(false);
700
738
  applicationIsForbidden = true;
701
739
  applicationForbiddenText = await res.text();
702
740
  }
703
741
  else {
704
- IAHYYGp(false);
742
+ _SUQSFYGK(false);
705
743
  if (debug) console.log("License check failed with status " + res?.status);
706
744
  }
707
745
  }
708
746
  catch (err) {
709
- IAHYYGp(false);
747
+ _SUQSFYGK(false);
710
748
  if (debug) console.error("License check failed", err);
711
749
  }
712
750
  }
713
- else if (debug) console.log("Runtime license check is skipped because license is already applied as \"" + $ZtH + "\"");
751
+ else if (debug) console.log("Runtime license check is skipped because license is already applied as \"" + __VqzR + "\"");
714
752
  }
715
- UxRDt = _dFNhgjF();
753
+ _$zwcLl = _kAPEtsE();
716
754
 
717
- async function __iMpjhj(ctx: IContext) {
755
+ async function _$TNE(ctx: IContext) {
718
756
  function createForbiddenElement() {
719
757
  const div = document.createElement("div");
720
758
  div.className = "needle-forbidden";
@@ -755,7 +793,7 @@ async function __iMpjhj(ctx: IContext) {
755
793
  const expectedTextStyle = text.style.cssText;
756
794
  const forbiddenText = applicationForbiddenText?.length > 1 ? applicationForbiddenText : "This web application has been paused.<br/>You might be in violation of the Needle Engine terms of use.<br/>Please contact the Needle support if you think this is a mistake.";
757
795
  text.innerHTML = forbiddenText;
758
- setInterval(() => {
796
+ setLicenseInterval(ctx, () => {
759
797
  if (text.innerHTML !== forbiddenText) text.innerHTML = forbiddenText;
760
798
  if (text.parentNode !== div) div.appendChild(text);
761
799
  if (div.style.cssText !== expectedStyle) div.style.cssText = expectedStyle;
@@ -765,7 +803,7 @@ async function __iMpjhj(ctx: IContext) {
765
803
  }
766
804
  let forbiddenElement = createForbiddenElement();
767
805
  const expectedCSS = forbiddenElement.style.cssText;
768
- setInterval(() => {
806
+ setLicenseInterval(ctx, () => {
769
807
  if (applicationIsForbidden === true) {
770
808
  if (forbiddenElement.style.cssText !== expectedCSS) forbiddenElement = createForbiddenElement();
771
809
  if (ctx.domElement.shadowRoot) {
@@ -779,40 +817,40 @@ async function __iMpjhj(ctx: IContext) {
779
817
  }, 500)
780
818
  }
781
819
 
782
- async function _$xBCv(ctx: IContext) {
820
+ async function gep(ctx: IContext) {
783
821
  try {
784
- if (!_$VfAckLjB() && !_$EHSDVcNK()) {
785
- return WksKzhWY(ctx);
822
+ if (!__csRKN() && !_qxvTsQJJ()) {
823
+ return $zQrLfV(ctx);
786
824
  }
787
825
  }
788
826
  catch (err) {
789
827
  if (debug) console.log("License check failed", err)
790
- return WksKzhWY(ctx)
828
+ return $zQrLfV(ctx)
791
829
  }
792
- if (debug) WksKzhWY(ctx)
830
+ if (debug) $zQrLfV(ctx)
793
831
  }
794
832
 
795
833
 
796
834
 
797
- async function WksKzhWY(ctx: IContext) {
835
+ async function $zQrLfV(ctx: IContext) {
798
836
 
799
837
  // if the engine loads faster than the license check, we need to capture the ready event here
800
838
  let isReady = false;
801
839
  ctx.domElement.addEventListener("ready", () => isReady = true);
802
840
 
803
- await UxRDt?.catch(() => { });
841
+ await _$zwcLl?.catch(() => { });
804
842
 
805
843
 
806
- if (_$VfAckLjB() || _$EHSDVcNK()) return;
807
- if ($uxbFepO() === false) _yCdKI();
844
+ if (__csRKN() || _qxvTsQJJ()) return;
845
+ if (__PemN() === false) _$Obysw();
808
846
 
809
847
  // check if the engine is already ready (meaning has finished loading)
810
848
  if (isReady) {
811
- __hUZrkc(ctx);
849
+ __HBEW(ctx);
812
850
  }
813
851
  else {
814
852
  ctx.domElement.addEventListener("ready", () => {
815
- __hUZrkc(ctx);
853
+ __HBEW(ctx);
816
854
  });
817
855
  }
818
856
  }
@@ -820,7 +858,7 @@ async function WksKzhWY(ctx: IContext) {
820
858
  // const licenseElementIdentifier = "needle-license-element";
821
859
  // const licenseDuration = 10000;
822
860
  // const licenseDelay = 1200;
823
- function __hUZrkc(ctx: IContext) {
861
+ function __HBEW(ctx: IContext) {
824
862
 
825
863
  const style = `
826
864
  position: relative;
@@ -833,7 +871,7 @@ function __hUZrkc(ctx: IContext) {
833
871
  padding: 10px;
834
872
  padding-left: 30px;
835
873
  `;
836
- if ($ZtH === "edu") {
874
+ if (__VqzR === "edu") {
837
875
  if (navigator.webdriver) {
838
876
  console.log("This project is supported by Needle for Education – https://needle.tools");
839
877
  }
@@ -871,7 +909,7 @@ function __hUZrkc(ctx: IContext) {
871
909
  banner.style.cssText = bannerStyle;
872
910
  banner.addEventListener("click", () => { window.open("https://needle.tools", "_blank") });
873
911
  let expectedBannerStyle = banner.style.cssText;
874
- setTimeout(() => {
912
+ setLicenseTimeout(ctx, () => {
875
913
  bannerStyle = bannerStyle.replace("opacity: 0", "opacity: 1");
876
914
  bannerStyle = bannerStyle.replace("transform: translateY(10px)", "transform: translateY(0)");
877
915
  banner.style.cssText = bannerStyle;
@@ -879,7 +917,7 @@ function __hUZrkc(ctx: IContext) {
879
917
  }, 100);
880
918
 
881
919
  // ensure the banner is always visible
882
- const interval = setInterval(() => {
920
+ const interval = setLicenseInterval(ctx, () => {
883
921
  const parent = ctx.domElement.shadowRoot || ctx.domElement;
884
922
  if (banner.parentNode !== parent) {
885
923
  parent.appendChild(banner);
@@ -890,16 +928,16 @@ function __hUZrkc(ctx: IContext) {
890
928
  }
891
929
  }, 1000);
892
930
 
893
- if (hTrHNMkg()) {
931
+ if ($yVfpT()) {
894
932
  const removeDelay = 20_000;
895
- setTimeout(() => {
933
+ setLicenseTimeout(ctx, () => {
896
934
  clearInterval(interval);
897
935
  banner?.remove();
898
936
  // show the logo every x minutes
899
937
  const intervalInMinutes = 5;
900
- setTimeout(() => {
938
+ setLicenseTimeout(ctx, () => {
901
939
  if (ctx.domElement.parentNode)
902
- __hUZrkc(ctx);
940
+ __HBEW(ctx);
903
941
  }, 1000 * 60 * intervalInMinutes)
904
942
  }, removeDelay);
905
943
  }
@@ -910,7 +948,7 @@ function __hUZrkc(ctx: IContext) {
910
948
  const base64Logo = "data:image/webp;base64,UklGRrABAABXRUJQVlA4WAoAAAAQAAAAHwAAHwAAQUxQSKEAAAARN6CmbSM4WR7vdARON11EBDq3fLiNbVtVzpMCPlKAEzsx0Y/x+Ovuv4dn0EFE/ydAvz6YggXzgh5sVgXM/zOC/4sii7qgGvB5N7hmuQYwkvazWAu1JPW41FXSHq6pnaQWvqYH18Fc0j1hO/BFTtIeSBlJi5w6qIIO7IOrwhFsB2Yxukif0FTRLpXswHR8MxbslKe9VZsn/Ub5C7YFOpqSTABWUDgg6AAAAFAGAJ0BKiAAIAA+7VyoTqmkpCI3+qgBMB2JbACdMt69DwMIQBLhkTO6XwY00UEDK6cNIDnuNibPf0EgAP7Y1myuiQHLDsF/0h5unrGh6WAbv7aegg2ZMd3uRKfT/3SJztcaujYfTvMXspfCTmYcoO6a+vhC3ss4M8uM58t4siiu59I4aOl59e9Sr6xoxYlHf2v+NnBNpJYeJf8jABQAId/PXuBkLEFkiCucgSGEcfhvajql/j3reCGl0M5/9gQWy7ayNPs+wlvIxFnNfSlfuND4CZOCyxOHhRqOmHN4ULHo3tCSrUNvgAA=";
911
949
 
912
950
  let lastLogTime = 0;
913
- async function _yCdKI(_logo?: string) {
951
+ async function _$Obysw(_logo?: string) {
914
952
  const now = Date.now();
915
953
  if (now - lastLogTime < 2000) return;
916
954
  lastLogTime = now;
@@ -943,7 +981,7 @@ async function _yCdKI(_logo?: string) {
943
981
  }
944
982
 
945
983
 
946
- async function _ZQiK(context: IContext) {
984
+ async function cVal(context: IContext) {
947
985
  // isAllowed() now also covers cross-origin-isolated pages (where beacons can't be sent)
948
986
  // and Do Not Track / GPC.
949
987
  if (!Telemetry.isAllowed(context)) {
@@ -957,7 +995,7 @@ async function _ZQiK(context: IContext) {
957
995
 
958
996
  // current url without query parameters
959
997
  const currentUrl = window.location.href.split("?")[0];
960
- const license = $ZtH;
998
+ const license = __VqzR;
961
999
 
962
1000
  const beaconData = {
963
1001
  license,
@@ -148,13 +148,27 @@ function internalInvokeLifecycleFunctions(ctx: Context, evt: Event) {
148
148
  }
149
149
  }
150
150
 
151
- const bufferArray = new Array<RegisteredLifecycleMethod>();
151
+ const buffers: Array<Array<RegisteredLifecycleMethod>> = [];
152
+ let invocationDepth = 0;
152
153
  const hookContext: LifecycleHookContext = {
153
154
  context: null as any as Context
154
155
  };
155
156
 
156
157
  function invoke(ctx: Context, methods: Array<RegisteredLifecycleMethod>, invokeOnce: boolean) {
157
- bufferArray.length = 0;
158
+ // Reuse storage per nesting depth: hooks may synchronously invoke other hooks.
159
+ const bufferArray = buffers[invocationDepth] ?? (buffers[invocationDepth] = []);
160
+ invocationDepth++;
161
+ try {
162
+ invokeWithBuffer(ctx, methods, invokeOnce, bufferArray);
163
+ }
164
+ finally {
165
+ // One-time/unregistered callbacks can capture entire scenes as well.
166
+ bufferArray.length = 0;
167
+ invocationDepth--;
168
+ }
169
+ }
170
+
171
+ function invokeWithBuffer(ctx: Context, methods: Array<RegisteredLifecycleMethod>, invokeOnce: boolean, bufferArray: Array<RegisteredLifecycleMethod>) {
158
172
  for (let i = 0; i < methods.length; i++) {
159
173
  bufferArray.push(methods[i]);
160
174
  }
@@ -168,6 +182,7 @@ function invoke(ctx: Context, methods: Array<RegisteredLifecycleMethod>, invokeO
168
182
  }
169
183
 
170
184
  if (invoke) {
185
+ const previousContext = hookContext.context;
171
186
  try {
172
187
  hookContext.context = ctx;
173
188
  entry.method?.call(hookContext, ctx);
@@ -175,6 +190,10 @@ function invoke(ctx: Context, methods: Array<RegisteredLifecycleMethod>, invokeO
175
190
  catch (e) {
176
191
  console.error("Error in lifecycle method", e);
177
192
  }
193
+ finally {
194
+ // This shared receiver must not keep the last invoked context alive.
195
+ hookContext.context = previousContext;
196
+ }
178
197
  }
179
198
 
180
199
  // Remove the method if it's a one time call
@@ -1,7 +1,7 @@
1
1
  import { FileLoader } from "three";
2
2
 
3
3
  import { showBalloonWarning } from "./debug/index.js";
4
- import { $uxbFepO } from "./engine_license.js";
4
+ import { __PemN } from "./engine_license.js";
5
5
  import { delay } from "./engine_utils.js";
6
6
  import { md5AsBytes, md5Hex, sha256Base64 } from "./engine_utils_hash.js";
7
7
 
@@ -45,7 +45,7 @@ export namespace BlobStorage {
45
45
  */
46
46
  export function canUpload(info: { filesize: number }) {
47
47
  const sizeInMB = info.filesize / 1024 / 1024;
48
- if ($uxbFepO()) {
48
+ if (__PemN()) {
49
49
  return sizeInMB < maxSizeInMB;
50
50
  }
51
51
  return sizeInMB < maxFreeSizeInMB;
@@ -100,7 +100,7 @@ export namespace BlobStorage {
100
100
  console.warn(`Your file is too large for uploading (${filesizeInMB.toFixed(1)}MB). Max allowed size is ${maxSizeInMB}MB`);
101
101
  return null;
102
102
  }
103
- else if (!$uxbFepO() && filesizeInMB > maxFreeSizeInMB) {
103
+ else if (!__PemN() && filesizeInMB > maxFreeSizeInMB) {
104
104
  if (opts?.silent !== true) showBalloonWarning(`File is too large for uploading. Please get a <a href=\"https://needle.tools/pricing\" target=\"_blank\">commercial license</a> to upload files larger than 5MB`);
105
105
  console.warn(`Your file is too large for uploading (${filesizeInMB.toFixed(1)}MB). Max size is 5MB for non-commercial users. Please get a commercial license at https://needle.tools/pricing for larger files (up to 50MB)`);
106
106
  return null;
@@ -8,6 +8,7 @@ import { getPeerjsInstance } from "../engine/engine_networking_peer.js";
8
8
  import { showBalloonMessage } from "./debug/index.js";
9
9
  import { Application } from "./engine_application.js";
10
10
  import { Context } from "./engine_context.js";
11
+ import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
11
12
  import type { IModel } from "./engine_networking_types.js";
12
13
  import { type IComponent, isComponent } from "./engine_types.js";
13
14
  import { getParam } from "./engine_utils.js";
@@ -121,17 +122,31 @@ function applySdpTransform(sdp) {
121
122
 
122
123
  export class PeerHandle extends EventDispatcher<any> {
123
124
 
124
- private static readonly instances: Map<string, PeerHandle> = new Map();
125
+ // Weak ownership prevents this cache from rooting a context and its scene indefinitely.
126
+ // IDs are scoped to contexts: separate engine elements may use the same stream ID.
127
+ private static readonly instances = new WeakMap<Context, Map<string, PeerHandle>>();
128
+
129
+ static {
130
+ ContextRegistry.registerCallback(ContextEvent.ContextDestroying, ({ context }) => {
131
+ const handles = PeerHandle.instances.get(context as Context);
132
+ if (!handles) return;
133
+ PeerHandle.instances.delete(context as Context);
134
+ for (const handle of handles.values()) handle.dispose();
135
+ handles.clear();
136
+ });
137
+ }
125
138
 
126
139
  static getOrCreate(context: Context, guid: string): PeerHandle {
127
- // if (id === undefined) {
128
- // // randomId
129
- // id = Math.random().toFixed(5);
130
- // }
131
- if (PeerHandle.instances.has(guid))
132
- return PeerHandle.instances.get(guid)!;
133
- const peer = new PeerHandle(context, guid);
134
- PeerHandle.instances.set(guid, peer);
140
+ let handles = PeerHandle.instances.get(context);
141
+ if (!handles) {
142
+ handles = new Map();
143
+ PeerHandle.instances.set(context, handles);
144
+ }
145
+ let peer = handles.get(guid);
146
+ if (!peer) {
147
+ peer = new PeerHandle(context, guid);
148
+ handles.set(guid, peer);
149
+ }
135
150
  return peer;
136
151
  }
137
152
 
@@ -151,6 +166,7 @@ export class PeerHandle extends EventDispatcher<any> {
151
166
  }
152
167
 
153
168
  makeCall(peerId: string, stream: MediaStream): CallHandle | undefined {
169
+ if (this._disposed) return undefined;
154
170
  if (!stream?.id) {
155
171
  if (debug) console.warn("Can not make a call: mediastream has no id or is undefined");
156
172
  else console.debug("Can not make a call: mediastream has no id or is undefined");
@@ -179,10 +195,8 @@ export class PeerHandle extends EventDispatcher<any> {
179
195
  }
180
196
 
181
197
  closeAll() {
182
- for (const call of this._incomingCalls) {
183
- call.close();
184
- }
185
- for (const call of this._outgoingCalls) {
198
+ // Closing a call synchronously removes it from the original array.
199
+ for (const call of [...this._incomingCalls, ...this._outgoingCalls]) {
186
200
  call.close();
187
201
  }
188
202
  this.updateCalls();
@@ -253,6 +267,23 @@ export class PeerHandle extends EventDispatcher<any> {
253
267
  }
254
268
  }
255
269
 
270
+ private _disposed = false;
271
+ private readonly _callIntervals = new Set<ReturnType<typeof setInterval>>();
272
+
273
+ private dispose() {
274
+ if (this._disposed) return;
275
+ this._disposed = true;
276
+ this.disable();
277
+ this.unsubscribePeerEvents();
278
+ for (const interval of this._callIntervals) clearInterval(interval);
279
+ this._callIntervals.clear();
280
+ this.closeAll();
281
+ this._incomingCalls.length = 0;
282
+ this._outgoingCalls.length = 0;
283
+ this._peer?.destroy();
284
+ this._peer = undefined;
285
+ }
286
+
256
287
  private _enabled: boolean = false;
257
288
  private _enabledPeer: boolean = false;
258
289
  private onConnectRoomFn: () => void = this.onConnectRoom.bind(this);
@@ -262,7 +293,7 @@ export class PeerHandle extends EventDispatcher<any> {
262
293
  // private _connectionPeerIdMap : Map<string, string> = new Map();
263
294
 
264
295
  enable() {
265
- if (this._enabled) return;
296
+ if (this._disposed || this._enabled) return;
266
297
  this._enabled = true;
267
298
  this.context.connection.beginListen(RoomEvents.JoinedRoom, this.onConnectRoomFn);
268
299
  // this.context.connection.beginListen(RoomEvents.UserJoinedRoom, this.onUserJoinedOrLeftRoomFn);
@@ -287,13 +318,19 @@ export class PeerHandle extends EventDispatcher<any> {
287
318
  // };
288
319
 
289
320
  private async setupPeer() {
290
- if (!this.context.connection.connectionId) return;
321
+ if (this._disposed || !this.context.connection.connectionId) return;
291
322
  if (this._enabledPeer) return;
292
323
  this._enabledPeer = true;
293
324
  if (!this._peer) {
294
325
  const peerId = this.getMyPeerId();
295
- if (peerId)
296
- this._peer = await getPeerjsInstance(peerId);
326
+ if (peerId) {
327
+ const peer = await getPeerjsInstance(peerId);
328
+ if (this._disposed) {
329
+ peer.destroy();
330
+ return;
331
+ }
332
+ this._peer = peer;
333
+ }
297
334
  else console.error("Failed to setup peerjs because we dont have a connection id", this.context.connection.connectionId);
298
335
  }
299
336
  if (this._enabled)
@@ -415,6 +452,7 @@ export class PeerHandle extends EventDispatcher<any> {
415
452
  });
416
453
 
417
454
  call.on("stream", () => {
455
+ if (this._disposed) return;
418
456
  if (debug) console.log("Received stream for call", call.metadata);
419
457
  // workaround for https://github.com/peers/peerjs/issues/636
420
458
  let intervalCounter = 0;
@@ -424,9 +462,15 @@ export class PeerHandle extends EventDispatcher<any> {
424
462
  if (debug) console.warn("Close call because stream is not active", call.metadata);
425
463
  intervalCounter += 1;
426
464
  clearInterval(closeInterval);
465
+ this._callIntervals.delete(closeInterval);
427
466
  handle.close();
428
467
  }
429
468
  }, 2000);
469
+ this._callIntervals.add(closeInterval);
470
+ call.on("close", () => {
471
+ clearInterval(closeInterval);
472
+ this._callIntervals.delete(closeInterval);
473
+ });
430
474
  });
431
475
  }
432
476
  return handle;
@@ -4,7 +4,7 @@ import { Quaternion, Vector2, Vector3, Vector4 } from "three";
4
4
 
5
5
  import { needleLogoOnlySVG } from "./assets/index.js";
6
6
  import { isDevEnvironment } from "./debug/debug.js";
7
- import { $uxbFepO } from "./engine_license.js";
7
+ import { __PemN } from "./engine_license.js";
8
8
  import { InternalAttributeUtils } from "./engine_utils_attributes.js";
9
9
  import type { NeedleEngineWebComponent } from "./webcomponents/needle-engine.js";
10
10
 
@@ -135,7 +135,7 @@ async function internalRenderQRCodeOverlays(canvas: HTMLCanvasElement, args: { s
135
135
  console.debug("[QR Code] No web component found")
136
136
  }
137
137
 
138
- const canUseCustomLogo = $uxbFepO();
138
+ const canUseCustomLogo = __PemN();
139
139
 
140
140
  // Query logo src from needle-engine attribute.
141
141
  // For any supported attribute it's possible to use "falsey" values (e.g. "0" or "false" to disable the logo in the QR code)