@viji-dev/core 0.5.5 → 0.5.7

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.
@@ -100,6 +100,13 @@ class IFrameManager {
100
100
  iframeReadyPromise = null;
101
101
  iframeReadyResolver = null;
102
102
  iframeReadyRejecter = null;
103
+ /**
104
+ * 5s safety timer that rejects `iframeReadyPromise` if the inline-script
105
+ * never emits `iframe-ready` (e.g. blob script error). Promoted from
106
+ * closure-captured local to instance state so `destroy()` can clear it
107
+ * on cancellation (docs/14 rule 5).
108
+ */
109
+ iframeReadyTimeoutId = null;
103
110
  lockedOrigin = null;
104
111
  // Window message dispatch
105
112
  hostMessageListener = null;
@@ -159,6 +166,8 @@ class IFrameManager {
159
166
  this.iframeReadyResolver = resolve;
160
167
  this.iframeReadyRejecter = reject;
161
168
  });
169
+ this.iframeReadyPromise.catch(() => {
170
+ });
162
171
  this.installHostMessageListener(iframe);
163
172
  const iframeContent = this.generateIFrameHTML();
164
173
  const blob = new Blob([iframeContent], { type: "text/html" });
@@ -184,7 +193,8 @@ class IFrameManager {
184
193
  iframe.style.cssText = `position:absolute;top:0;left:0;width:100%;height:100%;border:none;${visibility}`;
185
194
  this.hostContainer.appendChild(iframe);
186
195
  }
