Skip to content

Commit 21f3efa

Browse files
authored
refactor: introduce ActivityContext and de-globalize Activity access (#5936)
1 parent d373588 commit 21f3efa

File tree

4 files changed

+147
-72
lines changed

4 files changed

+147
-72
lines changed

js/activity-context.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* ActivityContext
3+
*
4+
* Single authority for accessing the runtime Activity instance.
5+
*
6+
* NOTE: This repo uses RequireJS shims + globals for browser builds.
7+
* This module supports AMD (RequireJS), CommonJS (Jest), and a browser global
8+
* fallback (ActivityContext) without exporting the Activity instance onto
9+
* window.activity.
10+
*/
11+
12+
(function (root, factory) {
13+
// Lazy singleton: factory runs once, under the loader's control when
14+
// possible (AMD), but always exposes a global for non-AMD consumers.
15+
let _mod;
16+
17+
function getModule() {
18+
if (!_mod) _mod = factory();
19+
return _mod;
20+
}
21+
22+
if (typeof define === "function" && define.amd) {
23+
define([], function () {
24+
return getModule();
25+
});
26+
} else if (typeof module !== "undefined" && module.exports) {
27+
module.exports = getModule();
28+
}
29+
30+
// Ensure a global reference exists so non-AMD code (activity.js, synthutils.js)
31+
// can access it immediately.
32+
try {
33+
root.ActivityContext = getModule();
34+
} catch (e) {
35+
// ignore if root is not writable in some hostile environments
36+
}
37+
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
38+
"use strict";
39+
40+
let _activity = null;
41+
42+
function setActivity(activityInstance) {
43+
if (!activityInstance) {
44+
throw new Error("Cannot set ActivityContext with a falsy value");
45+
}
46+
_activity = activityInstance;
47+
}
48+
49+
function getActivity() {
50+
if (!_activity) {
51+
throw new Error(
52+
"Activity not initialized yet. Use dependency injection or wait for initialization."
53+
);
54+
}
55+
return _activity;
56+
}
57+
58+
return { setActivity, getActivity };
59+
});

js/activity.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2969,7 +2969,19 @@ class Activity {
29692969

29702970
// Expose activity instance for external checks
29712971
if (typeof window !== "undefined") {
2972-
window.activity = this;
2972+
// Single authority: ActivityContext
2973+
// TODO: window.activity is deprecated; use ActivityContext instead
2974+
if (
2975+
window.ActivityContext &&
2976+
typeof window.ActivityContext.setActivity === "function"
2977+
) {
2978+
window.ActivityContext.setActivity(this);
2979+
}
2980+
2981+
// TEMP compatibility bridge
2982+
if (!window.activity) {
2983+
window.activity = this;
2984+
}
29732985
}
29742986
};
29752987

@@ -3034,7 +3046,19 @@ class Activity {
30343046

30353047
// Expose activity instance for external checks
30363048
if (typeof window !== "undefined") {
3037-
window.activity = this;
3049+
// Single authority: ActivityContext
3050+
// TODO: window.activity is deprecated; use ActivityContext instead
3051+
if (
3052+
window.ActivityContext &&
3053+
typeof window.ActivityContext.setActivity === "function"
3054+
) {
3055+
window.ActivityContext.setActivity(this);
3056+
}
3057+
3058+
// TEMP compatibility bridge
3059+
if (!window.activity) {
3060+
window.activity = this;
3061+
}
30383062
}
30393063
};
30403064

js/loader.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ requirejs.config({
8383
exports: "Notation"
8484
},
8585
"utils/synthutils": {
86-
deps: ["utils/utils"],
86+
deps: ["utils/utils", "activity/activity-context"],
8787
exports: "Synth"
8888
},
8989
"activity/logo": {
@@ -96,7 +96,13 @@ requirejs.config({
9696
exports: "Logo"
9797
},
9898
"activity/activity": {
99-
deps: ["utils/utils", "activity/logo", "activity/blocks", "activity/turtles"],
99+
deps: [
100+
"utils/utils",
101+
"activity/activity-context",
102+
"activity/logo",
103+
"activity/blocks",
104+
"activity/turtles"
105+
],
100106
exports: "Activity"
101107
},
102108
"materialize": {

js/utils/synthutils.js

Lines changed: 54 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2378,24 +2378,39 @@ function Synth() {
23782378
* @returns {Promise<void>}
23792379
*/
23802380
this.startTuner = async () => {
2381-
// Initialize required components for pie menu
2382-
if (!window.activity) {
2383-
window.activity = {
2384-
blocks: {
2385-
blockList: [],
2386-
setPitchOctave: () => {},
2387-
findPitchOctave: () => 4,
2388-
stageClick: false
2389-
},
2390-
logo: {
2391-
synth: this
2392-
},
2393-
canvas: document.createElement("canvas"),
2394-
blocksContainer: { x: 0, y: 0 },
2395-
getStageScale: () => 1,
2396-
KeySignatureEnv: ["A", "major", false]
2397-
};
2398-
}
2381+
const getSafeActivity = () => {
2382+
try {
2383+
if (
2384+
typeof window !== "undefined" &&
2385+
window.ActivityContext &&
2386+
typeof window.ActivityContext.getActivity === "function"
2387+
) {
2388+
return window.ActivityContext.getActivity();
2389+
}
2390+
} catch (e) {
2391+
// Fall through to warning below.
2392+
}
2393+
2394+
// In practice this runs in the browser; keep a safe fallback for tests.
2395+
try {
2396+
if (typeof module !== "undefined" && module.exports) {
2397+
// eslint-disable-next-line global-require
2398+
const ctx = require("../activity-context");
2399+
if (ctx && typeof ctx.getActivity === "function") {
2400+
return ctx.getActivity();
2401+
}
2402+
}
2403+
} catch (e) {
2404+
// Ignore.
2405+
}
2406+
2407+
console.warn("Activity not ready yet in synthutils");
2408+
return null;
2409+
};
2410+
2411+
// No fake globals: if Activity isn't ready, fail fast.
2412+
const activity = getSafeActivity();
2413+
if (!activity) return;
23992414

24002415
// Initialize wheelnav if not already done
24012416
if (typeof wheelnav !== "function") {
@@ -2676,13 +2691,30 @@ function Synth() {
26762691
}
26772692

26782693
try {
2679-
// Create a temporary block object to use with piemenuPitches
2694+
// Prepare a non-mutating activity proxy with a local logo fallback
2695+
const defaultLogo = {
2696+
synth: {
2697+
createDefaultSynth: () => {},
2698+
loadSynth: () => {},
2699+
setMasterVolume: () => {},
2700+
trigger: () => {},
2701+
inTemperament: "equal"
2702+
},
2703+
errorMsg: msg => {
2704+
console.warn(msg);
2705+
}
2706+
};
2707+
2708+
const logo = activity.logo || defaultLogo;
2709+
const activityProxy = Object.create(activity);
2710+
activityProxy.logo = logo;
2711+
26802712
const tempBlock = {
26812713
container: {
26822714
x: targetNoteSelector.offsetLeft,
26832715
y: targetNoteSelector.offsetTop
26842716
},
2685-
activity: window.activity,
2717+
activity: activityProxy,
26862718
blocks: {
26872719
blockList: [
26882720
{
@@ -2730,54 +2762,8 @@ function Synth() {
27302762
_triggerLock: false // This is needed for pitch preview
27312763
};
27322764

2733-
// Add required activity properties for preview
2734-
if (!window.activity.logo) {
2735-
window.activity.logo = {
2736-
synth: {
2737-
createDefaultSynth: () => {},
2738-
loadSynth: () => {},
2739-
setMasterVolume: () => {},
2740-
trigger: (turtle, note, duration, instrument) => {
2741-
// Use the Web Audio API to play the preview note
2742-
const audioContext = new (window.AudioContext ||
2743-
window.webkitAudioContext)();
2744-
const oscillator = audioContext.createOscillator();
2745-
const gainNode = audioContext.createGain();
2746-
2747-
oscillator.connect(gainNode);
2748-
gainNode.connect(audioContext.destination);
2749-
2750-
// Convert note to frequency
2751-
const freq = pitchToFrequency(note[0], "equal");
2752-
oscillator.frequency.value = freq;
2753-
2754-
// Set volume
2755-
gainNode.gain.value = 0.1; // Low volume for preview
2756-
2757-
// Schedule note
2758-
oscillator.start();
2759-
gainNode.gain.setValueAtTime(
2760-
0.1,
2761-
audioContext.currentTime
2762-
);
2763-
gainNode.gain.linearRampToValueAtTime(
2764-
0,
2765-
audioContext.currentTime + duration
2766-
);
2767-
oscillator.stop(
2768-
audioContext.currentTime + duration
2769-
);
2770-
},
2771-
inTemperament: "equal"
2772-
},
2773-
errorMsg: msg => {
2774-
console.warn(msg);
2775-
}
2776-
};
2777-
}
2778-
2779-
// Add key signature environment
2780-
window.activity.KeySignatureEnv = ["C", "major", false];
2765+
// Add key signature environment (on proxy, not real activity)
2766+
activityProxy.KeySignatureEnv = ["C", "major", false];
27812767

27822768
// Make sure wheelDiv is properly positioned and visible
27832769
const wheelDiv = docById("wheelDiv");

0 commit comments

Comments
 (0)