@wvdsh/sdk-js 1.3.26 → 1.3.29

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 CHANGED
@@ -84,6 +84,56 @@ var WavedashEvents = {
84
84
  // src/services/audio.ts
85
85
  import { IFRAME_MESSAGE_TYPE } from "@wvdsh/api";
86
86
 
87
+ // src/utils/logger.ts
88
+ var LOG_LEVEL = {
89
+ DEBUG: 0,
90
+ // Most verbose
91
+ INFO: 1,
92
+ WARN: 2,
93
+ ERROR: 3
94
+ };
95
+ var WavedashLogger = class {
96
+ constructor(logLevel = LOG_LEVEL.WARN) {
97
+ this.logLevel = logLevel;
98
+ }
99
+ setLogLevel(level) {
100
+ this.logLevel = level;
101
+ }
102
+ debug(message, ...args) {
103
+ if (this.logLevel <= LOG_LEVEL.DEBUG) {
104
+ console.log(`[WavedashJS] ${message}`, ...args);
105
+ }
106
+ }
107
+ info(message, ...args) {
108
+ if (this.logLevel <= LOG_LEVEL.INFO) {
109
+ console.log(`[WavedashJS] ${message}`, ...args);
110
+ }
111
+ }
112
+ warn(message, ...args) {
113
+ if (this.logLevel <= LOG_LEVEL.WARN) {
114
+ console.warn(`[WavedashJS] ${message}`, ...args);
115
+ }
116
+ }
117
+ error(message, ...args) {
118
+ if (this.logLevel <= LOG_LEVEL.ERROR) {
119
+ console.error(`[WavedashJS] ${message}`, ...args);
120
+ }
121
+ }
122
+ };
123
+ var logger = new WavedashLogger();
124
+
125
+ // src/utils/parentOrigin.ts
126
+ var _parentOrigin = "";
127
+ function setParentOrigin(origin) {
128
+ _parentOrigin = origin;
129
+ }
130
+ function getParentOrigin() {
131
+ return _parentOrigin;
132
+ }
133
+ function hasParentFrame() {
134
+ return _parentOrigin !== "";
135
+ }
136
+
87
137
  // src/services/manager.ts
88
138
  var WavedashManager = class {
89
139
  constructor(sdk) {
@@ -94,69 +144,30 @@ var WavedashManager = class {
94
144
  };
95
145
 
96
146
  // src/services/audio.ts
97
- var WeakRefSet = class {
98
- constructor() {
99
- this.set = /* @__PURE__ */ new Set();
100
- }
101
- add(value) {
102
- for (const ref of this.set) {
103
- if (ref.deref() === value) return;
104
- }
105
- this.set.add(new WeakRef(value));
106
- }
107
- forEach(callback) {
108
- for (const ref of this.set) {
109
- const v = ref.deref();
110
- if (v === void 0) this.set.delete(ref);
111
- else callback(v);
112
- }
113
- }
114
- clear() {
115
- this.set.clear();
116
- }
117
- };
118
147
  var AudioManager = class extends WavedashManager {
119
148
  constructor(sdk) {
120
149
  super(sdk);
121
150
  this._isMuted = false;
122
- // Web Audio contexts and their master gain nodes
123
- this.contexts = /* @__PURE__ */ new Map();
124
- // HTML media elements we know about + their game-intended muted state
125
- this.elements = new WeakRefSet();
126
- this.intendedMuted = /* @__PURE__ */ new WeakMap();
127
- // Speech synthesis utterances + their game-intended volume
128
- this.intendedUtteranceVolume = /* @__PURE__ */ new WeakMap();
129
- // Originals (restored on destroy)
130
- this.originalAudioContext = null;
131
- this.originalWebKitAudioContext = null;
132
- this.originalAudio = null;
133
- this.originalMutedDescriptor = null;
134
- this.originalPlay = null;
135
- this.originalSpeak = null;
136
- this.originalUtteranceVolumeDescriptor = null;
137
- this.mutationObserver = null;
151
+ // One shim per frame we've attached to.
152
+ this.frames = /* @__PURE__ */ new Set();
153
+ // Per-iframe state so we can re-shim across navigations/swaps and tear down
154
+ // the right frame when an iframe is removed or re-navigates. Keyed on the
155
+ // Document (replaced on every navigation), not contentWindow (a stable
156
+ // WindowProxy that survives navigations and so can't reveal a realm change).
157
+ this.iframeBindings = /* @__PURE__ */ new WeakMap();
158
+ this.iframeLoadHandlers = /* @__PURE__ */ new Map();
159
+ this.boundIframes = /* @__PURE__ */ new Set();
138
160
  this.handleMute = (data) => {
139
161
  if (this._isMuted === data.isMuted) return;
140
162
  this._isMuted = data.isMuted;
141
- const target = this._isMuted ? 0 : 1;
142
- this.contexts.forEach((gain, ctx) => {
143
- const now = ctx.currentTime;
144
- gain.gain.cancelScheduledValues(now);
145
- gain.gain.setValueAtTime(gain.gain.value, now);
146
- gain.gain.linearRampToValueAtTime(target, now + 0.05);
147
- });
148
- const setMutedNative = this.originalMutedDescriptor?.set;
149
- if (setMutedNative) {
150
- this.elements.forEach((el) => {
151
- const intended = this.intendedMuted.get(el) ?? false;
152
- setMutedNative.call(el, this._isMuted ? true : intended);
153
- });
154
- }
163
+ this.frames.forEach((shim) => shim.applyMute(this._isMuted));
155
164
  this.sdk.gameEventManager.notifyGame(WavedashEvents.MUTE_CHANGED, {
156
165
  isMuted: this._isMuted
157
166
  });
158
167
  };
159
- this.installShims();
168
+ if (typeof window !== "undefined") {
169
+ this.attachWindow(window);
170
+ }
160
171
  this.sdk.iframeMessenger.addEventListener(
161
172
  IFRAME_MESSAGE_TYPE.MUTE_CHANGED,
162
173
  this.handleMute
@@ -173,6 +184,12 @@ var AudioManager = class extends WavedashManager {
173
184
  * MUTE_CHANGED broadcast, so `isMuted()` updates independently of this result.
174
185
  */
175
186
  async requestMute(muted) {
187
+ if (!hasParentFrame()) {
188
+ logger.debug(
189
+ "requestMute() is disabled outside a Wavedash parent frame (e.g. `wavedash dev`)"
190
+ );
191
+ return false;
192
+ }
176
193
  const response = await this.sdk.iframeMessenger.requestFromParent(
177
194
  IFRAME_MESSAGE_TYPE.SET_MUTE,
178
195
  { muted }
@@ -185,15 +202,159 @@ var AudioManager = class extends WavedashManager {
185
202
  * host applied the change.
186
203
  */
187
204
  async toggleMute() {
205
+ if (!hasParentFrame()) {
206
+ logger.debug(
207
+ "toggleMute() is disabled outside a Wavedash parent frame (e.g. `wavedash dev`)"
208
+ );
209
+ return false;
210
+ }
188
211
  const response = await this.sdk.iframeMessenger.requestFromParent(
189
212
  IFRAME_MESSAGE_TYPE.TOGGLE_MUTE
190
213
  );
191
214
  return response.success;
192
215
  }
216
+ /** Shim a window we can reach. Same-origin only (cross-origin access throws). */
217
+ attachWindow(win) {
218
+ try {
219
+ void win.document;
220
+ } catch {
221
+ return;
222
+ }
223
+ const shim = new AudioFrameShim(this, win);
224
+ this.frames.add(shim);
225
+ }
226
+ /**
227
+ * Start tracking an iframe: attach now (already-loaded frames) and on every
228
+ * `load` (about:blank → game, and later src swaps). Idempotent.
229
+ */
230
+ bindIframe(iframe) {
231
+ if (!this.boundIframes.has(iframe)) {
232
+ this.boundIframes.add(iframe);
233
+ const onLoad = () => this.attachIframe(iframe);
234
+ this.iframeLoadHandlers.set(iframe, onLoad);
235
+ iframe.addEventListener("load", onLoad);
236
+ }
237
+ this.attachIframe(iframe);
238
+ }
239
+ /** Stop tracking an iframe and tear down its frame (iframe removed from DOM). */
240
+ unbindIframe(iframe) {
241
+ const handler = this.iframeLoadHandlers.get(iframe);
242
+ if (handler) {
243
+ iframe.removeEventListener("load", handler);
244
+ this.iframeLoadHandlers.delete(iframe);
245
+ }
246
+ this.boundIframes.delete(iframe);
247
+ this.teardownFrame(iframe);
248
+ }
193
249
  /**
194
- * Track a media element and (if SDK is currently muted) silence it.
195
- * Idempotent safe to call multiple times for the same element.
250
+ * Install (or re-install) a shim for an iframe's current document. No-ops
251
+ * while not yet navigated or already shimmed; replaces the previous shim when
252
+ * the iframe navigates to a fresh document, and drops it when it goes
253
+ * cross-origin (we can no longer reach it).
196
254
  */
255
+ attachIframe(iframe) {
256
+ let win = null;
257
+ let doc = null;
258
+ try {
259
+ const cw = iframe.contentWindow;
260
+ if (cw) {
261
+ doc = cw.document;
262
+ win = cw;
263
+ }
264
+ } catch {
265
+ }
266
+ if (!win || !doc) {
267
+ this.teardownFrame(iframe);
268
+ return;
269
+ }
270
+ const existing = this.iframeBindings.get(iframe);
271
+ if (existing) {
272
+ if (existing.doc === doc) return;
273
+ this.teardownFrame(iframe);
274
+ }
275
+ const shim = new AudioFrameShim(this, win);
276
+ this.frames.add(shim);
277
+ this.iframeBindings.set(iframe, { doc, shim });
278
+ if (this._isMuted) shim.applyMute(true);
279
+ }
280
+ /** Remove and uninstall the shim bound to an iframe's (previous) document. */
281
+ teardownFrame(iframe) {
282
+ const binding = this.iframeBindings.get(iframe);
283
+ if (!binding) return;
284
+ this.frames.delete(binding.shim);
285
+ binding.shim.uninstall();
286
+ this.iframeBindings.delete(iframe);
287
+ }
288
+ destroy() {
289
+ this.sdk.iframeMessenger.removeEventListener(
290
+ IFRAME_MESSAGE_TYPE.MUTE_CHANGED,
291
+ this.handleMute
292
+ );
293
+ this.boundIframes.forEach((iframe) => {
294
+ const handler = this.iframeLoadHandlers.get(iframe);
295
+ if (handler) iframe.removeEventListener("load", handler);
296
+ });
297
+ this.boundIframes.clear();
298
+ this.iframeLoadHandlers.clear();
299
+ this.frames.forEach((shim) => shim.uninstall());
300
+ this.frames.clear();
301
+ super.destroy();
302
+ }
303
+ };
304
+ var AudioFrameShim = class {
305
+ constructor(manager, win) {
306
+ this.contexts = /* @__PURE__ */ new Map();
307
+ // Tracked media elements + the game's intended muted value (what it last set).
308
+ this.elements = new WeakRefSet();
309
+ this.intendedMuted = /* @__PURE__ */ new WeakMap();
310
+ // Utterances + the game's intended volume.
311
+ this.intendedUtteranceVolume = /* @__PURE__ */ new WeakMap();
312
+ // Child iframes discovered in this frame's document. Tracked so we can
313
+ // cascade-unbind them when this frame is torn down — their own removal events
314
+ // never fire when the containing document is discarded wholesale.
315
+ this.boundChildren = /* @__PURE__ */ new Set();
316
+ // Originals, restored on uninstall.
317
+ this.originalAudioContext = null;
318
+ this.originalWebKitAudioContext = null;
319
+ this.originalAudio = null;
320
+ this.originalMutedDescriptor = null;
321
+ this.originalPlay = null;
322
+ this.originalSpeak = null;
323
+ this.originalUtteranceVolumeDescriptor = null;
324
+ this.mutationObserver = null;
325
+ this.manager = manager;
326
+ this.win = win;
327
+ this.doc = win.document ?? null;
328
+ this.installShims();
329
+ }
330
+ /** Push the current mute state onto everything this frame is tracking. */
331
+ applyMute(isMuted) {
332
+ const target = isMuted ? 0 : 1;
333
+ this.contexts.forEach((gain, ctx) => {
334
+ const now = ctx.currentTime;
335
+ gain.gain.cancelScheduledValues(now);
336
+ gain.gain.setValueAtTime(gain.gain.value, now);
337
+ gain.gain.linearRampToValueAtTime(target, now + 0.05);
338
+ });
339
+ const setMutedNative = this.originalMutedDescriptor?.set;
340
+ if (setMutedNative) {
341
+ this.elements.forEach((el) => {
342
+ const intended = this.intendedMuted.get(el) ?? false;
343
+ setMutedNative.call(el, isMuted ? true : intended);
344
+ });
345
+ }
346
+ }
347
+ /** Hand a discovered child iframe to the manager, remembering it for teardown. */
348
+ bindChild(iframe) {
349
+ this.boundChildren.add(iframe);
350
+ this.manager.bindIframe(iframe);
351
+ }
352
+ /** Stop tracking a child iframe that was removed from this document. */
353
+ unbindChild(iframe) {
354
+ this.boundChildren.delete(iframe);
355
+ this.manager.unbindIframe(iframe);
356
+ }
357
+ /** Track a media element and silence it if currently muted. Idempotent. */
197
358
  trackElement(el) {
198
359
  if (this.intendedMuted.has(el)) return;
199
360
  const getMuted = this.originalMutedDescriptor?.get;
@@ -201,139 +362,161 @@ var AudioManager = class extends WavedashManager {
201
362
  const current = getMuted ? getMuted.call(el) : el.muted;
202
363
  this.intendedMuted.set(el, current);
203
364
  this.elements.add(el);
204
- if (this._isMuted && !current && setMuted) {
365
+ if (this.manager.isMuted() && !current && setMuted) {
205
366
  setMuted.call(el, true);
206
367
  }
207
368
  }
208
369
  installShims() {
209
- if (typeof window === "undefined") return;
210
- if (window.AudioContext) {
211
- this.originalAudioContext = window.AudioContext;
212
- window.AudioContext = this.shimAudioContextClass(window.AudioContext);
213
- }
214
- const win = window;
215
- if (win.webkitAudioContext) {
216
- this.originalWebKitAudioContext = win.webkitAudioContext;
217
- win.webkitAudioContext = this.shimAudioContextClass(
218
- win.webkitAudioContext
219
- );
220
- }
221
- if (window.Audio) {
222
- const OriginalAudio = window.Audio;
370
+ const win = this.win;
371
+ const doc = this.doc;
372
+ if (win.AudioContext) {
373
+ this.originalAudioContext = win.AudioContext;
374
+ win.AudioContext = this.shimAudioContextClass(win.AudioContext);
375
+ }
376
+ const w = win;
377
+ if (w.webkitAudioContext) {
378
+ this.originalWebKitAudioContext = w.webkitAudioContext;
379
+ w.webkitAudioContext = this.shimAudioContextClass(w.webkitAudioContext);
380
+ }
381
+ if (win.Audio) {
382
+ const OriginalAudio = win.Audio;
223
383
  this.originalAudio = OriginalAudio;
224
- ((manager) => {
384
+ ((shim) => {
225
385
  const Shimmed = function(src) {
226
386
  const audio = new OriginalAudio(src);
227
- manager.trackElement(audio);
387
+ shim.trackElement(audio);
228
388
  return audio;
229
389
  };
230
390
  Shimmed.prototype = OriginalAudio.prototype;
231
- window.Audio = Shimmed;
391
+ win.Audio = Shimmed;
232
392
  })(this);
233
393
  }
234
- if (typeof document !== "undefined") {
235
- document.querySelectorAll("audio, video").forEach((el) => {
394
+ if (doc) {
395
+ const HTMLMediaElementCtor = win.HTMLMediaElement;
396
+ const HTMLIFrameElementCtor = win.HTMLIFrameElement;
397
+ const HTMLElementCtor = win.HTMLElement;
398
+ doc.querySelectorAll("audio, video").forEach((el) => {
236
399
  this.trackElement(el);
237
400
  });
238
- this.mutationObserver = new MutationObserver((mutations) => {
401
+ doc.querySelectorAll("iframe").forEach((el) => {
402
+ this.bindChild(el);
403
+ });
404
+ this.mutationObserver = new win.MutationObserver((mutations) => {
239
405
  for (const m of mutations) {
240
406
  m.addedNodes.forEach((node) => {
241
- if (node instanceof HTMLMediaElement) {
407
+ if (node instanceof HTMLMediaElementCtor) {
242
408
  this.trackElement(node);
243
- } else if (node instanceof HTMLElement) {
244
- node.querySelectorAll("audio, video").forEach((el) => {
245
- this.trackElement(el);
409
+ } else if (node instanceof HTMLIFrameElementCtor) {
410
+ this.bindChild(node);
411
+ } else if (node instanceof HTMLElementCtor) {
412
+ const el = node;
413
+ el.querySelectorAll("audio, video").forEach((m2) => {
414
+ this.trackElement(m2);
415
+ });
416
+ el.querySelectorAll("iframe").forEach((f) => {
417
+ this.bindChild(f);
418
+ });
419
+ }
420
+ });
421
+ m.removedNodes.forEach((node) => {
422
+ if (node instanceof HTMLIFrameElementCtor) {
423
+ this.unbindChild(node);
424
+ } else if (node instanceof HTMLElementCtor) {
425
+ node.querySelectorAll("iframe").forEach((f) => {
426
+ this.unbindChild(f);
246
427
  });
247
428
  }
248
429
  });
249
430
  }
250
431
  });
251
- this.mutationObserver.observe(document.documentElement, {
432
+ this.mutationObserver.observe(doc.documentElement, {
252
433
  childList: true,
253
434
  subtree: true
254
435
  });
255
436
  }
256
- this.originalMutedDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, "muted") ?? null;
437
+ this.originalMutedDescriptor = Object.getOwnPropertyDescriptor(
438
+ win.HTMLMediaElement.prototype,
439
+ "muted"
440
+ ) ?? null;
257
441
  const original = this.originalMutedDescriptor;
258
442
  if (original?.get && original?.set) {
259
- ((manager) => {
260
- Object.defineProperty(HTMLMediaElement.prototype, "muted", {
443
+ ((shim) => {
444
+ Object.defineProperty(win.HTMLMediaElement.prototype, "muted", {
261
445
  configurable: true,
262
446
  get() {
263
- const intended = manager.intendedMuted.get(this);
447
+ const intended = shim.intendedMuted.get(this);
264
448
  return intended !== void 0 ? intended : original.get.call(this);
265
449
  },
266
450
  set(value) {
267
- manager.intendedMuted.set(this, value);
268
- manager.elements.add(this);
269
- original.set.call(this, manager._isMuted ? true : value);
451
+ shim.intendedMuted.set(this, value);
452
+ shim.elements.add(this);
453
+ original.set.call(this, shim.manager.isMuted() ? true : value);
270
454
  }
271
455
  });
272
456
  })(this);
273
457
  }
274
- const originalPlay = HTMLMediaElement.prototype.play;
458
+ const originalPlay = win.HTMLMediaElement.prototype.play;
275
459
  this.originalPlay = originalPlay;
276
- ((manager) => {
277
- HTMLMediaElement.prototype.play = function() {
278
- manager.trackElement(this);
460
+ ((shim) => {
461
+ win.HTMLMediaElement.prototype.play = function() {
462
+ shim.trackElement(this);
279
463
  return originalPlay.call(this);
280
464
  };
281
465
  })(this);
282
466
  this.shimSpeechSynthesis();
283
467
  }
284
468
  /**
285
- * Shim `window.speechSynthesis` so speech respects the SDK mute state.
286
- *
287
- * Never swallows speak(): utterances have a lifecycle the game may sequence
288
- * off (onstart/onend, synth.speaking/pending checks), so every call is
289
- * delegated and silenced via volume instead. Volume is sampled at speak()
290
- * time, so forcing the native value to 0 right before delegating silences
291
- * anything spoken while muted; in-flight speech at the mute edge is
292
- * deliberately left to finish (can't be softened mid-utterance, and
293
- * cancel() would discard the pending queue).
469
+ * Shim `speechSynthesis`. We never swallow `speak()` (games sequence off its
470
+ * lifecycle) — instead we sample volume at call time and force it to 0 while
471
+ * muted. Speech already in flight at the mute edge is left to finish.
294
472
  */
295
473
  shimSpeechSynthesis() {
296
- if (!window.speechSynthesis || typeof SpeechSynthesisUtterance === "undefined") {
474
+ const win = this.win;
475
+ if (!win.speechSynthesis || typeof win.SpeechSynthesisUtterance === "undefined") {
297
476
  return;
298
477
  }
299
478
  this.originalUtteranceVolumeDescriptor = Object.getOwnPropertyDescriptor(
300
- SpeechSynthesisUtterance.prototype,
479
+ win.SpeechSynthesisUtterance.prototype,
301
480
  "volume"
302
481
  ) ?? null;
303
482
  const volDesc = this.originalUtteranceVolumeDescriptor;
304
483
  if (volDesc?.get && volDesc?.set) {
305
- ((manager) => {
306
- Object.defineProperty(SpeechSynthesisUtterance.prototype, "volume", {
307
- configurable: true,
308
- get() {
309
- const intended = manager.intendedUtteranceVolume.get(this);
310
- return intended !== void 0 ? intended : volDesc.get.call(this);
311
- },
312
- set(value) {
313
- manager.intendedUtteranceVolume.set(this, value);
314
- volDesc.set.call(this, value);
484
+ ((shim) => {
485
+ Object.defineProperty(
486
+ win.SpeechSynthesisUtterance.prototype,
487
+ "volume",
488
+ {
489
+ configurable: true,
490
+ get() {
491
+ const intended = shim.intendedUtteranceVolume.get(this);
492
+ return intended !== void 0 ? intended : volDesc.get.call(this);
493
+ },
494
+ set(value) {
495
+ shim.intendedUtteranceVolume.set(this, value);
496
+ volDesc.set.call(this, value);
497
+ }
315
498
  }
316
- });
499
+ );
317
500
  })(this);
318
501
  }
319
- const speechSynthesis = window.speechSynthesis;
502
+ const speechSynthesis = win.speechSynthesis;
320
503
  const originalSpeak = speechSynthesis.speak;
321
504
  this.originalSpeak = originalSpeak;
322
- ((manager) => {
505
+ ((shim) => {
323
506
  speechSynthesis.speak = function(utterance) {
324
- if (manager._isMuted) {
325
- if (!manager.intendedUtteranceVolume.has(utterance)) {
507
+ if (shim.manager.isMuted()) {
508
+ if (!shim.intendedUtteranceVolume.has(utterance)) {
326
509
  const current = volDesc?.get ? volDesc.get.call(utterance) : utterance.volume;
327
- manager.intendedUtteranceVolume.set(utterance, current);
510
+ shim.intendedUtteranceVolume.set(utterance, current);
328
511
  }
329
512
  if (volDesc?.set) volDesc.set.call(utterance, 0);
330
513
  else utterance.volume = 0;
331
514
  } else {
332
- const intended = manager.intendedUtteranceVolume.get(utterance);
515
+ const intended = shim.intendedUtteranceVolume.get(utterance);
333
516
  if (intended !== void 0) {
334
517
  if (volDesc?.set) volDesc.set.call(utterance, intended);
335
518
  else utterance.volume = intended;
336
- manager.intendedUtteranceVolume.delete(utterance);
519
+ shim.intendedUtteranceVolume.delete(utterance);
337
520
  }
338
521
  }
339
522
  return originalSpeak.call(speechSynthesis, utterance);
@@ -341,13 +524,13 @@ var AudioManager = class extends WavedashManager {
341
524
  })(this);
342
525
  }
343
526
  shimAudioContextClass(Original) {
344
- return /* @__PURE__ */ ((manager) => class extends Original {
527
+ return /* @__PURE__ */ ((shim) => class extends Original {
345
528
  constructor(opts) {
346
529
  super(opts);
347
530
  const masterGain = this.createGain();
348
531
  masterGain.connect(this.destination);
349
532
  masterGain.gain.setValueAtTime(
350
- manager._isMuted ? 0 : 1,
533
+ shim.manager.isMuted() ? 0 : 1,
351
534
  this.currentTime
352
535
  );
353
536
  Object.defineProperty(this, "destination", {
@@ -356,60 +539,101 @@ var AudioManager = class extends WavedashManager {
356
539
  return masterGain;
357
540
  }
358
541
  });
359
- manager.contexts.set(this, masterGain);
542
+ shim.contexts.set(this, masterGain);
360
543
  }
361
544
  close() {
362
- manager.contexts.delete(this);
545
+ shim.contexts.delete(this);
363
546
  return super.close();
364
547
  }
365
548
  })(this);
366
549
  }
367
- destroy() {
368
- this.sdk.iframeMessenger.removeEventListener(
369
- IFRAME_MESSAGE_TYPE.MUTE_CHANGED,
370
- this.handleMute
371
- );
372
- if (this.mutationObserver) {
373
- this.mutationObserver.disconnect();
374
- this.mutationObserver = null;
550
+ /**
551
+ * Restore the globals we patched. Best-effort per statement: a frame reached
552
+ * through an iframe may have navigated away (globals gone) before teardown.
553
+ */
554
+ uninstall() {
555
+ const win = this.win;
556
+ try {
557
+ if (this.mutationObserver) {
558
+ this.mutationObserver.disconnect();
559
+ this.mutationObserver = null;
560
+ }
561
+ } catch {
375
562
  }
376
- if (typeof window !== "undefined") {
377
- if (this.originalAudioContext) {
378
- window.AudioContext = this.originalAudioContext;
563
+ this.boundChildren.forEach((child) => this.manager.unbindIframe(child));
564
+ this.boundChildren.clear();
565
+ const restore = (fn) => {
566
+ try {
567
+ fn();
568
+ } catch {
379
569
  }
380
- const win = window;
381
- if (this.originalWebKitAudioContext && win.webkitAudioContext) {
382
- win.webkitAudioContext = this.originalWebKitAudioContext;
570
+ };
571
+ restore(() => {
572
+ if (this.originalAudioContext)
573
+ win.AudioContext = this.originalAudioContext;
574
+ });
575
+ restore(() => {
576
+ const w = win;
577
+ if (this.originalWebKitAudioContext && w.webkitAudioContext) {
578
+ w.webkitAudioContext = this.originalWebKitAudioContext;
383
579
  }
384
- if (this.originalAudio) {
385
- window.Audio = this.originalAudio;
580
+ });
581
+ restore(() => {
582
+ if (this.originalAudio) win.Audio = this.originalAudio;
583
+ });
584
+ restore(() => {
585
+ if (this.originalSpeak && win.speechSynthesis) {
586
+ win.speechSynthesis.speak = this.originalSpeak;
386
587
  }
387
- if (this.originalSpeak && window.speechSynthesis) {
388
- window.speechSynthesis.speak = this.originalSpeak;
588
+ });
589
+ restore(() => {
590
+ if (this.originalUtteranceVolumeDescriptor && typeof win.SpeechSynthesisUtterance !== "undefined") {
591
+ Object.defineProperty(
592
+ win.SpeechSynthesisUtterance.prototype,
593
+ "volume",
594
+ this.originalUtteranceVolumeDescriptor
595
+ );
389
596
  }
390
- }
391
- if (this.originalUtteranceVolumeDescriptor && typeof SpeechSynthesisUtterance !== "undefined") {
392
- Object.defineProperty(
393
- SpeechSynthesisUtterance.prototype,
394
- "volume",
395
- this.originalUtteranceVolumeDescriptor
396
- );
397
- }
398
- if (this.originalPlay) {
399
- HTMLMediaElement.prototype.play = this.originalPlay;
400
- }
401
- if (this.originalMutedDescriptor) {
402
- Object.defineProperty(
403
- HTMLMediaElement.prototype,
404
- "muted",
405
- this.originalMutedDescriptor
406
- );
407
- }
597
+ });
598
+ restore(() => {
599
+ if (this.originalPlay) {
600
+ win.HTMLMediaElement.prototype.play = this.originalPlay;
601
+ }
602
+ });
603
+ restore(() => {
604
+ if (this.originalMutedDescriptor) {
605
+ Object.defineProperty(
606
+ win.HTMLMediaElement.prototype,
607
+ "muted",
608
+ this.originalMutedDescriptor
609
+ );
610
+ }
611
+ });
408
612
  this.contexts.clear();
409
613
  this.elements.clear();
410
614
  this.intendedMuted = /* @__PURE__ */ new WeakMap();
411
615
  this.intendedUtteranceVolume = /* @__PURE__ */ new WeakMap();
412
- super.destroy();
616
+ }
617
+ };
618
+ var WeakRefSet = class {
619
+ constructor() {
620
+ this.set = /* @__PURE__ */ new Set();
621
+ }
622
+ add(value) {
623
+ for (const ref of this.set) {
624
+ if (ref.deref() === value) return;
625
+ }
626
+ this.set.add(new WeakRef(value));
627
+ }
628
+ forEach(callback) {
629
+ for (const ref of this.set) {
630
+ const v = ref.deref();
631
+ if (v === void 0) this.set.delete(ref);
632
+ else callback(v);
633
+ }
634
+ }
635
+ clear() {
636
+ this.set.clear();
413
637
  }
414
638
  };
415
639
 
@@ -483,44 +707,6 @@ function toBlobFromIndexedDBValue(value) {
483
707
  throw new Error("Unrecognized value shape from IndexedDB");
484
708
  }
485
709
 
486
- // src/utils/logger.ts
487
- var LOG_LEVEL = {
488
- DEBUG: 0,
489
- // Most verbose
490
- INFO: 1,
491
- WARN: 2,
492
- ERROR: 3
493
- };
494
- var WavedashLogger = class {
495
- constructor(logLevel = LOG_LEVEL.WARN) {
496
- this.logLevel = logLevel;
497
- }
498
- setLogLevel(level) {
499
- this.logLevel = level;
500
- }
501
- debug(message, ...args) {
502
- if (this.logLevel <= LOG_LEVEL.DEBUG) {
503
- console.log(`[WavedashJS] ${message}`, ...args);
504
- }
505
- }
506
- info(message, ...args) {
507
- if (this.logLevel <= LOG_LEVEL.INFO) {
508
- console.log(`[WavedashJS] ${message}`, ...args);
509
- }
510
- }
511
- warn(message, ...args) {
512
- if (this.logLevel <= LOG_LEVEL.WARN) {
513
- console.warn(`[WavedashJS] ${message}`, ...args);
514
- }
515
- }
516
- error(message, ...args) {
517
- if (this.logLevel <= LOG_LEVEL.ERROR) {
518
- console.error(`[WavedashJS] ${message}`, ...args);
519
- }
520
- }
521
- };
522
- var logger = new WavedashLogger();
523
-
524
710
  // src/services/fileSystem.ts
525
711
  import { api } from "@wvdsh/api";
526
712
  var REMOTE_STORAGE_FOLDER = "userfs";
@@ -967,6 +1153,7 @@ var FullscreenManager = class extends WavedashManager {
967
1153
  super(sdk);
968
1154
  this._isFullscreen = false;
969
1155
  this.listeners = /* @__PURE__ */ new Set();
1156
+ if (!hasParentFrame()) return;
970
1157
  this.sdk.iframeMessenger.addEventListener(
971
1158
  IFRAME_MESSAGE_TYPE2.FULLSCREEN_CHANGED,
972
1159
  (data) => {
@@ -988,6 +1175,12 @@ var FullscreenManager = class extends WavedashManager {
988
1175
  * (e.g. browser rejected for lack of user activation).
989
1176
  */
990
1177
  async requestFullscreen(fullscreen) {
1178
+ if (!hasParentFrame()) {
1179
+ logger.debug(
1180
+ "requestFullscreen() is disabled outside a Wavedash parent frame (e.g. `wavedash dev`)"
1181
+ );
1182
+ return false;
1183
+ }
991
1184
  const response = await this.sdk.iframeMessenger.requestFromParent(
992
1185
  IFRAME_MESSAGE_TYPE2.SET_FULLSCREEN,
993
1186
  { fullscreen }
@@ -995,6 +1188,12 @@ var FullscreenManager = class extends WavedashManager {
995
1188
  return response.success;
996
1189
  }
997
1190
  async toggleFullscreen() {
1191
+ if (!hasParentFrame()) {
1192
+ logger.debug(
1193
+ "toggleFullscreen() is disabled outside a Wavedash parent frame (e.g. `wavedash dev`)"
1194
+ );
1195
+ return false;
1196
+ }
998
1197
  const response = await this.sdk.iframeMessenger.requestFromParent(
999
1198
  IFRAME_MESSAGE_TYPE2.TOGGLE_FULLSCREEN
1000
1199
  );
@@ -1157,7 +1356,7 @@ var HeartbeatManager = class extends WavedashManager {
1157
1356
  this.gamepadPollInterval = setInterval(() => {
1158
1357
  this.pollGamepads();
1159
1358
  }, this.GAMEPAD_POLL_INTERVAL_MS);
1160
- this.deviceFingerprintReady = this.sdk.iframeMessenger.requestFromParent(IFRAME_MESSAGE_TYPE3.GET_DEVICE_FINGERPRINT).then((fingerprint) => {
1359
+ this.deviceFingerprintReady = !hasParentFrame() ? Promise.resolve() : this.sdk.iframeMessenger.requestFromParent(IFRAME_MESSAGE_TYPE3.GET_DEVICE_FINGERPRINT).then((fingerprint) => {
1161
1360
  this.deviceFingerprint = fingerprint;
1162
1361
  }).catch(() => {
1163
1362
  });
@@ -1706,6 +1905,11 @@ var _LobbyManager = class _LobbyManager extends WavedashManager {
1706
1905
  if (!this.lobbyId) {
1707
1906
  throw new Error("User is not in a lobby");
1708
1907
  }
1908
+ if (!hasParentFrame()) {
1909
+ throw new Error(
1910
+ "Lobby invite links are not available outside a Wavedash parent frame (e.g. `wavedash dev`)"
1911
+ );
1912
+ }
1709
1913
  const inviteLink = await this.sdk.iframeMessenger.requestFromParent(
1710
1914
  IFRAME_MESSAGE_TYPE4.GET_LOBBY_INVITE_LINK,
1711
1915
  { lobbyId: this.lobbyId, copyToClipboard }
@@ -1971,6 +2175,7 @@ var OverlayManager = class extends WavedashManager {
1971
2175
  this.toggleOverlay();
1972
2176
  }
1973
2177
  };
2178
+ if (!hasParentFrame()) return;
1974
2179
  this.sdk.iframeMessenger.addEventListener(
1975
2180
  IFRAME_MESSAGE_TYPE5.TAKE_FOCUS,
1976
2181
  takeFocus
@@ -3301,7 +3506,76 @@ _P2PManager.MEMORY_WARNING_THRESHOLD_BYTES = 128 * 1024 * 1024;
3301
3506
  var P2PManager = _P2PManager;
3302
3507
 
3303
3508
  // src/services/paidContent.ts
3304
- import { IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE6 } from "@wvdsh/api";
3509
+ import { api as api7, IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE6 } from "@wvdsh/api";
3510
+
3511
+ // src/utils/devPaywall.ts
3512
+ var Z_INDEX = 2147483647;
3513
+ function showDevPaywall(contentIdentifier) {
3514
+ if (typeof document === "undefined") return Promise.resolve(false);
3515
+ return new Promise((resolve) => {
3516
+ const overlay = document.createElement("div");
3517
+ overlay.style.cssText = "position:fixed;inset:0;z-index:" + Z_INDEX + ";display:flex;align-items:center;justify-content:center;background:rgba(8,10,18,0.72);font:14px ui-sans-serif,system-ui,sans-serif;color:#e2e8f0";
3518
+ const card = document.createElement("div");
3519
+ card.style.cssText = "max-width:420px;width:calc(100% - 48px);background:#11151f;border:1px solid #2a3344;border-radius:12px;padding:24px;box-shadow:0 20px 60px rgba(0,0,0,0.5);box-sizing:border-box";
3520
+ const badge = document.createElement("p");
3521
+ badge.textContent = "wavedash dev \u2014 simulated purchase";
3522
+ badge.style.cssText = "margin:0 0 12px;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:#7c8aa5";
3523
+ const title = document.createElement("p");
3524
+ title.textContent = "Unlock paid content?";
3525
+ title.style.cssText = "margin:0 0 8px;font-size:18px;font-weight:600;color:#f1f5f9";
3526
+ const body = document.createElement("p");
3527
+ body.style.cssText = "margin:0 0 20px;line-height:1.5;color:#cbd5e1";
3528
+ body.append(
3529
+ document.createTextNode("This game is requesting purchase of \u201C")
3530
+ );
3531
+ const id = document.createElement("code");
3532
+ id.textContent = contentIdentifier;
3533
+ id.style.cssText = "background:#1c2433;border-radius:4px;padding:1px 6px;color:#93c5fd";
3534
+ body.append(
3535
+ id,
3536
+ document.createTextNode(
3537
+ "\u201D. No real payment happens in dev \u2014 simulate the outcome to test your flow."
3538
+ )
3539
+ );
3540
+ const buttons = document.createElement("div");
3541
+ buttons.style.cssText = "display:flex;gap:12px;justify-content:flex-end";
3542
+ const baseBtn = "border-radius:8px;padding:9px 18px;font-size:14px;font-weight:600;cursor:pointer;border:1px solid transparent";
3543
+ const cancel = document.createElement("button");
3544
+ cancel.type = "button";
3545
+ cancel.textContent = "Cancel";
3546
+ cancel.style.cssText = baseBtn + ";background:transparent;border-color:#374151;color:#cbd5e1";
3547
+ const buy = document.createElement("button");
3548
+ buy.type = "button";
3549
+ buy.textContent = "Simulate purchase";
3550
+ buy.style.cssText = baseBtn + ";background:#2563eb;color:#fff";
3551
+ buttons.append(cancel, buy);
3552
+ card.append(badge, title, body, buttons);
3553
+ overlay.append(card);
3554
+ let settled = false;
3555
+ const finish = (purchased) => {
3556
+ if (settled) return;
3557
+ settled = true;
3558
+ document.removeEventListener("keydown", onKeyDown, true);
3559
+ overlay.remove();
3560
+ resolve(purchased);
3561
+ };
3562
+ const onKeyDown = (event) => {
3563
+ if (event.key === "Escape") {
3564
+ event.preventDefault();
3565
+ finish(false);
3566
+ }
3567
+ };
3568
+ cancel.addEventListener("click", () => finish(false));
3569
+ buy.addEventListener("click", () => finish(true));
3570
+ overlay.addEventListener("click", (event) => {
3571
+ if (event.target === overlay) finish(false);
3572
+ });
3573
+ document.addEventListener("keydown", onKeyDown, true);
3574
+ document.body.append(overlay);
3575
+ });
3576
+ }
3577
+
3578
+ // src/services/paidContent.ts
3305
3579
  var PAYWALL_TIMEOUT_MS = 10 * 60 * 1e3;
3306
3580
  function decodeJwtPayload(jwt) {
3307
3581
  try {
@@ -3343,6 +3617,22 @@ var PaidContentManager = class extends WavedashManager {
3343
3617
  }
3344
3618
  this.paywallOpen = true;
3345
3619
  this.restorePointerLock = suspendPointerLock();
3620
+ if (!hasParentFrame()) {
3621
+ let purchased;
3622
+ try {
3623
+ purchased = await showDevPaywall(contentIdentifier);
3624
+ } finally {
3625
+ this.restorePointerLock?.();
3626
+ this.restorePointerLock = void 0;
3627
+ this.paywallOpen = false;
3628
+ }
3629
+ if (!purchased) return false;
3630
+ await this.sdk.convexClient.mutation(api7.sdk.paidContent.mockPurchase, {
3631
+ contentIdentifier
3632
+ });
3633
+ await this.sdk.ensureGameplayJwt(true);
3634
+ return true;
3635
+ }
3346
3636
  let response;
3347
3637
  try {
3348
3638
  response = await this.sdk.iframeMessenger.requestFromParent(
@@ -3369,7 +3659,7 @@ var PaidContentManager = class extends WavedashManager {
3369
3659
  };
3370
3660
 
3371
3661
  // src/services/stats.ts
3372
- import { api as api7 } from "@wvdsh/api";
3662
+ import { api as api8 } from "@wvdsh/api";
3373
3663
  import throttle2 from "lodash.throttle";
3374
3664
  var STORE_THROTTLE_MS = 1e3;
3375
3665
  var StatsManager = class extends WavedashManager {
@@ -3427,7 +3717,7 @@ var StatsManager = class extends WavedashManager {
3427
3717
  subscribe() {
3428
3718
  this.subscriptions.push(
3429
3719
  this.sdk.convexClient.onUpdate(
3430
- api7.sdk.gameAchievements.listStatIdentifiers,
3720
+ api8.sdk.gameAchievements.listStatIdentifiers,
3431
3721
  {},
3432
3722
  (ids) => {
3433
3723
  this.knownStatIds = new Set(ids);
@@ -3437,7 +3727,7 @@ var StatsManager = class extends WavedashManager {
3437
3727
  }
3438
3728
  ),
3439
3729
  this.sdk.convexClient.onUpdate(
3440
- api7.sdk.gameAchievements.listAchievementIdentifiers,
3730
+ api8.sdk.gameAchievements.listAchievementIdentifiers,
3441
3731
  {},
3442
3732
  (ids) => {
3443
3733
  this.knownAchievementIds = new Set(ids);
@@ -3447,7 +3737,7 @@ var StatsManager = class extends WavedashManager {
3447
3737
  }
3448
3738
  ),
3449
3739
  this.sdk.convexClient.onUpdate(
3450
- api7.sdk.gameAchievements.getMyAchievementsForGame,
3740
+ api8.sdk.gameAchievements.getMyAchievementsForGame,
3451
3741
  {},
3452
3742
  (achievements) => {
3453
3743
  this.loaded.achievements = true;
@@ -3463,7 +3753,7 @@ var StatsManager = class extends WavedashManager {
3463
3753
  }
3464
3754
  async requestStats() {
3465
3755
  const newStats = await this.sdk.convexClient.query(
3466
- api7.sdk.gameAchievements.getMyStatsForGame,
3756
+ api8.sdk.gameAchievements.getMyStatsForGame,
3467
3757
  {}
3468
3758
  );
3469
3759
  this.loaded.stats = true;
@@ -3494,11 +3784,11 @@ var StatsManager = class extends WavedashManager {
3494
3784
  if (!pending) return;
3495
3785
  this.inFlightPersist = Promise.all([
3496
3786
  pending.stats.length > 0 ? this.sdk.convexClient.mutation(
3497
- api7.sdk.gameAchievements.setUserGameStats,
3787
+ api8.sdk.gameAchievements.setUserGameStats,
3498
3788
  { stats: pending.stats }
3499
3789
  ) : Promise.resolve(),
3500
3790
  pending.achievements.length > 0 ? this.sdk.convexClient.mutation(
3501
- api7.sdk.gameAchievements.setUserGameAchievements,
3791
+ api8.sdk.gameAchievements.setUserGameAchievements,
3502
3792
  { achievements: pending.achievements }
3503
3793
  ) : Promise.resolve()
3504
3794
  ]).then(() => {
@@ -3575,14 +3865,14 @@ var StatsManager = class extends WavedashManager {
3575
3865
  };
3576
3866
 
3577
3867
  // src/services/ugc.ts
3578
- import { api as api8 } from "@wvdsh/api";
3868
+ import { api as api9 } from "@wvdsh/api";
3579
3869
  var UGCManager = class extends WavedashManager {
3580
3870
  constructor(sdk) {
3581
3871
  super(sdk);
3582
3872
  }
3583
3873
  async createUGCItem(ugcType, title, description, visibility, filePath) {
3584
3874
  const { ugcId, uploadUrl } = await this.sdk.convexClient.mutation(
3585
- api8.sdk.userGeneratedContent.createUGCItem,
3875
+ api9.sdk.userGeneratedContent.createUGCItem,
3586
3876
  {
3587
3877
  ugcType,
3588
3878
  title,
@@ -3609,7 +3899,7 @@ var UGCManager = class extends WavedashManager {
3609
3899
  async updateUGCItem(ugcId, updates = {}) {
3610
3900
  const { title, description, visibility, filePath } = updates;
3611
3901
  const { uploadUrl } = await this.sdk.convexClient.mutation(
3612
- api8.sdk.userGeneratedContent.updateUGCItem,
3902
+ api9.sdk.userGeneratedContent.updateUGCItem,
3613
3903
  {
3614
3904
  ugcId,
3615
3905
  title,
@@ -3635,14 +3925,14 @@ var UGCManager = class extends WavedashManager {
3635
3925
  }
3636
3926
  async deleteUGCItem(ugcId) {
3637
3927
  await this.sdk.convexClient.mutation(
3638
- api8.sdk.userGeneratedContent.deleteUGCItem,
3928
+ api9.sdk.userGeneratedContent.deleteUGCItem,
3639
3929
  { ugcId }
3640
3930
  );
3641
3931
  return ugcId;
3642
3932
  }
3643
3933
  async downloadUGCItem(ugcId, filePath) {
3644
3934
  const downloadUrl = await this.sdk.convexClient.query(
3645
- api8.sdk.userGeneratedContent.getUGCItemDownloadUrl,
3935
+ api9.sdk.userGeneratedContent.getUGCItemDownloadUrl,
3646
3936
  { ugcId }
3647
3937
  );
3648
3938
  try {
@@ -3657,7 +3947,7 @@ var UGCManager = class extends WavedashManager {
3657
3947
  const { createdBy, ugcType, titleSearch, numItems, continueCursor } = args;
3658
3948
  const filters = createdBy !== void 0 || ugcType !== void 0 || titleSearch !== void 0 ? { createdBy, ugcType, titleSearch } : void 0;
3659
3949
  return await this.sdk.convexClient.query(
3660
- api8.sdk.userGeneratedContent.listUGCItems,
3950
+ api9.sdk.userGeneratedContent.listUGCItems,
3661
3951
  {
3662
3952
  filters,
3663
3953
  numItems,
@@ -3667,15 +3957,6 @@ var UGCManager = class extends WavedashManager {
3667
3957
  }
3668
3958
  };
3669
3959
 
3670
- // src/utils/parentOrigin.ts
3671
- var _parentOrigin = "";
3672
- function setParentOrigin(origin) {
3673
- _parentOrigin = origin;
3674
- }
3675
- function getParentOrigin() {
3676
- return _parentOrigin;
3677
- }
3678
-
3679
3960
  // src/utils/iframeMessenger.ts
3680
3961
  var RESPONSE_TIMEOUT_MS = 15e3;
3681
3962
  var IFrameMessenger = class {
@@ -5119,6 +5400,7 @@ var WavedashSDK = class extends EventTarget {
5119
5400
  const refreshQuery = new URLSearchParams({
5120
5401
  [UrlParams.Caller]: PlayRouteCaller.Wavedash
5121
5402
  });
5403
+ if (forceRefresh) refreshQuery.set("fresh", "1");
5122
5404
  const refreshPath = `/auth/refresh?${refreshQuery.toString()}`;
5123
5405
  const response = await fetch(refreshPath, {
5124
5406
  method: "POST",