187
- const timeoutId = setTimeout(() => {
196
+ this.iframeReadyTimeoutId = setTimeout(() => {
197
+ this.iframeReadyTimeoutId = null;
188
198
  if (this.iframeReadyRejecter) {
189
199
  const r = this.iframeReadyRejecter;
190
200
  this.iframeReadyResolver = null;
@@ -192,11 +202,6 @@ class IFrameManager {
192
202
  r(new VijiCoreError("IFrame load timeout", "IFRAME_TIMEOUT"));
193
203
  }
194
204
  }, 5e3);
195
- const originalResolver = this.iframeReadyResolver;
196
- this.iframeReadyResolver = () => {
197
- clearTimeout(timeoutId);
198
- originalResolver();
199
- };
200
205
  iframe.onerror = () => {
201
206
  if (this.iframeReadyRejecter) {
202
207
  this.iframeReadyRejecter(
@@ -323,6 +328,10 @@ class IFrameManager {
323
328
  switch (msg.type) {
324
329
  case "iframe-ready": {
325
330
  this.lockedOrigin = OPAQUE_ORIGIN;
331
+ if (this.iframeReadyTimeoutId !== null) {
332
+ clearTimeout(this.iframeReadyTimeoutId);
333
+ this.iframeReadyTimeoutId = null;
334
+ }
326
335
  if (this.iframeReadyResolver) {
327
336
  const r = this.iframeReadyResolver;
328
337
  this.iframeReadyResolver = null;
@@ -463,6 +472,16 @@ class IFrameManager {
463
472
  */
464
473
  destroy() {
465
474
  try {
475
+ if (this.iframeReadyTimeoutId !== null) {
476
+ clearTimeout(this.iframeReadyTimeoutId);
477
+ this.iframeReadyTimeoutId = null;
478
+ }
479
+ if (this.iframeReadyRejecter) {
480
+ const r = this.iframeReadyRejecter;
481
+ this.iframeReadyResolver = null;
482
+ this.iframeReadyRejecter = null;
483
+ r(new VijiCoreError("Initialization cancelled", "INITIALIZATION_CANCELLED"));
484
+ }
466
485
  if (this.iframe?.contentWindow && this.lockedOrigin) {
467
486
  try {
468
487
  this.postEnvelope({ type: "viji-terminate" });
@@ -670,6 +689,20 @@ class WorkerManager {
670
689
  */
671
690
  controlPort = null;
672
691
  onControlPortMessageBound = null;
692
+ /**
693
+ * Bootstrap-window cancellation state. The 10s `viji-ready` timer and
694
+ * the in-flight `vijiReady` rejecter are tracked on the instance so
695
+ * `destroy()` can cancel an in-progress bootstrap synchronously
696
+ * (clearing the timer + rejecting the awaiter with
697
+ * `INITIALIZATION_CANCELLED`). `isDestroyed` is the synchronous flag
698
+ * the outer catch in `createWorker` consults to distinguish a
699
+ * destroy-mid-init from a genuine bootstrap failure (e.g. the
700
+ * `postEnvelope` synchronous throw path, where no cancellation error
701
+ * was rejected through the awaiter).
702
+ */
703
+ bootstrapTimeoutId = null;
704
+ bootstrapReject = null;
705
+ isDestroyed = false;
673
706
  /**
674
707
  * Bootstraps the worker inside the iframe.
675
708
  *
@@ -723,7 +756,9 @@ class WorkerManager {
723
756
  }
724
757
  };
725
758
  const vijiReady = new Promise((resolve, reject) => {
726
- const timeoutId = setTimeout(() => {
759
+ this.bootstrapReject = reject;
760
+ this.bootstrapTimeoutId = setTimeout(() => {
761
+ this.bootstrapTimeoutId = null;
727
762
  reject(
728
763
  new VijiCoreError(
729
764
  "viji-ready timeout",
@@ -732,15 +767,29 @@ class WorkerManager {
732
767
  );
733
768
  }, 1e4);
734
769
  this.iframeManager.onVijiReady(() => {
735
- clearTimeout(timeoutId);
770
+ if (this.bootstrapTimeoutId !== null) {
771
+ clearTimeout(this.bootstrapTimeoutId);
772
+ this.bootstrapTimeoutId = null;
773
+ }
774
+ this.bootstrapReject = null;
736
775
  resolve();
737
776
  });
738
777
  });
739
- this.iframeManager.postEnvelope(
740
- bootstrap,
741
- collectBootstrapTransferables(bootstrap)
742
- );
743
- await vijiReady;
778
+ vijiReady.catch(() => {
779
+ });
780
+ try {
781
+ this.iframeManager.postEnvelope(
782
+ bootstrap,
783
+ collectBootstrapTransferables(bootstrap)
784
+ );
785
+ await vijiReady;
786
+ } finally {
787
+ if (this.bootstrapTimeoutId !== null) {
788
+ clearTimeout(this.bootstrapTimeoutId);
789
+ this.bootstrapTimeoutId = null;
790
+ }
791
+ this.bootstrapReject = null;
792
+ }
744
793
  this.controlPort = this.iframeManager.getControlPort1();
745
794
  if (this.controlPort && this.onWorkerMessageBound) {
746
795
  const dispatcher = this.onWorkerMessageBound;
@@ -752,6 +801,15 @@ class WorkerManager {
752
801
  this.postMessage("set-scene-code", { sceneCode: this.sceneCode });
753
802
  this.isInitialized = true;
754
803
  } catch (error) {
804
+ if (error instanceof VijiCoreError && error.code === "INITIALIZATION_CANCELLED") {
805
+ throw error;
806
+ }
807
+ if (this.isDestroyed) {
808
+ throw new VijiCoreError(
809
+ "Initialization cancelled",
810
+ "INITIALIZATION_CANCELLED"
811
+ );
812
+ }
755
813
  throw new VijiCoreError(
756
814
  `Failed to create worker: ${error}`,
757
815
  "WORKER_CREATION_ERROR",
@@ -856,6 +914,16 @@ class WorkerManager {
856
914
  */
857
915
  destroy() {
858
916
  try {
917
+ this.isDestroyed = true;
918
+ if (this.bootstrapTimeoutId !== null) {
919
+ clearTimeout(this.bootstrapTimeoutId);
920
+ this.bootstrapTimeoutId = null;
921
+ }
922
+ if (this.bootstrapReject) {
923
+ const r = this.bootstrapReject;
924
+ this.bootstrapReject = null;
925
+ r(new VijiCoreError("Initialization cancelled", "INITIALIZATION_CANCELLED"));
926
+ }
859
927
  this.pendingMessages.forEach(({ timeout, reject }) => {
860
928
  clearTimeout(timeout);
861
929
  reject(new VijiCoreError("Worker destroyed", "WORKER_DESTROYED"));
@@ -1897,7 +1965,7 @@ class EssentiaOnsetDetection {
1897
1965
  this.initPromise = (async () => {
1898
1966
  try {
1899
1967
  const essentiaModule = await import("./essentia.js-core.es-DnrJE0uR.js");
1900
- const wasmModule = await import("./essentia-wasm.web-C58CPq4U.js").then((n) => n.e);
1968
+ const wasmModule = await import("./essentia-wasm.web-htE1Skqw.js").then((n) => n.e);
1901
1969
  const EssentiaClass = essentiaModule.Essentia || essentiaModule.default?.Essentia || essentiaModule.default;
1902
1970
  let WASMModule = wasmModule.default || wasmModule.EssentiaWASM || wasmModule.default?.EssentiaWASM;
1903
1971
  if (!WASMModule) {
@@ -7422,6 +7490,7 @@ class AudioChannel {
7422
7490
  if (this.workletNode) {
7423
7491
  try {
7424
7492
  this.workletNode.port.onmessage = null;
7493
+ this.workletNode.port.close();
7425
7494
  this.workletNode.disconnect();
7426
7495
  } catch (_) {
7427
7496
  }
@@ -7471,6 +7540,14 @@ class AudioSystem {
7471
7540
  onsetLogBuffer = [];
7472
7541
  // Debug logging control
7473
7542
  debugMode = false;
7543
+ /**
7544
+ * One-way lifecycle flag flipped at the top of `resetAudioState()`.
7545
+ * Gates re-entry into `ensureAudioContext()` and the public stream-mutation
7546
+ * entry points so an in-flight async caller (e.g. a worker RPC firing
7547
+ * mid-teardown) cannot resurrect a fresh `AudioContext` after destroy
7548
+ * has already nulled the field. AudioSystem is single-use after reset.
7549
+ */
7550
+ destroyed = false;
7474
7551
  // Diagnostic logger
7475
7552
  diagnosticLogger = new DiagnosticLogger();
7476
7553
  // Industry-grade audio analysis modules (4-layer architecture)
@@ -8056,6 +8133,7 @@ class AudioSystem {
8056
8133
  * Handle audio stream update for main stream (called from VijiCore)
8057
8134
  */
8058
8135
  handleAudioStreamUpdate(data) {
8136
+ if (this.destroyed) return;
8059
8137
  try {
8060
8138
  if (data.audioStream) {
8061
8139
  this.setAudioStream(data.audioStream);
@@ -8072,6 +8150,9 @@ class AudioSystem {
8072
8150
  * Ensure the shared AudioContext is created and resumed.
8073
8151
  */
8074
8152
  async ensureAudioContext() {
8153
+ if (this.destroyed) {
8154
+ throw new Error("AudioSystem is destroyed");
8155
+ }
8075
8156
  if (!this.audioContext) {
8076
8157
  this.audioContext = new AudioContext();
8077
8158
  }
@@ -8123,6 +8204,7 @@ class AudioSystem {
8123
8204
  if (ch.workletNode) {
8124
8205
  try {
8125
8206
  ch.workletNode.port.onmessage = null;
8207
+ ch.workletNode.port.close();
8126
8208
  ch.workletNode.disconnect();
8127
8209
  } catch {
8128
8210
  }
@@ -8173,6 +8255,7 @@ class AudioSystem {
8173
8255
  * Set the main audio stream for analysis (preserves original public API surface).
8174
8256
  */
8175
8257
  async setAudioStream(audioStream) {
8258
+ if (this.destroyed) return;
8176
8259
  this.disconnectMainStream();
8177
8260
  this.resetEssentiaBandHistories();
8178
8261
  await this.connectChannel(this.mainChannel, audioStream, true);
@@ -8444,8 +8527,14 @@ class AudioSystem {
8444
8527
  /**
8445
8528
  * Reset all audio state (called when destroying).
8446
8529
  * Disconnects all channels, closes AudioContext, resets all modules.
8530
+ *
8531
+ * Returns a Promise that resolves once `AudioContext.close()` has fully
8532
+ * transitioned the context to `closed`. Callers tearing the system down
8533
+ * (e.g. `VijiCore.destroy()`) MUST await this; otherwise Blink keeps the
8534
+ * context alive in `Pending activities` and leaks ~4 MB per cycle.
8447
8535
  */
8448
- resetAudioState() {
8536
+ async resetAudioState() {
8537
+ this.destroyed = true;
8449
8538
  this.stopAnalysisLoop();
8450
8539
  this.stopStalenessTimer();
8451
8540
  this.stopIdleTicker();
@@ -8458,9 +8547,14 @@ class AudioSystem {
8458
8547
  this.mainChannel.audioState.isConnected = false;
8459
8548
  this.mainChannel.isAnalysisRunning = false;
8460
8549
  this.mainChannel.currentStream = null;
8461
- if (this.audioContext && this.audioContext.state !== "closed") {
8462
- this.audioContext.close();
8463
- this.audioContext = null;
8550
+ const ctxToClose = this.audioContext;
8551
+ this.audioContext = null;
8552
+ if (ctxToClose && ctxToClose.state !== "closed") {
8553
+ try {
8554
+ await ctxToClose.close();
8555
+ } catch (e) {
8556
+ this.debugLog(`AudioContext.close() rejected: ${e?.message ?? e}`);
8557
+ }
8464
8558
  }
8465
8559
  this.workletRegistered = false;
8466
8560
  this.onsetDetection.reset();
@@ -8482,6 +8576,7 @@ class AudioSystem {
8482
8576
  * @param stream The MediaStream to analyze
8483
8577
  */
8484
8578
  async addStream(streamIndex, stream) {
8579
+ if (this.destroyed) return;
8485
8580
  if (this.additionalChannels.has(streamIndex)) {
8486
8581
  this.removeStream(streamIndex);
8487
8582
  }
@@ -8511,6 +8606,7 @@ class AudioSystem {
8511
8606
  * Mirrors video's reinitializeAdditionalCoordinators pattern.
8512
8607
  */
8513
8608
  async reinitializeAdditionalChannels(streams, baseIndex) {
8609
+ if (this.destroyed) return;
8514
8610
  const toRemove = [];
8515
8611
  for (const [idx, ch] of this.additionalChannels) {
8516
8612
  if (idx >= baseIndex && idx < baseIndex + 100) {
@@ -8628,6 +8724,7 @@ class AudioSystem {
8628
8724
  // do when audio is active.
8629
8725
  // ─────────────────────────────────────────────────────────────────────────
8630
8726
  startIdleTicker() {
8727
+ if (this.destroyed) return;
8631
8728
  if (this.idleTickerHandle !== null) return;
8632
8729
  if (typeof requestAnimationFrame === "undefined") return;
8633
8730
  this.idleTickerLastTime = performance.now();
@@ -8642,6 +8739,7 @@ class AudioSystem {
8642
8739
  }
8643
8740
  tickIdle(now) {
8644
8741
  this.idleTickerHandle = null;
8742
+ if (this.destroyed) return;
8645
8743
  if (this.mainChannel.audioState.isConnected) return;
8646
8744
  const dtMs = Math.min(100, now - this.idleTickerLastTime);
8647
8745
  this.idleTickerLastTime = now;
@@ -10018,6 +10116,9 @@ class VijiCore {
10018
10116
  } catch (error) {
10019
10117
  this.isInitializing = false;
10020
10118
  await this.cleanup();
10119
+ if (error instanceof VijiCoreError && (error.code === "INITIALIZATION_CANCELLED" || error.code === "INSTANCE_DESTROYED")) {
10120
+ throw error;
10121
+ }
10021
10122
  throw new VijiCoreError(
10022
10123
  `Failed to initialize VijiCore: ${error}`,
10023
10124
  "INITIALIZATION_ERROR",
@@ -11630,7 +11731,7 @@ class VijiCore {
11630
11731
  }
11631
11732
  this.deviceAudioStreamIndices.clear();
11632
11733
  if (this.audioSystem) {
11633
- this.audioSystem.resetAudioState();
11734
+ await this.audioSystem.resetAudioState();
11634
11735
  this.audioSystem = null;
11635
11736
  }
11636
11737
  this.currentAudioStream = null;
@@ -11723,7 +11824,7 @@ function validateCoreStatePayload(state) {
11723
11824
  }
11724
11825
  return null;
11725
11826
  }
11726
- const VERSION = "0.5.5";
11827
+ const VERSION = "0.5.7";
11727
11828
  export {
11728
11829
  AudioSystem as A,
11729
11830
  VERSION as V,
@@ -11731,4 +11832,4 @@ export {
11731
11832
  VijiCoreError as b,
11732
11833
  getDefaultExportFromCjs as g
11733
11834
  };
11734
- //# sourceMappingURL=index-DsJxKERc.js.map
11835
+ //# sourceMappingURL=index-CwwVLcjs.js.map